diff --git a/.fossa.yml b/.fossa.yml index 26ba9356136b..e3901d1f122b 100644 --- a/.fossa.yml +++ b/.fossa.yml @@ -280,6 +280,12 @@ targets: - type: gradle path: ./ target: ':instrumentation:scala-fork-join-2.8:javaagent' + - type: gradle + path: ./ + target: ':instrumentation:sofa-rpc-5.4:javaagent' + - type: gradle + path: ./ + target: ':instrumentation:sofa-rpc-5.4:library-autoconfigure' - type: gradle path: ./ target: ':instrumentation:spark-2.3:javaagent' diff --git a/docs/instrumentation-list.yaml b/docs/instrumentation-list.yaml index b7235be6ab58..1f5e887daae7 100644 --- a/docs/instrumentation-list.yaml +++ b/docs/instrumentation-list.yaml @@ -10796,6 +10796,85 @@ libraries: target_versions: javaagent: - jakarta.servlet:jakarta.servlet-api:[5.0.0,) + sofastack: + - name: sofa-rpc-5.4 + display_name: SOFARPC + description: | + The SOFARPC instrumentation provides RPC client spans and metrics, and RPC server spans and metrics for Apache SOFARPC RPC calls. + semantic_conventions: + - RPC_CLIENT_SPANS + - RPC_CLIENT_METRICS + - RPC_SERVER_SPANS + - RPC_SERVER_METRICS + library_link: https://github.com/sofastack/sofa-rpc/ + source_path: instrumentation/sofa-rpc-5.4 + scope: + name: io.opentelemetry.sofa-rpc-5.4 + target_versions: + javaagent: + - com.alipay.sofa:sofa-rpc-all:[5.4.0,) + configurations: + - name: otel.instrumentation.common.peer-service-mapping + description: Used to specify a mapping from host names or IP addresses to peer + services. + type: map + default: '' + telemetry: + - when: default + metrics: + - name: rpc.client.duration + description: The duration of an outbound RPC invocation. + type: HISTOGRAM + unit: ms + attributes: + - name: rpc.method + type: STRING + - name: rpc.service + type: STRING + - name: rpc.system + type: STRING + - name: server.address + type: STRING + - name: server.port + type: LONG + - name: rpc.server.duration + description: The duration of an inbound RPC invocation. + type: HISTOGRAM + unit: ms + attributes: + - name: rpc.method + type: STRING + - name: rpc.service + type: STRING + - name: rpc.system + type: STRING + spans: + - span_kind: CLIENT + attributes: + - name: peer.service + type: STRING + - name: rpc.method + type: STRING + - name: rpc.service + type: STRING + - name: rpc.system + type: STRING + - name: server.address + type: STRING + - name: server.port + type: LONG + - span_kind: SERVER + attributes: + - name: network.peer.address + type: STRING + - name: network.peer.port + type: LONG + - name: rpc.method + type: STRING + - name: rpc.service + type: STRING + - name: rpc.system + type: STRING spark: - name: spark-2.3 description: | diff --git a/docs/supported-libraries.md b/docs/supported-libraries.md index 35406a5a24d7..d769f1a1e809 100644 --- a/docs/supported-libraries.md +++ b/docs/supported-libraries.md @@ -138,6 +138,7 @@ These are the supported libraries and frameworks: | [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+ | [opentelemetry-servlet-3.0](../instrumentation/servlet/servlet-3.0/library) | [HTTP Server Spans], [HTTP Server Metrics] | +| [SOFARPC](https://github.com/sofastack/sofa-rpc/) | 5.4.0+ | [opentelemetry-sofa-rpc-5.4](../instrumentation/sofa-rpc-5.4/library-autoconfigure) | [RPC Client Spans], [RPC Server Spans] | | [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 | diff --git a/instrumentation-docs/instrumentations.sh b/instrumentation-docs/instrumentations.sh index d7883245e600..ff4ab7364bf0 100755 --- a/instrumentation-docs/instrumentations.sh +++ b/instrumentation-docs/instrumentations.sh @@ -204,6 +204,7 @@ readonly INSTRUMENTATIONS=( "pulsar:pulsar-2.8:javaagent:testExperimental" "reactor:reactor-netty:reactor-netty-0.9:javaagent:test" "reactor:reactor-netty:reactor-netty-1.0:javaagent:test" + "sofa-rpc-5.4:javaagent:testSofaRpc" "spring:spring-batch-3.0:javaagent:test" "spring:spring-data:spring-data-1.8:javaagent:test" "spring:spring-integration-4.1:javaagent:test" diff --git a/instrumentation/sofa-rpc-5.4/javaagent/build.gradle.kts b/instrumentation/sofa-rpc-5.4/javaagent/build.gradle.kts new file mode 100644 index 000000000000..32afff53bb3d --- /dev/null +++ b/instrumentation/sofa-rpc-5.4/javaagent/build.gradle.kts @@ -0,0 +1,74 @@ +plugins { + id("otel.javaagent-instrumentation") +} + +muzzle { + pass { + group.set("com.alipay.sofa") + module.set("sofa-rpc-all") + versions.set("[5.4.0,)") + assertInverse.set(true) + } +} + +dependencies { + implementation(project(":instrumentation:sofa-rpc-5.4:library-autoconfigure")) + + library("com.alipay.sofa:sofa-rpc-all:5.4.0") +} + +val latestDepTest = findProperty("testLatestDeps") as Boolean + +testing { + suites { + // using a test suite to ensure that project(":instrumentation:sofa-rpc-5.4: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 testSofaRpc by registering(JvmTestSuite::class) { + dependencies { + implementation(project(":instrumentation:sofa-rpc-5.4:testing")) + if (latestDepTest) { + implementation("com.alipay.sofa:sofa-rpc-all:latest.release") + } else { + implementation("com.alipay.sofa:sofa-rpc-all:5.4.0") + } + runtimeOnly("ch.qos.logback:logback-classic:1.2.13") + runtimeOnly("ch.qos.logback:logback-core:1.2.13") + runtimeOnly("org.slf4j:slf4j-api:1.7.21") + } + } + } +} + +tasks.withType().configureEach { + systemProperty("testLatestDeps", findProperty("testLatestDeps") as Boolean) + jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") + // to suppress non-fatal errors on jdk17 + jvmArgs("--add-opens=java.base/java.math=ALL-UNNAMED") + // required on jdk17 + jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") + + systemProperty("collectMetadata", findProperty("collectMetadata")?.toString() ?: "false") +} + +tasks { + check { + dependsOn(testing.suites) + } + + if (findProperty("denyUnsafe") as Boolean) { + // SOFA RPC's tracer module uses Disruptor which requires sun.misc.Unsafe. + withType().configureEach { + enabled = false + } + } +} + +configurations.named("testSofaRpcRuntimeClasspath") { + resolutionStrategy { + // requires old logback (and therefore also old slf4j) + force("ch.qos.logback:logback-classic:1.2.13") + force("ch.qos.logback:logback-core:1.2.13") + force("org.slf4j:slf4j-api:1.7.21") + } +} diff --git a/instrumentation/sofa-rpc-5.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/sofarpc/v5_4/OpenTelemetryClientFilter.java b/instrumentation/sofa-rpc-5.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/sofarpc/v5_4/OpenTelemetryClientFilter.java new file mode 100644 index 000000000000..d7cbe3be603c --- /dev/null +++ b/instrumentation/sofa-rpc-5.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/sofarpc/v5_4/OpenTelemetryClientFilter.java @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.sofarpc.v5_4; + +import com.alipay.sofa.rpc.config.ConsumerConfig; +import com.alipay.sofa.rpc.core.exception.SofaRpcException; +import com.alipay.sofa.rpc.core.request.SofaRequest; +import com.alipay.sofa.rpc.core.response.SofaResponse; +import com.alipay.sofa.rpc.ext.Extension; +import com.alipay.sofa.rpc.filter.AutoActive; +import com.alipay.sofa.rpc.filter.Filter; +import com.alipay.sofa.rpc.filter.FilterInvoker; + +@Extension(value = "openTelemetryClient", order = -25000) +@AutoActive(consumerSide = true) +public final class OpenTelemetryClientFilter extends Filter { + + private final Filter delegate; + + public OpenTelemetryClientFilter() { + delegate = SofaRpcSingletons.CLIENT_FILTER; + } + + @Override + @SuppressWarnings("ThrowsUncheckedException") + public SofaResponse invoke(FilterInvoker invoker, SofaRequest request) throws SofaRpcException { + return delegate.invoke(invoker, request); + } + + @Override + // Suppress rawtypes warning: SOFARPC Filter interface uses raw ConsumerConfig type + @SuppressWarnings({"rawtypes", "ThrowsUncheckedException"}) + public void onAsyncResponse( + ConsumerConfig config, SofaRequest request, SofaResponse response, Throwable exception) + throws SofaRpcException { + delegate.onAsyncResponse(config, request, response, exception); + } +} diff --git a/instrumentation/sofa-rpc-5.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/sofarpc/v5_4/OpenTelemetryServerFilter.java b/instrumentation/sofa-rpc-5.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/sofarpc/v5_4/OpenTelemetryServerFilter.java new file mode 100644 index 000000000000..84afc67b12d3 --- /dev/null +++ b/instrumentation/sofa-rpc-5.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/sofarpc/v5_4/OpenTelemetryServerFilter.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.sofarpc.v5_4; + +import com.alipay.sofa.rpc.core.exception.SofaRpcException; +import com.alipay.sofa.rpc.core.request.SofaRequest; +import com.alipay.sofa.rpc.core.response.SofaResponse; +import com.alipay.sofa.rpc.ext.Extension; +import com.alipay.sofa.rpc.filter.AutoActive; +import com.alipay.sofa.rpc.filter.Filter; +import com.alipay.sofa.rpc.filter.FilterInvoker; + +@Extension(value = "openTelemetryServer", order = -25000) +@AutoActive(providerSide = true) +public final class OpenTelemetryServerFilter extends Filter { + + private final Filter delegate; + + public OpenTelemetryServerFilter() { + delegate = SofaRpcSingletons.SERVER_FILTER; + } + + @Override + @SuppressWarnings("ThrowsUncheckedException") + public SofaResponse invoke(FilterInvoker invoker, SofaRequest request) throws SofaRpcException { + return delegate.invoke(invoker, request); + } +} diff --git a/instrumentation/sofa-rpc-5.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/sofarpc/v5_4/SofaRpcInstrumentationModule.java b/instrumentation/sofa-rpc-5.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/sofarpc/v5_4/SofaRpcInstrumentationModule.java new file mode 100644 index 000000000000..7c7e15bfc911 --- /dev/null +++ b/instrumentation/sofa-rpc-5.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/sofarpc/v5_4/SofaRpcInstrumentationModule.java @@ -0,0 +1,74 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.sofarpc.v5_4; + +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.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 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 SofaRpcInstrumentationModule extends InstrumentationModule + implements ExperimentalInstrumentationModule { + public SofaRpcInstrumentationModule() { + super("sofa-rpc", "sofa-rpc-5.4"); + } + + @Override + public void registerHelperResources(HelperResourceBuilder helperResourceBuilder) { + helperResourceBuilder.register( + "META-INF/services/com.alipay.sofa.rpc.filter.Filter", + "sofa-rpc-5.4/META-INF/com.alipay.sofa.rpc.filter.Filter"); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + return hasClassesNamed("com.alipay.sofa.rpc.filter.Filter") + // Class was added in 5.4.0 + .and(hasClassesNamed("com.alipay.sofa.rpc.transport.ClientHandler")); + } + + @Override + public void injectClasses(ClassInjector injector) { + injector + .proxyBuilder( + "io.opentelemetry.javaagent.instrumentation.sofarpc.v5_4.OpenTelemetryClientFilter") + .inject(InjectionMode.CLASS_ONLY); + injector + .proxyBuilder( + "io.opentelemetry.javaagent.instrumentation.sofarpc.v5_4.OpenTelemetryServerFilter") + .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() { + return named("com.alipay.sofa.rpc.ext.ExtensionLoader"); + } + + @Override + public void transform(TypeTransformer transformer) { + // Nothing to transform, this type instrumentation is only used for injecting resources. + } + } +} diff --git a/instrumentation/sofa-rpc-5.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/sofarpc/v5_4/SofaRpcSingletons.java b/instrumentation/sofa-rpc-5.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/sofarpc/v5_4/SofaRpcSingletons.java new file mode 100644 index 000000000000..1479bb40001d --- /dev/null +++ b/instrumentation/sofa-rpc-5.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/sofarpc/v5_4/SofaRpcSingletons.java @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.sofarpc.v5_4; + +import com.alipay.sofa.rpc.filter.Filter; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.net.PeerServiceAttributesExtractor; +import io.opentelemetry.instrumentation.sofarpc.v5_4.SofaRpcTelemetry; +import io.opentelemetry.instrumentation.sofarpc.v5_4.internal.SofaRpcClientNetworkAttributesGetter; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; + +public final class SofaRpcSingletons { + public static final Filter CLIENT_FILTER; + public static final Filter SERVER_FILTER; + + static { + SofaRpcTelemetry telemetry = + SofaRpcTelemetry.builder(GlobalOpenTelemetry.get()) + .addAttributesExtractor( + PeerServiceAttributesExtractor.create( + new SofaRpcClientNetworkAttributesGetter(), + AgentCommonConfig.get().getPeerServiceResolver())) + .build(); + CLIENT_FILTER = telemetry.newClientFilter(); + SERVER_FILTER = telemetry.newServerFilter(); + } + + private SofaRpcSingletons() {} +} diff --git a/instrumentation/sofa-rpc-5.4/javaagent/src/main/resources/sofa-rpc-5.4/META-INF/com.alipay.sofa.rpc.filter.Filter b/instrumentation/sofa-rpc-5.4/javaagent/src/main/resources/sofa-rpc-5.4/META-INF/com.alipay.sofa.rpc.filter.Filter new file mode 100644 index 000000000000..d42eff1f2be5 --- /dev/null +++ b/instrumentation/sofa-rpc-5.4/javaagent/src/main/resources/sofa-rpc-5.4/META-INF/com.alipay.sofa.rpc.filter.Filter @@ -0,0 +1,3 @@ +io.opentelemetry.javaagent.instrumentation.sofarpc.v5_4.OpenTelemetryClientFilter +io.opentelemetry.javaagent.instrumentation.sofarpc.v5_4.OpenTelemetryServerFilter + diff --git a/instrumentation/sofa-rpc-5.4/javaagent/src/testSofaRpc/java/io/opentelemetry/javaagent/instrumentation/sofarpc/v5_4/SofaRpcAgentTest.java b/instrumentation/sofa-rpc-5.4/javaagent/src/testSofaRpc/java/io/opentelemetry/javaagent/instrumentation/sofarpc/v5_4/SofaRpcAgentTest.java new file mode 100644 index 000000000000..d687c2c9c3cd --- /dev/null +++ b/instrumentation/sofa-rpc-5.4/javaagent/src/testSofaRpc/java/io/opentelemetry/javaagent/instrumentation/sofarpc/v5_4/SofaRpcAgentTest.java @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.sofarpc.v5_4; + +import io.opentelemetry.instrumentation.sofarpc.v5_4.AbstractSofaRpcTest; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +class SofaRpcAgentTest extends AbstractSofaRpcTest { + + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Override + protected InstrumentationExtension testing() { + return testing; + } + + @Override + protected boolean hasPeerService() { + return true; + } +} diff --git a/instrumentation/sofa-rpc-5.4/javaagent/src/testSofaRpc/java/io/opentelemetry/javaagent/instrumentation/sofarpc/v5_4/SofaRpcAgentTraceChainTest.java b/instrumentation/sofa-rpc-5.4/javaagent/src/testSofaRpc/java/io/opentelemetry/javaagent/instrumentation/sofarpc/v5_4/SofaRpcAgentTraceChainTest.java new file mode 100644 index 000000000000..6bc866fe73db --- /dev/null +++ b/instrumentation/sofa-rpc-5.4/javaagent/src/testSofaRpc/java/io/opentelemetry/javaagent/instrumentation/sofarpc/v5_4/SofaRpcAgentTraceChainTest.java @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.sofarpc.v5_4; + +import io.opentelemetry.instrumentation.sofarpc.v5_4.AbstractSofaRpcTraceChainTest; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +class SofaRpcAgentTraceChainTest extends AbstractSofaRpcTraceChainTest { + + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Override + protected InstrumentationExtension testing() { + return testing; + } + + @Override + protected boolean hasPeerService() { + return true; + } +} diff --git a/instrumentation/sofa-rpc-5.4/library-autoconfigure/build.gradle.kts b/instrumentation/sofa-rpc-5.4/library-autoconfigure/build.gradle.kts new file mode 100644 index 000000000000..f7c8eb3495ad --- /dev/null +++ b/instrumentation/sofa-rpc-5.4/library-autoconfigure/build.gradle.kts @@ -0,0 +1,30 @@ +plugins { + id("otel.library-instrumentation") +} + +dependencies { + compileOnly("com.google.auto.value:auto-value-annotations") + annotationProcessor("com.google.auto.value:auto-value") + + // 5.4.0 is the recommended minimum version for production use + library("com.alipay.sofa:sofa-rpc-all:5.4.0") + + testImplementation(project(":instrumentation:sofa-rpc-5.4:testing")) +} + +configurations.testRuntimeClasspath { + resolutionStrategy { + // requires old logback (and therefore also old slf4j) + force("ch.qos.logback:logback-classic:1.2.13") + force("ch.qos.logback:logback-core:1.2.13") + force("org.slf4j:slf4j-api:1.7.21") + } +} + +tasks.withType().configureEach { + jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") + // to suppress non-fatal errors on jdk17 + jvmArgs("--add-opens=java.base/java.math=ALL-UNNAMED") + // required on jdk17 + jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") +} diff --git a/instrumentation/sofa-rpc-5.4/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/OpenTelemetryClientFilter.java b/instrumentation/sofa-rpc-5.4/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/OpenTelemetryClientFilter.java new file mode 100644 index 000000000000..d4cf58fa78f2 --- /dev/null +++ b/instrumentation/sofa-rpc-5.4/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/OpenTelemetryClientFilter.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.sofarpc.v5_4; + +import com.alipay.sofa.rpc.config.ConsumerConfig; +import com.alipay.sofa.rpc.core.exception.SofaRpcException; +import com.alipay.sofa.rpc.core.request.SofaRequest; +import com.alipay.sofa.rpc.core.response.SofaResponse; +import com.alipay.sofa.rpc.ext.Extension; +import com.alipay.sofa.rpc.filter.AutoActive; +import com.alipay.sofa.rpc.filter.Filter; +import com.alipay.sofa.rpc.filter.FilterInvoker; +import io.opentelemetry.api.GlobalOpenTelemetry; + +@Extension(value = "openTelemetryClient", order = -25000) +@AutoActive(consumerSide = true) +public final class OpenTelemetryClientFilter extends Filter { + + private final Filter delegate; + + public OpenTelemetryClientFilter() { + delegate = SofaRpcTelemetry.create(GlobalOpenTelemetry.get()).newClientFilter(); + } + + @Override + @SuppressWarnings("ThrowsUncheckedException") + public SofaResponse invoke(FilterInvoker invoker, SofaRequest request) throws SofaRpcException { + return delegate.invoke(invoker, request); + } + + @Override + // Suppress rawtypes warning: SOFARPC Filter interface uses raw ConsumerConfig type + @SuppressWarnings({"rawtypes", "ThrowsUncheckedException"}) + public void onAsyncResponse( + ConsumerConfig config, SofaRequest request, SofaResponse response, Throwable exception) + throws SofaRpcException { + delegate.onAsyncResponse(config, request, response, exception); + } +} diff --git a/instrumentation/sofa-rpc-5.4/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/OpenTelemetryServerFilter.java b/instrumentation/sofa-rpc-5.4/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/OpenTelemetryServerFilter.java new file mode 100644 index 000000000000..af60909ec588 --- /dev/null +++ b/instrumentation/sofa-rpc-5.4/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/OpenTelemetryServerFilter.java @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.sofarpc.v5_4; + +import com.alipay.sofa.rpc.core.exception.SofaRpcException; +import com.alipay.sofa.rpc.core.request.SofaRequest; +import com.alipay.sofa.rpc.core.response.SofaResponse; +import com.alipay.sofa.rpc.ext.Extension; +import com.alipay.sofa.rpc.filter.AutoActive; +import com.alipay.sofa.rpc.filter.Filter; +import com.alipay.sofa.rpc.filter.FilterInvoker; +import io.opentelemetry.api.GlobalOpenTelemetry; + +@Extension(value = "openTelemetryServer", order = -25000) +@AutoActive(providerSide = true) +public final class OpenTelemetryServerFilter extends Filter { + + private final Filter delegate; + + public OpenTelemetryServerFilter() { + delegate = SofaRpcTelemetry.create(GlobalOpenTelemetry.get()).newServerFilter(); + } + + @Override + @SuppressWarnings("ThrowsUncheckedException") + public SofaResponse invoke(FilterInvoker invoker, SofaRequest request) throws SofaRpcException { + return delegate.invoke(invoker, request); + } +} diff --git a/instrumentation/sofa-rpc-5.4/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/SofaRpcAttributesGetter.java b/instrumentation/sofa-rpc-5.4/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/SofaRpcAttributesGetter.java new file mode 100644 index 000000000000..c8f62f0fea6e --- /dev/null +++ b/instrumentation/sofa-rpc-5.4/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/SofaRpcAttributesGetter.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.sofarpc.v5_4; + +import io.opentelemetry.instrumentation.api.incubator.semconv.rpc.RpcAttributesGetter; +import javax.annotation.Nullable; + +enum SofaRpcAttributesGetter implements RpcAttributesGetter { + INSTANCE; + + @Override + public String getSystem(SofaRpcRequest request) { + return "sofa_rpc"; + } + + @Override + @Nullable + public String getService(SofaRpcRequest request) { + return request.request().getInterfaceName(); + } + + @Override + public String getMethod(SofaRpcRequest request) { + return request.request().getMethodName(); + } +} diff --git a/instrumentation/sofa-rpc-5.4/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/SofaRpcErrorAttributesExtractor.java b/instrumentation/sofa-rpc-5.4/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/SofaRpcErrorAttributesExtractor.java new file mode 100644 index 000000000000..47071cb42ede --- /dev/null +++ b/instrumentation/sofa-rpc-5.4/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/SofaRpcErrorAttributesExtractor.java @@ -0,0 +1,48 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.sofarpc.v5_4; + +import static io.opentelemetry.instrumentation.api.internal.AttributesExtractorUtil.internalSet; + +import com.alipay.sofa.rpc.core.response.SofaResponse; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.semconv.ErrorAttributes; +import javax.annotation.Nullable; + +/** + * Extracts error.type attribute for SOFARPC spans. + * + *

This extractor sets the error.type attribute based on the exception passed to {@link + * AttributesExtractor#onEnd(AttributesBuilder, Context, Object, Object, Throwable)}. If an + * exception is present, it uses the exception's class name as the error type, which follows the + * OpenTelemetry semantic conventions for error reporting. + * + *

The error type is extracted from the exception's class name (e.g., + * "java.lang.IllegalStateException") to provide low-cardinality error classification. + */ +final class SofaRpcErrorAttributesExtractor + implements AttributesExtractor { + + @Override + public void onStart(AttributesBuilder attributes, Context parentContext, SofaRpcRequest request) { + // No start attributes + } + + @Override + public void onEnd( + AttributesBuilder attributes, + Context context, + SofaRpcRequest request, + @Nullable SofaResponse response, + @Nullable Throwable error) { + if (error != null) { + // Use exception class name as error type + internalSet(attributes, ErrorAttributes.ERROR_TYPE, error.getClass().getName()); + } + } +} diff --git a/instrumentation/sofa-rpc-5.4/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/SofaRpcHeadersGetter.java b/instrumentation/sofa-rpc-5.4/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/SofaRpcHeadersGetter.java new file mode 100644 index 000000000000..fe11891b3dbc --- /dev/null +++ b/instrumentation/sofa-rpc-5.4/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/SofaRpcHeadersGetter.java @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.sofarpc.v5_4; + +import com.alipay.sofa.rpc.core.request.SofaRequest; +import io.opentelemetry.context.propagation.TextMapGetter; +import java.util.Collections; +import java.util.Map; +import javax.annotation.Nullable; + +enum SofaRpcHeadersGetter implements TextMapGetter { + INSTANCE; + + @Override + public Iterable keys(SofaRpcRequest request) { + SofaRequest sofaRequest = request.request(); + Map requestProps = sofaRequest.getRequestProps(); + if (requestProps != null && !requestProps.isEmpty()) { + return requestProps.keySet(); + } + return Collections.emptySet(); + } + + @Override + @Nullable + public String get(SofaRpcRequest request, String key) { + SofaRequest sofaRequest = request.request(); + Object value = sofaRequest.getRequestProp(key); + // Only return String values, ignore other types + return value instanceof String ? (String) value : null; + } +} diff --git a/instrumentation/sofa-rpc-5.4/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/SofaRpcHeadersSetter.java b/instrumentation/sofa-rpc-5.4/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/SofaRpcHeadersSetter.java new file mode 100644 index 000000000000..4ec39a0d1894 --- /dev/null +++ b/instrumentation/sofa-rpc-5.4/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/SofaRpcHeadersSetter.java @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.sofarpc.v5_4; + +import io.opentelemetry.context.propagation.TextMapSetter; + +enum SofaRpcHeadersSetter implements TextMapSetter { + INSTANCE; + + @Override + public void set(SofaRpcRequest request, String key, String value) { + request.request().addRequestProp(key, value); + } +} diff --git a/instrumentation/sofa-rpc-5.4/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/SofaRpcNetworkServerAttributesGetter.java b/instrumentation/sofa-rpc-5.4/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/SofaRpcNetworkServerAttributesGetter.java new file mode 100644 index 000000000000..3ffe1abb9b6c --- /dev/null +++ b/instrumentation/sofa-rpc-5.4/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/SofaRpcNetworkServerAttributesGetter.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.sofarpc.v5_4; + +import com.alipay.sofa.rpc.core.response.SofaResponse; +import io.opentelemetry.instrumentation.api.semconv.network.NetworkAttributesGetter; +import java.net.InetSocketAddress; +import javax.annotation.Nullable; + +final class SofaRpcNetworkServerAttributesGetter + implements NetworkAttributesGetter { + + @Nullable + @Override + public InetSocketAddress getNetworkLocalInetSocketAddress( + SofaRpcRequest request, @Nullable SofaResponse response) { + return request.localAddress(); + } + + @Override + @Nullable + public InetSocketAddress getNetworkPeerInetSocketAddress( + SofaRpcRequest request, @Nullable SofaResponse response) { + return request.remoteAddress(); + } +} diff --git a/instrumentation/sofa-rpc-5.4/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/SofaRpcRequest.java b/instrumentation/sofa-rpc-5.4/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/SofaRpcRequest.java new file mode 100644 index 000000000000..41cb223c5b14 --- /dev/null +++ b/instrumentation/sofa-rpc-5.4/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/SofaRpcRequest.java @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.sofarpc.v5_4; + +import com.alipay.sofa.rpc.client.ProviderInfo; +import com.alipay.sofa.rpc.context.RpcInternalContext; +import com.alipay.sofa.rpc.core.request.SofaRequest; +import com.google.auto.value.AutoValue; +import java.net.InetSocketAddress; +import javax.annotation.Nullable; + +@AutoValue +public abstract class SofaRpcRequest { + + static SofaRpcRequest create(SofaRequest request) { + RpcInternalContext context = RpcInternalContext.getContext(); + + // Get network addresses from RpcInternalContext + InetSocketAddress remoteAddress = context != null ? context.getRemoteAddress() : null; + InetSocketAddress localAddress = context != null ? context.getLocalAddress() : null; + ProviderInfo providerInfo = context != null ? context.getProviderInfo() : null; + + return new AutoValue_SofaRpcRequest(request, remoteAddress, localAddress, providerInfo); + } + + public abstract SofaRequest request(); + + @Nullable + public abstract InetSocketAddress remoteAddress(); + + @Nullable + public abstract InetSocketAddress localAddress(); + + @Nullable + public abstract ProviderInfo providerInfo(); +} diff --git a/instrumentation/sofa-rpc-5.4/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/SofaRpcTelemetry.java b/instrumentation/sofa-rpc-5.4/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/SofaRpcTelemetry.java new file mode 100644 index 000000000000..78c963f9e131 --- /dev/null +++ b/instrumentation/sofa-rpc-5.4/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/SofaRpcTelemetry.java @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.sofarpc.v5_4; + +import com.alipay.sofa.rpc.core.response.SofaResponse; +import com.alipay.sofa.rpc.filter.Filter; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; + +/** Entrypoint for instrumenting SOFARPC servers and clients. */ +public final class SofaRpcTelemetry { + + private final Instrumenter serverInstrumenter; + private final Instrumenter clientInstrumenter; + + /** Returns a new {@link SofaRpcTelemetry} configured with the given {@link OpenTelemetry}. */ + public static SofaRpcTelemetry create(OpenTelemetry openTelemetry) { + return builder(openTelemetry).build(); + } + + /** + * Returns a new {@link SofaRpcTelemetryBuilder} configured with the given {@link OpenTelemetry}. + */ + public static SofaRpcTelemetryBuilder builder(OpenTelemetry openTelemetry) { + return new SofaRpcTelemetryBuilder(openTelemetry); + } + + SofaRpcTelemetry( + Instrumenter serverInstrumenter, + Instrumenter clientInstrumenter) { + this.serverInstrumenter = serverInstrumenter; + this.clientInstrumenter = clientInstrumenter; + } + + /** Returns a new SOFARPC client {@link Filter} that traces SOFARPC RPC invocations. */ + public Filter newClientFilter() { + return new TracingFilter(clientInstrumenter, true); + } + + /** Returns a new SOFARPC server {@link Filter} that traces SOFARPC RPC invocations. */ + public Filter newServerFilter() { + return new TracingFilter(serverInstrumenter, false); + } +} diff --git a/instrumentation/sofa-rpc-5.4/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/SofaRpcTelemetryBuilder.java b/instrumentation/sofa-rpc-5.4/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/SofaRpcTelemetryBuilder.java new file mode 100644 index 000000000000..ec172f339902 --- /dev/null +++ b/instrumentation/sofa-rpc-5.4/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/SofaRpcTelemetryBuilder.java @@ -0,0 +1,129 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.sofarpc.v5_4; + +import com.alipay.sofa.rpc.core.response.SofaResponse; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +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.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.semconv.network.NetworkAttributesExtractor; +import io.opentelemetry.instrumentation.api.semconv.network.ServerAttributesExtractor; +import io.opentelemetry.instrumentation.sofarpc.v5_4.internal.SofaRpcClientNetworkAttributesGetter; +import java.util.ArrayList; +import java.util.List; +import java.util.function.UnaryOperator; +import javax.annotation.Nullable; + +/** A builder of {@link SofaRpcTelemetry}. */ +public final class SofaRpcTelemetryBuilder { + + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.sofa-rpc-5.4"; + + // 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 UnaryOperator> clientSpanNameExtractorTransformer = + UnaryOperator.identity(); + private UnaryOperator> serverSpanNameExtractorTransformer = + UnaryOperator.identity(); + + SofaRpcTelemetryBuilder(OpenTelemetry openTelemetry) { + this.openTelemetry = openTelemetry; + } + + /** Sets the {@code peer.service} attribute for RPC client spans. */ + @CanIgnoreReturnValue + public SofaRpcTelemetryBuilder setPeerService(String peerService) { + this.peerService = peerService; + return this; + } + + /** + * Adds an additional {@link AttributesExtractor} to invoke to set attributes to instrumented + * items. + */ + @CanIgnoreReturnValue + public SofaRpcTelemetryBuilder addAttributesExtractor( + AttributesExtractor attributesExtractor) { + attributesExtractors.add(attributesExtractor); + return this; + } + + /** Sets custom client {@link SpanNameExtractor} via transform function. */ + @CanIgnoreReturnValue + public SofaRpcTelemetryBuilder setClientSpanNameExtractorCustomizer( + UnaryOperator> clientSpanNameExtractor) { + this.clientSpanNameExtractorTransformer = clientSpanNameExtractor; + return this; + } + + /** Sets custom server {@link SpanNameExtractor} via transform function. */ + @CanIgnoreReturnValue + public SofaRpcTelemetryBuilder setServerSpanNameExtractorCustomizer( + UnaryOperator> serverSpanNameExtractor) { + this.serverSpanNameExtractorTransformer = serverSpanNameExtractor; + return this; + } + + /** + * Returns a new {@link SofaRpcTelemetry} with the settings of this {@link + * SofaRpcTelemetryBuilder}. + */ + public SofaRpcTelemetry build() { + SofaRpcAttributesGetter rpcAttributesGetter = SofaRpcAttributesGetter.INSTANCE; + SpanNameExtractor spanNameExtractor = + RpcSpanNameExtractor.create(rpcAttributesGetter); + SpanNameExtractor clientSpanNameExtractor = + clientSpanNameExtractorTransformer.apply(spanNameExtractor); + SpanNameExtractor serverSpanNameExtractor = + serverSpanNameExtractorTransformer.apply(spanNameExtractor); + SofaRpcClientNetworkAttributesGetter netClientAttributesGetter = + new SofaRpcClientNetworkAttributesGetter(); + SofaRpcNetworkServerAttributesGetter netServerAttributesGetter = + new SofaRpcNetworkServerAttributesGetter(); + + InstrumenterBuilder serverInstrumenterBuilder = + Instrumenter.builder( + openTelemetry, INSTRUMENTATION_NAME, serverSpanNameExtractor) + .addAttributesExtractor(RpcServerAttributesExtractor.create(rpcAttributesGetter)) + .addAttributesExtractor(NetworkAttributesExtractor.create(netServerAttributesGetter)) + .addAttributesExtractor(new SofaRpcErrorAttributesExtractor()) + .addAttributesExtractors(attributesExtractors) + .addOperationMetrics(RpcServerMetrics.get()); + + InstrumenterBuilder clientInstrumenterBuilder = + Instrumenter.builder( + openTelemetry, INSTRUMENTATION_NAME, clientSpanNameExtractor) + .addAttributesExtractor(RpcClientAttributesExtractor.create(rpcAttributesGetter)) + .addAttributesExtractor(ServerAttributesExtractor.create(netClientAttributesGetter)) + .addAttributesExtractor(NetworkAttributesExtractor.create(netClientAttributesGetter)) + .addAttributesExtractor(new SofaRpcErrorAttributesExtractor()) + .addAttributesExtractors(attributesExtractors) + .addOperationMetrics(RpcClientMetrics.get()); + + if (peerService != null) { + clientInstrumenterBuilder.addAttributesExtractor( + AttributesExtractor.constant(PEER_SERVICE, peerService)); + } + + return new SofaRpcTelemetry( + serverInstrumenterBuilder.buildServerInstrumenter(SofaRpcHeadersGetter.INSTANCE), + clientInstrumenterBuilder.buildClientInstrumenter(SofaRpcHeadersSetter.INSTANCE)); + } +} diff --git a/instrumentation/sofa-rpc-5.4/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/TracingFilter.java b/instrumentation/sofa-rpc-5.4/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/TracingFilter.java new file mode 100644 index 000000000000..88542ba41bee --- /dev/null +++ b/instrumentation/sofa-rpc-5.4/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/TracingFilter.java @@ -0,0 +1,127 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.sofarpc.v5_4; + +import com.alipay.sofa.rpc.common.RemotingConstants; +import com.alipay.sofa.rpc.config.AbstractInterfaceConfig; +import com.alipay.sofa.rpc.config.ConsumerConfig; +import com.alipay.sofa.rpc.core.exception.SofaRpcException; +import com.alipay.sofa.rpc.core.request.SofaRequest; +import com.alipay.sofa.rpc.core.response.SofaResponse; +import com.alipay.sofa.rpc.filter.Filter; +import com.alipay.sofa.rpc.filter.FilterInvoker; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.util.VirtualField; + +final class TracingFilter extends Filter { + + private final Instrumenter instrumenter; + private final boolean isClientSide; + + private static final VirtualField ASYNC_CONTEXT = + VirtualField.find(SofaRequest.class, Context.class); + + TracingFilter(Instrumenter instrumenter, boolean isClientSide) { + this.instrumenter = instrumenter; + this.isClientSide = isClientSide; + } + + @Override + @SuppressWarnings("ThrowsUncheckedException") + public SofaResponse invoke(FilterInvoker invoker, SofaRequest request) throws SofaRpcException { + if (shouldSkipLocalCall(invoker)) { + return invoker.invoke(request); + } + + Context parentContext = Context.current(); + SofaRpcRequest sofaRpcRequest = SofaRpcRequest.create(request); + + if (!instrumenter.shouldStart(parentContext, sofaRpcRequest)) { + return invoker.invoke(request); + } + Context context = instrumenter.start(parentContext, sofaRpcRequest); + + SofaResponse response; + boolean isSynchronous = true; + try (Scope ignored = context.makeCurrent()) { + response = invoker.invoke(request); + if (isClientSide && request.isAsync()) { + isSynchronous = false; + ASYNC_CONTEXT.set(request, context); + } + } catch (Throwable e) { + instrumenter.end(context, sofaRpcRequest, null, e); + throw e; + } + + if (isSynchronous) { + Throwable exception = extractException(response); + instrumenter.end(context, sofaRpcRequest, response, exception); + } + + return response; + } + + private static boolean shouldSkipLocalCall(FilterInvoker invoker) { + AbstractInterfaceConfig config = invoker.getConfig(); + + if (config instanceof ConsumerConfig) { + ConsumerConfig consumerConfig = (ConsumerConfig) config; + + if (consumerConfig.isInJVM()) { + return true; + } + + String directUrl = consumerConfig.getDirectUrl(); + if (directUrl != null + && (directUrl.startsWith("local://") || directUrl.startsWith("injvm://"))) { + return true; + } + } + return false; + } + + private static Throwable extractException(SofaResponse response) { + if (response == null) { + return null; + } + + Object appResponse = response.getAppResponse(); + if (appResponse instanceof Throwable) { + return (Throwable) appResponse; + } + + if (response.isError() + || "true".equals(response.getResponseProp(RemotingConstants.HEAD_RESPONSE_ERROR))) { + String errorMsg = response.getErrorMsg(); + if (errorMsg != null) { + return new SofaRpcException( + com.alipay.sofa.rpc.core.exception.RpcErrorType.SERVER_UNDECLARED_ERROR, errorMsg); + } + } + + return null; + } + + @Override + // Suppress rawtypes warning: SOFARPC Filter interface uses raw ConsumerConfig type + @SuppressWarnings({"rawtypes", "ThrowsUncheckedException"}) + public void onAsyncResponse( + ConsumerConfig config, SofaRequest request, SofaResponse response, Throwable exception) + throws SofaRpcException { + if (!isClientSide) { + return; + } + Context context = ASYNC_CONTEXT.get(request); + if (context == null) { + return; + } + Throwable error = exception != null ? exception : extractException(response); + instrumenter.end(context, SofaRpcRequest.create(request), response, error); + } +} diff --git a/instrumentation/sofa-rpc-5.4/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/internal/SofaRpcClientNetworkAttributesGetter.java b/instrumentation/sofa-rpc-5.4/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/internal/SofaRpcClientNetworkAttributesGetter.java new file mode 100644 index 000000000000..e0e9b0c3558c --- /dev/null +++ b/instrumentation/sofa-rpc-5.4/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/internal/SofaRpcClientNetworkAttributesGetter.java @@ -0,0 +1,50 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.sofarpc.v5_4.internal; + +import com.alipay.sofa.rpc.client.ProviderInfo; +import com.alipay.sofa.rpc.core.response.SofaResponse; +import io.opentelemetry.instrumentation.api.semconv.network.NetworkAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.network.ServerAttributesGetter; +import io.opentelemetry.instrumentation.sofarpc.v5_4.SofaRpcRequest; +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 SofaRpcClientNetworkAttributesGetter + implements ServerAttributesGetter, + NetworkAttributesGetter { + + @Nullable + @Override + public String getServerAddress(SofaRpcRequest request) { + ProviderInfo providerInfo = request.providerInfo(); + if (providerInfo != null) { + return providerInfo.getHost(); + } + return null; + } + + @Override + @Nullable + public Integer getServerPort(SofaRpcRequest request) { + ProviderInfo providerInfo = request.providerInfo(); + if (providerInfo != null) { + return providerInfo.getPort(); + } + return null; + } + + @Override + @Nullable + public InetSocketAddress getNetworkPeerInetSocketAddress( + SofaRpcRequest request, @Nullable SofaResponse response) { + return request.remoteAddress(); + } +} diff --git a/instrumentation/sofa-rpc-5.4/library-autoconfigure/src/main/resources/META-INF/services/com.alipay.sofa.rpc.filter.Filter b/instrumentation/sofa-rpc-5.4/library-autoconfigure/src/main/resources/META-INF/services/com.alipay.sofa.rpc.filter.Filter new file mode 100644 index 000000000000..5498b775b4a3 --- /dev/null +++ b/instrumentation/sofa-rpc-5.4/library-autoconfigure/src/main/resources/META-INF/services/com.alipay.sofa.rpc.filter.Filter @@ -0,0 +1,3 @@ +io.opentelemetry.instrumentation.sofarpc.v5_4.OpenTelemetryClientFilter +io.opentelemetry.instrumentation.sofarpc.v5_4.OpenTelemetryServerFilter + diff --git a/instrumentation/sofa-rpc-5.4/library-autoconfigure/src/test/java/io/opentelemetry/instrumentation/sofarpc/v5_4/SofaRpcHeadersGetterTest.java b/instrumentation/sofa-rpc-5.4/library-autoconfigure/src/test/java/io/opentelemetry/instrumentation/sofarpc/v5_4/SofaRpcHeadersGetterTest.java new file mode 100644 index 000000000000..c012c5044d3e --- /dev/null +++ b/instrumentation/sofa-rpc-5.4/library-autoconfigure/src/test/java/io/opentelemetry/instrumentation/sofarpc/v5_4/SofaRpcHeadersGetterTest.java @@ -0,0 +1,85 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.sofarpc.v5_4; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import com.alipay.sofa.rpc.core.request.SofaRequest; +import java.util.Collections; +import java.util.Iterator; +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 SofaRpcHeadersGetterTest { + + @Mock SofaRequest sofaRequest; + + @Test + void testKeys() { + Map requestProps = Collections.singletonMap("key", "value"); + when(sofaRequest.getRequestProps()).thenReturn(requestProps); + + SofaRpcRequest request = SofaRpcRequest.create(sofaRequest); + + Iterator iterator = SofaRpcHeadersGetter.INSTANCE.keys(request).iterator(); + assertThat(iterator.hasNext()).isTrue(); + assertThat(iterator.next()).isEqualTo("key"); + assertThat(iterator.hasNext()).isFalse(); + } + + @Test + void testKeysEmpty() { + when(sofaRequest.getRequestProps()).thenReturn(Collections.emptyMap()); + + SofaRpcRequest request = SofaRpcRequest.create(sofaRequest); + + Iterator iterator = SofaRpcHeadersGetter.INSTANCE.keys(request).iterator(); + assertThat(iterator.hasNext()).isFalse(); + } + + @Test + void testKeysNull() { + when(sofaRequest.getRequestProps()).thenReturn(null); + + SofaRpcRequest request = SofaRpcRequest.create(sofaRequest); + + Iterator iterator = SofaRpcHeadersGetter.INSTANCE.keys(request).iterator(); + assertThat(iterator.hasNext()).isFalse(); + } + + @Test + void testGet() { + when(sofaRequest.getRequestProp("key")).thenReturn("value"); + + SofaRpcRequest request = SofaRpcRequest.create(sofaRequest); + + assertThat(SofaRpcHeadersGetter.INSTANCE.get(request, "key")).isEqualTo("value"); + } + + @Test + void testGetNonString() { + when(sofaRequest.getRequestProp("key")).thenReturn(123); + + SofaRpcRequest request = SofaRpcRequest.create(sofaRequest); + + // Should return null for non-String values + assertThat(SofaRpcHeadersGetter.INSTANCE.get(request, "key")).isNull(); + } + + @Test + void testGetNull() { + when(sofaRequest.getRequestProp("key")).thenReturn(null); + + SofaRpcRequest request = SofaRpcRequest.create(sofaRequest); + + assertThat(SofaRpcHeadersGetter.INSTANCE.get(request, "key")).isNull(); + } +} diff --git a/instrumentation/sofa-rpc-5.4/library-autoconfigure/src/test/java/io/opentelemetry/instrumentation/sofarpc/v5_4/SofaRpcTest.java b/instrumentation/sofa-rpc-5.4/library-autoconfigure/src/test/java/io/opentelemetry/instrumentation/sofarpc/v5_4/SofaRpcTest.java new file mode 100644 index 000000000000..df1843b5a03d --- /dev/null +++ b/instrumentation/sofa-rpc-5.4/library-autoconfigure/src/test/java/io/opentelemetry/instrumentation/sofarpc/v5_4/SofaRpcTest.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.sofarpc.v5_4; + +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +class SofaRpcTest extends AbstractSofaRpcTest { + + @RegisterExtension + static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + @Override + protected InstrumentationExtension testing() { + return testing; + } + + @Override + protected boolean hasPeerService() { + return false; + } +} diff --git a/instrumentation/sofa-rpc-5.4/library-autoconfigure/src/test/java/io/opentelemetry/instrumentation/sofarpc/v5_4/SofaRpcTraceChainTest.java b/instrumentation/sofa-rpc-5.4/library-autoconfigure/src/test/java/io/opentelemetry/instrumentation/sofarpc/v5_4/SofaRpcTraceChainTest.java new file mode 100644 index 000000000000..b96a14f2910a --- /dev/null +++ b/instrumentation/sofa-rpc-5.4/library-autoconfigure/src/test/java/io/opentelemetry/instrumentation/sofarpc/v5_4/SofaRpcTraceChainTest.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.sofarpc.v5_4; + +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +class SofaRpcTraceChainTest extends AbstractSofaRpcTraceChainTest { + + @RegisterExtension + static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + @Override + protected InstrumentationExtension testing() { + return testing; + } + + @Override + protected boolean hasPeerService() { + return false; + } +} diff --git a/instrumentation/sofa-rpc-5.4/metadata.yaml b/instrumentation/sofa-rpc-5.4/metadata.yaml new file mode 100644 index 000000000000..eb2f644070a8 --- /dev/null +++ b/instrumentation/sofa-rpc-5.4/metadata.yaml @@ -0,0 +1,15 @@ +display_name: SOFARPC +description: > + The SOFARPC instrumentation provides RPC client spans and metrics, and RPC server spans and + metrics for SOFARPC RPC calls. +semantic_conventions: + - RPC_CLIENT_SPANS + - RPC_CLIENT_METRICS + - RPC_SERVER_SPANS + - RPC_SERVER_METRICS +library_link: https://github.com/sofastack/sofa-rpc/ +configurations: + - name: otel.instrumentation.common.peer-service-mapping + description: Used to specify a mapping from host names or IP addresses to peer services. + type: map + default: '' diff --git a/instrumentation/sofa-rpc-5.4/testing/build.gradle.kts b/instrumentation/sofa-rpc-5.4/testing/build.gradle.kts new file mode 100644 index 000000000000..9d0dfa73f39f --- /dev/null +++ b/instrumentation/sofa-rpc-5.4/testing/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + id("otel.java-conventions") +} + +dependencies { + api("io.opentelemetry.javaagent:opentelemetry-testing-common") + + api("com.alipay.sofa:sofa-rpc-all:5.4.0") + + implementation("javax.annotation:javax.annotation-api:1.3.2") + implementation("com.google.guava:guava") + + implementation("io.opentelemetry:opentelemetry-api") +} + +configurations.testRuntimeClasspath { + resolutionStrategy { + // requires old logback (and therefore also old slf4j) + force("ch.qos.logback:logback-classic:1.2.13") + force("ch.qos.logback:logback-core:1.2.13") + force("org.slf4j:slf4j-api:1.7.21") + } +} diff --git a/instrumentation/sofa-rpc-5.4/testing/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/AbstractSofaRpcTest.java b/instrumentation/sofa-rpc-5.4/testing/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/AbstractSofaRpcTest.java new file mode 100644 index 000000000000..e0e5472a8fdd --- /dev/null +++ b/instrumentation/sofa-rpc-5.4/testing/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/AbstractSofaRpcTest.java @@ -0,0 +1,628 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.sofarpc.v5_4; + +import static io.opentelemetry.instrumentation.testing.GlobalTraceUtil.runWithSpan; +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.ErrorAttributes.ERROR_TYPE; +import static io.opentelemetry.semconv.NetworkAttributes.NETWORK_PEER_ADDRESS; +import static io.opentelemetry.semconv.NetworkAttributes.NETWORK_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.PeerIncubatingAttributes.PEER_SERVICE; +import static io.opentelemetry.semconv.incubating.RpcIncubatingAttributes.RPC_METHOD; +import static io.opentelemetry.semconv.incubating.RpcIncubatingAttributes.RPC_SERVICE; +import static io.opentelemetry.semconv.incubating.RpcIncubatingAttributes.RPC_SYSTEM; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.alipay.sofa.rpc.api.GenericService; +import com.alipay.sofa.rpc.api.future.SofaResponseFuture; +import com.alipay.sofa.rpc.common.RpcConstants; +import com.alipay.sofa.rpc.config.ApplicationConfig; +import com.alipay.sofa.rpc.config.ConsumerConfig; +import com.alipay.sofa.rpc.config.ProviderConfig; +import com.alipay.sofa.rpc.config.ServerConfig; +import com.alipay.sofa.rpc.core.exception.SofaRpcRuntimeException; +import com.alipay.sofa.rpc.core.exception.SofaTimeOutException; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.sofarpc.v5_4.api.ErrorService; +import io.opentelemetry.instrumentation.sofarpc.v5_4.api.HelloService; +import io.opentelemetry.instrumentation.sofarpc.v5_4.impl.ErrorServiceImpl; +import io.opentelemetry.instrumentation.sofarpc.v5_4.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.sdk.trace.data.StatusData; +import org.assertj.core.api.AbstractAssert; +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; + +public abstract class AbstractSofaRpcTest { + + protected abstract InstrumentationExtension testing(); + + protected abstract boolean hasPeerService(); + + @RegisterExtension static final AutoCleanupExtension cleanup = AutoCleanupExtension.create(); + + @BeforeAll + static void setUp() {} + + @AfterAll + static void tearDown() {} + + ConsumerConfig configureClient(int port) { + ConsumerConfig consumer = new ConsumerConfig<>(); + consumer + .setInterfaceId(HelloService.class.getName()) + .setApplication(new ApplicationConfig().setAppName("sofa-rpc-test-consumer")) + .setDirectUrl("bolt://127.0.0.1:" + port) + .setRegister(false) + .setTimeout(30000); + return consumer; + } + + ConsumerConfig configureGenericClient(int port) { + ConsumerConfig consumer = new ConsumerConfig<>(); + consumer + .setInterfaceId(HelloService.class.getName()) + .setApplication(new ApplicationConfig().setAppName("sofa-rpc-test-consumer")) + .setDirectUrl("bolt://127.0.0.1:" + port) + .setRegister(false) + .setTimeout(30000) + .setGeneric(true); + return consumer; + } + + ProviderConfig configureServer(int port) { + ServerConfig serverConfig = + new ServerConfig().setProtocol("bolt").setHost("127.0.0.1").setPort(port).setDaemon(false); + + ProviderConfig provider = new ProviderConfig<>(); + provider + .setInterfaceId(HelloService.class.getName()) + .setRef(new HelloServiceImpl()) + .setApplication(new ApplicationConfig().setAppName("sofa-rpc-test-provider")) + .setServer(serverConfig) + .setRegister(false); + return provider; + } + + ProviderConfig configureErrorServer(int port) { + ServerConfig serverConfig = + new ServerConfig().setProtocol("bolt").setHost("127.0.0.1").setPort(port).setDaemon(false); + + ProviderConfig provider = new ProviderConfig<>(); + provider + .setInterfaceId(ErrorService.class.getName()) + .setRef(new ErrorServiceImpl()) + .setApplication(new ApplicationConfig().setAppName("sofa-rpc-test-error-provider")) + .setServer(serverConfig) + .setRegister(false); + return provider; + } + + ConsumerConfig configureErrorClient(int port) { + ConsumerConfig consumer = new ConsumerConfig<>(); + consumer + .setInterfaceId(ErrorService.class.getName()) + .setApplication(new ApplicationConfig().setAppName("sofa-rpc-test-error-consumer")) + .setDirectUrl("bolt://127.0.0.1:" + port) + .setRegister(false) + .setTimeout(30000); + return consumer; + } + + @Test + void testSofaRpcBase() { + int port = PortUtils.findOpenPort(); + + ProviderConfig providerConfig = configureServer(port); + cleanup.deferCleanup(providerConfig::unExport); + providerConfig.export(); + + ConsumerConfig consumerConfig = configureGenericClient(port); + cleanup.deferCleanup(consumerConfig::unRefer); + GenericService genericService = consumerConfig.refer(); + + 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("com.alipay.sofa.rpc.api.GenericService/$invoke") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(RPC_SYSTEM, "sofa_rpc"), + equalTo(RPC_SERVICE, GenericService.class.getName()), + equalTo(RPC_METHOD, "$invoke"), + equalTo( + PEER_SERVICE, hasPeerService() ? "test-peer-service" : null), + equalTo(SERVER_ADDRESS, "127.0.0.1"), + satisfies(SERVER_PORT, k -> k.isInstanceOf(Long.class)), + satisfies( + NETWORK_PEER_ADDRESS, + AbstractSofaRpcTest::assertNetworkPeerAddress), + satisfies( + NETWORK_PEER_PORT, AbstractSofaRpcTest::assertNetworkPeerPort), + satisfies(NETWORK_TYPE, AbstractSofaRpcTest::assertNetworkType)), + span -> + span.hasName( + "io.opentelemetry.instrumentation.sofarpc.v5_4.api.HelloService/hello") + .hasKind(SpanKind.SERVER) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfying( + equalTo(RPC_SYSTEM, "sofa_rpc"), + equalTo( + RPC_SERVICE, + "io.opentelemetry.instrumentation.sofarpc.v5_4.api.HelloService"), + equalTo(RPC_METHOD, "hello"), + satisfies( + NETWORK_PEER_ADDRESS, + AbstractSofaRpcTest::assertNetworkPeerAddress), + satisfies( + NETWORK_PEER_PORT, + AbstractSofaRpcTest::assertNetworkPeerPort)))); + + testing() + .waitAndAssertMetrics( + "io.opentelemetry.sofa-rpc-5.4", + "rpc.server.duration", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasUnit("ms") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point.hasAttributesSatisfyingExactly( + equalTo(RPC_SYSTEM, "sofa_rpc"), + equalTo( + RPC_SERVICE, + "io.opentelemetry.instrumentation.sofarpc.v5_4.api.HelloService"), + equalTo(RPC_METHOD, "hello")))))); + + testing() + .waitAndAssertMetrics( + "io.opentelemetry.sofa-rpc-5.4", + "rpc.client.duration", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasUnit("ms") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point.hasAttributesSatisfyingExactly( + equalTo(RPC_SYSTEM, "sofa_rpc"), + equalTo( + RPC_SERVICE, GenericService.class.getName()), + equalTo(RPC_METHOD, "$invoke"), + equalTo(SERVER_ADDRESS, "127.0.0.1"), + satisfies( + SERVER_PORT, k -> k.isInstanceOf(Long.class)), + satisfies( + NETWORK_TYPE, + AbstractSofaRpcTest::assertNetworkType)))))); + } + + @Test + void testSofaRpcAsync() throws InterruptedException { + int port = PortUtils.findOpenPort(); + + ProviderConfig providerConfig = configureServer(port); + cleanup.deferCleanup(providerConfig::unExport); + providerConfig.export(); + + ConsumerConfig consumerConfig = configureGenericClient(port); + consumerConfig.setInvokeType(RpcConstants.INVOKER_TYPE_FUTURE); + cleanup.deferCleanup(consumerConfig::unRefer); + GenericService genericService = consumerConfig.refer(); + + Object result = + runWithSpan( + "parent", + () -> { + genericService.$invoke( + "hello", new String[] {String.class.getName()}, new Object[] {"hello"}); + return SofaResponseFuture.getResponse(5000, false); + }); + + assertThat(result).isEqualTo("hello"); + + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName("com.alipay.sofa.rpc.api.GenericService/$invoke") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(RPC_SYSTEM, "sofa_rpc"), + equalTo(RPC_SERVICE, GenericService.class.getName()), + equalTo(RPC_METHOD, "$invoke"), + equalTo( + PEER_SERVICE, hasPeerService() ? "test-peer-service" : null), + equalTo(SERVER_ADDRESS, "127.0.0.1"), + satisfies(SERVER_PORT, k -> k.isInstanceOf(Long.class)), + satisfies( + NETWORK_PEER_ADDRESS, + AbstractSofaRpcTest::assertNetworkPeerAddress), + satisfies( + NETWORK_PEER_PORT, AbstractSofaRpcTest::assertNetworkPeerPort), + satisfies(NETWORK_TYPE, AbstractSofaRpcTest::assertNetworkType)), + span -> + span.hasName( + "io.opentelemetry.instrumentation.sofarpc.v5_4.api.HelloService/hello") + .hasKind(SpanKind.SERVER) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfying( + equalTo(RPC_SYSTEM, "sofa_rpc"), + equalTo( + RPC_SERVICE, + "io.opentelemetry.instrumentation.sofarpc.v5_4.api.HelloService"), + equalTo(RPC_METHOD, "hello"), + satisfies( + NETWORK_PEER_ADDRESS, + AbstractSofaRpcTest::assertNetworkPeerAddress), + satisfies( + NETWORK_PEER_PORT, AbstractSofaRpcTest::assertNetworkPeerPort), + satisfies(NETWORK_TYPE, AbstractSofaRpcTest::assertNetworkType)))); + + testing() + .waitAndAssertMetrics( + "io.opentelemetry.sofa-rpc-5.4", + "rpc.server.duration", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasUnit("ms") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point.hasAttributesSatisfyingExactly( + equalTo(RPC_SYSTEM, "sofa_rpc"), + equalTo( + RPC_SERVICE, + "io.opentelemetry.instrumentation.sofarpc.v5_4.api.HelloService"), + equalTo(RPC_METHOD, "hello")))))); + + testing() + .waitAndAssertMetrics( + "io.opentelemetry.sofa-rpc-5.4", + "rpc.client.duration", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasUnit("ms") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point.hasAttributesSatisfyingExactly( + equalTo(RPC_SYSTEM, "sofa_rpc"), + equalTo( + RPC_SERVICE, GenericService.class.getName()), + equalTo(RPC_METHOD, "$invoke"), + equalTo(SERVER_ADDRESS, "127.0.0.1"), + satisfies( + SERVER_PORT, k -> k.isInstanceOf(Long.class)), + satisfies( + NETWORK_TYPE, + AbstractSofaRpcTest::assertNetworkType)))))); + } + + static void assertNetworkType(AbstractStringAssert stringAssert) { + stringAssert.satisfiesAnyOf( + // this attribute is not filled reliably, it is either null or + // "ipv4"/"ipv6" + val -> assertThat(val).isNull(), + val -> assertThat(val).isEqualTo("ipv4"), + val -> assertThat(val).isEqualTo("ipv6")); + } + + // Compatible with null returned by unresolved addresses + static void assertNetworkPeerAddress(AbstractAssert assertion) { + assertion.satisfiesAnyOf( + val -> assertThat(val).isNull(), val -> assertThat(val).isInstanceOf(String.class)); + } + + // Compatible with null returned by unresolved addresses + static void assertNetworkPeerPort(AbstractAssert assertion) { + assertion.satisfiesAnyOf( + val -> assertThat(val).isNull(), val -> assertThat(val).isInstanceOf(Long.class)); + } + + @Test + void testSofaRpcRpcException() { + int port = PortUtils.findOpenPort(); + + ProviderConfig providerConfig = configureErrorServer(port); + cleanup.deferCleanup(providerConfig::unExport); + providerConfig.export(); + + ConsumerConfig consumerConfig = configureErrorClient(port); + cleanup.deferCleanup(consumerConfig::unRefer); + ErrorService errorService = consumerConfig.refer(); + + assertThatThrownBy(() -> runWithSpan("parent", errorService::throwException)) + .isInstanceOf(SofaRpcRuntimeException.class); + + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName( + "io.opentelemetry.instrumentation.sofarpc.v5_4.api.ErrorService/throwException") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasStatus(StatusData.error()) + .hasAttributesSatisfyingExactly( + equalTo(RPC_SYSTEM, "sofa_rpc"), + equalTo( + RPC_SERVICE, + "io.opentelemetry.instrumentation.sofarpc.v5_4.api.ErrorService"), + equalTo(RPC_METHOD, "throwException"), + equalTo( + PEER_SERVICE, hasPeerService() ? "test-peer-service" : null), + equalTo(SERVER_ADDRESS, "127.0.0.1"), + satisfies(SERVER_PORT, k -> k.isInstanceOf(Long.class)), + satisfies( + NETWORK_PEER_ADDRESS, + AbstractSofaRpcTest::assertNetworkPeerAddress), + satisfies( + NETWORK_PEER_PORT, AbstractSofaRpcTest::assertNetworkPeerPort), + satisfies(NETWORK_TYPE, AbstractSofaRpcTest::assertNetworkType), + satisfies( + ERROR_TYPE, + errorType -> + errorType + .isInstanceOf(String.class) + .satisfies( + str -> + assertThat(str) + .contains("SofaRpcRuntimeException")))), + span -> + span.hasName( + "io.opentelemetry.instrumentation.sofarpc.v5_4.api.ErrorService/throwException") + .hasKind(SpanKind.SERVER) + .hasParent(trace.getSpan(1)) + .hasStatus(StatusData.error()) + .hasAttributesSatisfying( + equalTo(RPC_SYSTEM, "sofa_rpc"), + equalTo( + RPC_SERVICE, + "io.opentelemetry.instrumentation.sofarpc.v5_4.api.ErrorService"), + equalTo(RPC_METHOD, "throwException"), + satisfies( + NETWORK_PEER_ADDRESS, + AbstractSofaRpcTest::assertNetworkPeerAddress), + satisfies( + NETWORK_PEER_PORT, AbstractSofaRpcTest::assertNetworkPeerPort), + satisfies( + ERROR_TYPE, + errorType -> + errorType + .isInstanceOf(String.class) + .satisfies( + str -> + assertThat(str) + .contains("SofaRpcRuntimeException")))))); + testing() + .waitAndAssertMetrics( + "io.opentelemetry.sofa-rpc-5.4", + "rpc.server.duration", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasUnit("ms") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point.hasAttributesSatisfyingExactly( + equalTo(RPC_SYSTEM, "sofa_rpc"), + equalTo( + RPC_SERVICE, + "io.opentelemetry.instrumentation.sofarpc.v5_4.api.ErrorService"), + equalTo(RPC_METHOD, "throwException")))))); + + testing() + .waitAndAssertMetrics( + "io.opentelemetry.sofa-rpc-5.4", + "rpc.client.duration", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasUnit("ms") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point.hasAttributesSatisfying( + equalTo(RPC_SYSTEM, "sofa_rpc"), + equalTo( + RPC_SERVICE, + "io.opentelemetry.instrumentation.sofarpc.v5_4.api.ErrorService"), + equalTo(RPC_METHOD, "throwException"), + equalTo(SERVER_ADDRESS, "127.0.0.1"), + satisfies( + SERVER_PORT, k -> k.isInstanceOf(Long.class)), + satisfies( + NETWORK_TYPE, + AbstractSofaRpcTest::assertNetworkType)))))); + } + + @Test + void testSofaRpcBusinessException() { + int port = PortUtils.findOpenPort(); + + // Start error service provider + ProviderConfig providerConfig = configureErrorServer(port); + cleanup.deferCleanup(providerConfig::unExport); + providerConfig.export(); + + // Start consumer + ConsumerConfig consumerConfig = configureErrorClient(port); + cleanup.deferCleanup(consumerConfig::unRefer); + ErrorService errorService = consumerConfig.refer(); + + assertThatThrownBy(() -> runWithSpan("parent", errorService::throwBusinessException)) + .isInstanceOf(IllegalStateException.class); + + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName( + "io.opentelemetry.instrumentation.sofarpc.v5_4.api.ErrorService/throwBusinessException") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasStatus(StatusData.error()) + .hasAttributesSatisfyingExactly( + equalTo(RPC_SYSTEM, "sofa_rpc"), + equalTo( + RPC_SERVICE, + "io.opentelemetry.instrumentation.sofarpc.v5_4.api.ErrorService"), + equalTo(RPC_METHOD, "throwBusinessException"), + equalTo( + PEER_SERVICE, hasPeerService() ? "test-peer-service" : null), + equalTo(SERVER_ADDRESS, "127.0.0.1"), + satisfies(SERVER_PORT, k -> k.isInstanceOf(Long.class)), + satisfies( + NETWORK_PEER_ADDRESS, + AbstractSofaRpcTest::assertNetworkPeerAddress), + satisfies( + NETWORK_PEER_PORT, AbstractSofaRpcTest::assertNetworkPeerPort), + satisfies(NETWORK_TYPE, AbstractSofaRpcTest::assertNetworkType), + satisfies( + ERROR_TYPE, + errorType -> + errorType + .isInstanceOf(String.class) + .satisfies( + str -> + assertThat(str) + .contains("IllegalStateException")))), + span -> + span.hasName( + "io.opentelemetry.instrumentation.sofarpc.v5_4.api.ErrorService/throwBusinessException") + .hasKind(SpanKind.SERVER) + .hasParent(trace.getSpan(1)) + .hasStatus(StatusData.error()) + .hasAttributesSatisfying( + equalTo(RPC_SYSTEM, "sofa_rpc"), + equalTo( + RPC_SERVICE, + "io.opentelemetry.instrumentation.sofarpc.v5_4.api.ErrorService"), + equalTo(RPC_METHOD, "throwBusinessException"), + satisfies( + NETWORK_PEER_ADDRESS, + AbstractSofaRpcTest::assertNetworkPeerAddress), + satisfies( + NETWORK_PEER_PORT, AbstractSofaRpcTest::assertNetworkPeerPort), + satisfies( + ERROR_TYPE, + errorType -> + errorType + .isInstanceOf(String.class) + .satisfies( + str -> + assertThat(str) + .contains("IllegalStateException")))))); + } + + @Test + void testSofaRpcTimeout() { + int port = PortUtils.findOpenPort(); + + ProviderConfig providerConfig = configureErrorServer(port); + cleanup.deferCleanup(providerConfig::unExport); + providerConfig.export(); + + ConsumerConfig consumerConfig = configureErrorClient(port); + consumerConfig.setTimeout(1000); // 1 second timeout + cleanup.deferCleanup(consumerConfig::unRefer); + ErrorService errorService = consumerConfig.refer(); + + assertThatThrownBy(() -> runWithSpan("parent", errorService::timeout)) + .isInstanceOf(SofaTimeOutException.class); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName( + "io.opentelemetry.instrumentation.sofarpc.v5_4.api.ErrorService/timeout") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasStatus(StatusData.error()) + .hasAttributesSatisfyingExactly( + equalTo(RPC_SYSTEM, "sofa_rpc"), + equalTo( + RPC_SERVICE, + "io.opentelemetry.instrumentation.sofarpc.v5_4.api.ErrorService"), + equalTo(RPC_METHOD, "timeout"), + equalTo( + PEER_SERVICE, hasPeerService() ? "test-peer-service" : null), + equalTo(SERVER_ADDRESS, "127.0.0.1"), + satisfies(SERVER_PORT, k -> k.isInstanceOf(Long.class)), + satisfies( + NETWORK_PEER_ADDRESS, + AbstractSofaRpcTest::assertNetworkPeerAddress), + satisfies( + NETWORK_PEER_PORT, AbstractSofaRpcTest::assertNetworkPeerPort), + satisfies(NETWORK_TYPE, AbstractSofaRpcTest::assertNetworkType), + satisfies( + ERROR_TYPE, + errorType -> + errorType + .isInstanceOf(String.class) + .satisfies( + str -> + assertThat(str) + .satisfiesAnyOf( + s -> assertThat(s).contains("timeout"), + s -> assertThat(s).contains("Timeout"), + s -> + assertThat(s) + .contains( + "SofaTimeOutException"))))))); + } +} diff --git a/instrumentation/sofa-rpc-5.4/testing/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/AbstractSofaRpcTraceChainTest.java b/instrumentation/sofa-rpc-5.4/testing/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/AbstractSofaRpcTraceChainTest.java new file mode 100644 index 000000000000..2ddaab09c721 --- /dev/null +++ b/instrumentation/sofa-rpc-5.4/testing/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/AbstractSofaRpcTraceChainTest.java @@ -0,0 +1,454 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.sofarpc.v5_4; + +import static io.opentelemetry.instrumentation.testing.GlobalTraceUtil.runWithSpan; +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.NetworkAttributes.NETWORK_PEER_ADDRESS; +import static io.opentelemetry.semconv.NetworkAttributes.NETWORK_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.PeerIncubatingAttributes.PEER_SERVICE; +import static io.opentelemetry.semconv.incubating.RpcIncubatingAttributes.RPC_METHOD; +import static io.opentelemetry.semconv.incubating.RpcIncubatingAttributes.RPC_SERVICE; +import static io.opentelemetry.semconv.incubating.RpcIncubatingAttributes.RPC_SYSTEM; + +import com.alipay.sofa.rpc.api.GenericService; +import com.alipay.sofa.rpc.config.ApplicationConfig; +import com.alipay.sofa.rpc.config.ConsumerConfig; +import com.alipay.sofa.rpc.config.ProviderConfig; +import com.alipay.sofa.rpc.config.ServerConfig; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.sofarpc.v5_4.api.HelloService; +import io.opentelemetry.instrumentation.sofarpc.v5_4.api.MiddleService; +import io.opentelemetry.instrumentation.sofarpc.v5_4.impl.HelloServiceImpl; +import io.opentelemetry.instrumentation.sofarpc.v5_4.impl.MiddleServiceImpl; +import io.opentelemetry.instrumentation.test.utils.PortUtils; +import io.opentelemetry.instrumentation.testing.internal.AutoCleanupExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +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 AbstractSofaRpcTraceChainTest { + + @RegisterExtension static final AutoCleanupExtension cleanup = AutoCleanupExtension.create(); + + @BeforeAll + static void setUp() {} + + @AfterAll + static void tearDown() {} + + protected abstract InstrumentationExtension testing(); + + protected abstract boolean hasPeerService(); + + ConsumerConfig configureClient(int port) { + ConsumerConfig consumer = new ConsumerConfig<>(); + consumer + .setInterfaceId(HelloService.class.getName()) + .setApplication(new ApplicationConfig().setAppName("sofa-rpc-test-consumer")) + .setDirectUrl("bolt://127.0.0.1:" + port) + .setRegister(false) + .setTimeout(30000); + return consumer; + } + + ConsumerConfig configureLocalClient(int port) { + ConsumerConfig consumer = new ConsumerConfig<>(); + consumer + .setInterfaceId(HelloService.class.getName()) + .setApplication(new ApplicationConfig().setAppName("sofa-rpc-test-consumer")) + .setDirectUrl("local://127.0.0.1:" + port) + .setRegister(false) + .setInJVM(true) + .setTimeout(30000); + return consumer; + } + + ConsumerConfig configureGenericMiddleClient(int port) { + ConsumerConfig consumer = new ConsumerConfig<>(); + consumer + .setInterfaceId(MiddleService.class.getName()) + .setApplication(new ApplicationConfig().setAppName("sofa-rpc-test-consumer")) + .setDirectUrl("bolt://127.0.0.1:" + port) + .setRegister(false) + .setTimeout(30000) + .setGeneric(true); + return consumer; + } + + ServerConfig getServerConfig(int port) { + return new ServerConfig() + .setProtocol("bolt") + .setHost("127.0.0.1") + .setPort(port) + .setDaemon(false); + } + + ProviderConfig configureServer(int port) { + ProviderConfig provider = new ProviderConfig<>(); + provider + .setInterfaceId(HelloService.class.getName()) + .setRef(new HelloServiceImpl()) + .setApplication(new ApplicationConfig().setAppName("sofa-rpc-test-provider")) + .setServer(getServerConfig(port)) + .setRegister(false); + return provider; + } + + ProviderConfig configureMiddleServer( + int port, ConsumerConfig consumerConfig) { + ProviderConfig provider = new ProviderConfig<>(); + provider + .setInterfaceId(MiddleService.class.getName()) + .setRef(new MiddleServiceImpl(consumerConfig)) + .setApplication(new ApplicationConfig().setAppName("sofa-rpc-test-middle")) + .setServer(getServerConfig(port)) + .setRegister(false); + return provider; + } + + @Test + @DisplayName("test that context is propagated correctly in chained sofa-rpc calls") + void testSofaRpcChain() { + int port = PortUtils.findOpenPorts(2); + int middlePort = port + 1; + + // setup hello service provider + ProviderConfig helloProviderConfig = configureServer(port); + cleanup.deferCleanup(helloProviderConfig::unExport); + helloProviderConfig.export(); + + // set hello service consumer + ConsumerConfig helloConsumerConfig = configureClient(port); + cleanup.deferCleanup(helloConsumerConfig::unRefer); + + // setup middle service provider + ProviderConfig middleProviderConfig = + configureMiddleServer(middlePort, helloConsumerConfig); + cleanup.deferCleanup(middleProviderConfig::unExport); + middleProviderConfig.export(); + + // setup middle service consumer + ConsumerConfig middleConsumerConfig = configureGenericMiddleClient(middlePort); + cleanup.deferCleanup(middleConsumerConfig::unRefer); + GenericService genericMiddleService = middleConsumerConfig.refer(); + + Object response = + runWithSpan( + "parent", + () -> + genericMiddleService.$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("com.alipay.sofa.rpc.api.GenericService/$invoke") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(RPC_SYSTEM, "sofa_rpc"), + equalTo(RPC_SERVICE, GenericService.class.getName()), + equalTo(RPC_METHOD, "$invoke"), + equalTo( + PEER_SERVICE, hasPeerService() ? "test-peer-service" : null), + equalTo(SERVER_ADDRESS, "127.0.0.1"), + satisfies(SERVER_PORT, k -> k.isInstanceOf(Long.class)), + satisfies( + NETWORK_PEER_ADDRESS, + AbstractSofaRpcTest::assertNetworkPeerAddress), + satisfies( + NETWORK_PEER_PORT, AbstractSofaRpcTest::assertNetworkPeerPort), + satisfies(NETWORK_TYPE, AbstractSofaRpcTest::assertNetworkType)), + span -> + span.hasName( + "io.opentelemetry.instrumentation.sofarpc.v5_4.api.MiddleService/hello") + .hasKind(SpanKind.SERVER) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfying( + equalTo(RPC_SYSTEM, "sofa_rpc"), + equalTo( + RPC_SERVICE, + "io.opentelemetry.instrumentation.sofarpc.v5_4.api.MiddleService"), + equalTo(RPC_METHOD, "hello"), + satisfies( + NETWORK_PEER_ADDRESS, + AbstractSofaRpcTest::assertNetworkPeerAddress), + satisfies( + NETWORK_PEER_PORT, AbstractSofaRpcTest::assertNetworkPeerPort)), + span -> + span.hasName( + "io.opentelemetry.instrumentation.sofarpc.v5_4.api.HelloService/hello") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(2)) + .hasAttributesSatisfyingExactly( + equalTo(RPC_SYSTEM, "sofa_rpc"), + equalTo( + RPC_SERVICE, + "io.opentelemetry.instrumentation.sofarpc.v5_4.api.HelloService"), + equalTo(RPC_METHOD, "hello"), + equalTo( + PEER_SERVICE, hasPeerService() ? "test-peer-service" : null), + equalTo(SERVER_ADDRESS, "127.0.0.1"), + satisfies(SERVER_PORT, k -> k.isInstanceOf(Long.class)), + satisfies( + NETWORK_PEER_ADDRESS, + AbstractSofaRpcTest::assertNetworkPeerAddress), + satisfies( + NETWORK_PEER_PORT, AbstractSofaRpcTest::assertNetworkPeerPort), + satisfies(NETWORK_TYPE, AbstractSofaRpcTest::assertNetworkType)), + span -> + span.hasName( + "io.opentelemetry.instrumentation.sofarpc.v5_4.api.HelloService/hello") + .hasKind(SpanKind.SERVER) + .hasParent(trace.getSpan(3)) + .hasAttributesSatisfying( + equalTo(RPC_SYSTEM, "sofa_rpc"), + equalTo( + RPC_SERVICE, + "io.opentelemetry.instrumentation.sofarpc.v5_4.api.HelloService"), + equalTo(RPC_METHOD, "hello"), + satisfies( + NETWORK_PEER_ADDRESS, + AbstractSofaRpcTest::assertNetworkPeerAddress), + satisfies( + NETWORK_PEER_PORT, + AbstractSofaRpcTest::assertNetworkPeerPort)))); + + testing() + .waitAndAssertMetrics( + "io.opentelemetry.sofa-rpc-5.4", + "rpc.server.duration", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasUnit("ms") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point.hasAttributesSatisfyingExactly( + equalTo(RPC_SYSTEM, "sofa_rpc"), + equalTo( + RPC_SERVICE, + "io.opentelemetry.instrumentation.sofarpc.v5_4.api.HelloService"), + equalTo(RPC_METHOD, "hello")), + point -> + point.hasAttributesSatisfyingExactly( + equalTo(RPC_SYSTEM, "sofa_rpc"), + equalTo( + RPC_SERVICE, + "io.opentelemetry.instrumentation.sofarpc.v5_4.api.MiddleService"), + equalTo(RPC_METHOD, "hello")))))); + + testing() + .waitAndAssertMetrics( + "io.opentelemetry.sofa-rpc-5.4", + "rpc.client.duration", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasUnit("ms") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point.hasAttributesSatisfyingExactly( + equalTo(RPC_SYSTEM, "sofa_rpc"), + equalTo( + RPC_SERVICE, GenericService.class.getName()), + equalTo(RPC_METHOD, "$invoke"), + equalTo(SERVER_ADDRESS, "127.0.0.1"), + satisfies( + SERVER_PORT, k -> k.isInstanceOf(Long.class)), + satisfies( + NETWORK_TYPE, + AbstractSofaRpcTest::assertNetworkType)), + point -> + point.hasAttributesSatisfyingExactly( + equalTo(RPC_SYSTEM, "sofa_rpc"), + equalTo( + RPC_SERVICE, + "io.opentelemetry.instrumentation.sofarpc.v5_4.api.HelloService"), + equalTo(RPC_METHOD, "hello"), + equalTo(SERVER_ADDRESS, "127.0.0.1"), + satisfies( + SERVER_PORT, k -> k.isInstanceOf(Long.class)), + satisfies( + NETWORK_TYPE, + AbstractSofaRpcTest::assertNetworkType)))))); + } + + @Test + @DisplayName("test ignore local calls") + void testSofaRpcChainLocal() { + int port = PortUtils.findOpenPort(); + + // Setup middle service provider with HelloService provider in same process for local calls + ConsumerConfig helloConsumerConfig = configureLocalClient(port); + cleanup.deferCleanup(helloConsumerConfig::unRefer); + + ProviderConfig helloProviderConfig = configureServer(port); + cleanup.deferCleanup(helloProviderConfig::unExport); + helloProviderConfig.export(); + + ProviderConfig middleProviderConfig = + configureMiddleServer(port, helloConsumerConfig); + cleanup.deferCleanup(middleProviderConfig::unExport); + middleProviderConfig.export(); + + // Setup middle service consumer + ConsumerConfig middleConsumerConfig = configureGenericMiddleClient(port); + cleanup.deferCleanup(middleConsumerConfig::unRefer); + GenericService genericMiddleService = middleConsumerConfig.refer(); + + Object response = + runWithSpan( + "parent", + () -> + genericMiddleService.$invoke( + "hello", new String[] {String.class.getName()}, new Object[] {"hello"})); + + assertThat(response).isEqualTo("hello"); + // Strategy: Only skip CLIENT spans for local calls, keep SERVER spans + // The local call from MiddleService to HelloService will: + // - Skip CLIENT span (detected via ConsumerConfig.isInJVM() or directUrl="local://") + // - Keep SERVER span (server cannot reliably detect local calls) + // Note: Since the CLIENT span is skipped, trace context may not propagate correctly, + // resulting in HelloService SERVER span appearing in a separate trace + testing() + .waitAndAssertTraces( + // First trace: parent -> MiddleService CLIENT -> MiddleService SERVER + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName("com.alipay.sofa.rpc.api.GenericService/$invoke") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(RPC_SYSTEM, "sofa_rpc"), + equalTo(RPC_SERVICE, GenericService.class.getName()), + equalTo(RPC_METHOD, "$invoke"), + equalTo( + PEER_SERVICE, hasPeerService() ? "test-peer-service" : null), + equalTo(SERVER_ADDRESS, "127.0.0.1"), + satisfies(SERVER_PORT, k -> k.isInstanceOf(Long.class)), + satisfies( + NETWORK_PEER_ADDRESS, + AbstractSofaRpcTest::assertNetworkPeerAddress), + satisfies( + NETWORK_PEER_PORT, AbstractSofaRpcTest::assertNetworkPeerPort), + satisfies(NETWORK_TYPE, AbstractSofaRpcTest::assertNetworkType)), + span -> + span.hasName( + "io.opentelemetry.instrumentation.sofarpc.v5_4.api.MiddleService/hello") + .hasKind(SpanKind.SERVER) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfying( + equalTo(RPC_SYSTEM, "sofa_rpc"), + equalTo( + RPC_SERVICE, + "io.opentelemetry.instrumentation.sofarpc.v5_4.api.MiddleService"), + equalTo(RPC_METHOD, "hello"), + satisfies( + NETWORK_PEER_ADDRESS, + AbstractSofaRpcTest::assertNetworkPeerAddress), + satisfies( + NETWORK_PEER_PORT, + AbstractSofaRpcTest::assertNetworkPeerPort))), + // Second trace: HelloService SERVER span (appears in separate trace because CLIENT span + // was skipped) + // This is expected behavior when CLIENT span is skipped - trace context may not + // propagate + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName( + "io.opentelemetry.instrumentation.sofarpc.v5_4.api.HelloService/hello") + .hasKind(SpanKind.SERVER) + .hasNoParent() // No parent because CLIENT span was skipped + .hasAttributesSatisfying( + equalTo(RPC_SYSTEM, "sofa_rpc"), + equalTo( + RPC_SERVICE, + "io.opentelemetry.instrumentation.sofarpc.v5_4.api.HelloService"), + equalTo(RPC_METHOD, "hello"), + satisfies( + NETWORK_PEER_ADDRESS, + AbstractSofaRpcTest::assertNetworkPeerAddress), + satisfies( + NETWORK_PEER_PORT, + AbstractSofaRpcTest::assertNetworkPeerPort)))); + + testing() + .waitAndAssertMetrics( + "io.opentelemetry.sofa-rpc-5.4", + "rpc.server.duration", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasUnit("ms") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point.hasAttributesSatisfyingExactly( + equalTo(RPC_SYSTEM, "sofa_rpc"), + equalTo( + RPC_SERVICE, + "io.opentelemetry.instrumentation.sofarpc.v5_4.api.MiddleService"), + equalTo(RPC_METHOD, "hello")), + // HelloService SERVER metrics are also kept for local calls + point -> + point.hasAttributesSatisfyingExactly( + equalTo(RPC_SYSTEM, "sofa_rpc"), + equalTo( + RPC_SERVICE, + "io.opentelemetry.instrumentation.sofarpc.v5_4.api.HelloService"), + equalTo(RPC_METHOD, "hello")))))); + + testing() + .waitAndAssertMetrics( + "io.opentelemetry.sofa-rpc-5.4", + "rpc.client.duration", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasUnit("ms") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point.hasAttributesSatisfyingExactly( + equalTo(RPC_SYSTEM, "sofa_rpc"), + equalTo( + RPC_SERVICE, GenericService.class.getName()), + equalTo(RPC_METHOD, "$invoke"), + equalTo(SERVER_ADDRESS, "127.0.0.1"), + satisfies( + SERVER_PORT, k -> k.isInstanceOf(Long.class)), + satisfies( + NETWORK_TYPE, + AbstractSofaRpcTest::assertNetworkType)))))); + } +} diff --git a/instrumentation/sofa-rpc-5.4/testing/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/api/ErrorService.java b/instrumentation/sofa-rpc-5.4/testing/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/api/ErrorService.java new file mode 100644 index 000000000000..40866eb5ac6f --- /dev/null +++ b/instrumentation/sofa-rpc-5.4/testing/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/api/ErrorService.java @@ -0,0 +1,14 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.sofarpc.v5_4.api; + +public interface ErrorService { + String throwException(); + + String throwBusinessException(); + + String timeout(); +} diff --git a/instrumentation/sofa-rpc-5.4/testing/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/api/HelloService.java b/instrumentation/sofa-rpc-5.4/testing/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/api/HelloService.java new file mode 100644 index 000000000000..6ec34cb0b2ae --- /dev/null +++ b/instrumentation/sofa-rpc-5.4/testing/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/api/HelloService.java @@ -0,0 +1,10 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.sofarpc.v5_4.api; + +public interface HelloService { + String hello(String hello); +} diff --git a/instrumentation/sofa-rpc-5.4/testing/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/api/MiddleService.java b/instrumentation/sofa-rpc-5.4/testing/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/api/MiddleService.java new file mode 100644 index 000000000000..a0021ed52af6 --- /dev/null +++ b/instrumentation/sofa-rpc-5.4/testing/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/api/MiddleService.java @@ -0,0 +1,10 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.sofarpc.v5_4.api; + +public interface MiddleService { + String hello(String hello); +} diff --git a/instrumentation/sofa-rpc-5.4/testing/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/impl/ErrorServiceImpl.java b/instrumentation/sofa-rpc-5.4/testing/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/impl/ErrorServiceImpl.java new file mode 100644 index 000000000000..5881e7109052 --- /dev/null +++ b/instrumentation/sofa-rpc-5.4/testing/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/impl/ErrorServiceImpl.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.sofarpc.v5_4.impl; + +import com.alipay.sofa.rpc.core.exception.SofaRpcRuntimeException; +import io.opentelemetry.instrumentation.sofarpc.v5_4.api.ErrorService; + +public class ErrorServiceImpl implements ErrorService { + @Override + public String throwException() { + throw new SofaRpcRuntimeException("RPC error"); + } + + @Override + public String throwBusinessException() { + throw new IllegalStateException("Business error"); + } + + @Override + public String timeout() { + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + return "timeout"; + } +} diff --git a/instrumentation/sofa-rpc-5.4/testing/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/impl/HelloServiceImpl.java b/instrumentation/sofa-rpc-5.4/testing/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/impl/HelloServiceImpl.java new file mode 100644 index 000000000000..20bfa4861142 --- /dev/null +++ b/instrumentation/sofa-rpc-5.4/testing/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/impl/HelloServiceImpl.java @@ -0,0 +1,15 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.sofarpc.v5_4.impl; + +import io.opentelemetry.instrumentation.sofarpc.v5_4.api.HelloService; + +public class HelloServiceImpl implements HelloService { + @Override + public String hello(String hello) { + return hello; + } +} diff --git a/instrumentation/sofa-rpc-5.4/testing/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/impl/MiddleServiceImpl.java b/instrumentation/sofa-rpc-5.4/testing/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/impl/MiddleServiceImpl.java new file mode 100644 index 000000000000..c7bc8d167f04 --- /dev/null +++ b/instrumentation/sofa-rpc-5.4/testing/src/main/java/io/opentelemetry/instrumentation/sofarpc/v5_4/impl/MiddleServiceImpl.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.sofarpc.v5_4.impl; + +import com.alipay.sofa.rpc.config.ConsumerConfig; +import io.opentelemetry.instrumentation.sofarpc.v5_4.api.HelloService; +import io.opentelemetry.instrumentation.sofarpc.v5_4.api.MiddleService; + +public class MiddleServiceImpl implements MiddleService { + + private final ConsumerConfig consumerConfig; + + public MiddleServiceImpl(ConsumerConfig consumerConfig) { + this.consumerConfig = consumerConfig; + } + + @Override + public String hello(String hello) { + HelloService helloService = consumerConfig.refer(); + return helloService.hello(hello); + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index c7da8ed4fdf4..ab8f82bd6a49 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -608,6 +608,9 @@ include(":instrumentation:servlet:servlet-common:bootstrap") include(":instrumentation:servlet:servlet-common:javaagent") include(":instrumentation:servlet:servlet-common:library") include(":instrumentation:servlet:servlet-javax-common:library") +include(":instrumentation:sofa-rpc-5.4:javaagent") +include(":instrumentation:sofa-rpc-5.4:library-autoconfigure") +include(":instrumentation:sofa-rpc-5.4:testing") include(":instrumentation:spark-2.3:javaagent") include(":instrumentation:spring:spring-batch-3.0:javaagent") include(":instrumentation:spring:spring-boot-actuator-autoconfigure-2.0:javaagent")