diff --git a/docs/supported-libraries.md b/docs/supported-libraries.md index 6cddd284af7d..54a25f62542c 100644 --- a/docs/supported-libraries.md +++ b/docs/supported-libraries.md @@ -90,6 +90,7 @@ These are the supported libraries and frameworks: | [Jedis](https://github.com/xetorthio/jedis) | 1.4+ | N/A | [Database Client Spans] | | [JMS](https://javaee.github.io/javaee-spec/javadocs/javax/jms/package-summary.html) | 1.1+ | N/A | [Messaging Spans] | | [Jodd Http](https://http.jodd.org/) | 4.2+ | N/A | [HTTP Client Spans], [HTTP Client Metrics] | +| [JSON-RPC](https://github.com/briandilley/jsonrpc4j) | 1.3.3+ | N/A | [RPC Client Spans], [RPC Client Metrics], [RPC Server Spans], [RPC Server Metrics] | | [JSP](https://javaee.github.io/javaee-spec/javadocs/javax/servlet/jsp/package-summary.html) | 2.3.x only | N/A | Controller Spans [3] | | [Kotlin Coroutines](https://kotlinlang.org/docs/coroutines-overview.html) | 1.0+ | N/A | Context propagation | | [Ktor](https://github.com/ktorio/ktor) | 1.0+ | [opentelemetry-ktor-1.0](../instrumentation/ktor/ktor-1.0/library),
[opentelemetry-ktor-2.0](../instrumentation/ktor/ktor-2.0/library),
[opentelemetry-ktor-3.0](../instrumentation/ktor/ktor-3.0/library) | [HTTP Client Spans], [HTTP Client Metrics], [HTTP Server Spans], [HTTP Server Metrics] | diff --git a/instrumentation/jsonrpc4j-1.3/javaagent/build.gradle.kts b/instrumentation/jsonrpc4j-1.3/javaagent/build.gradle.kts new file mode 100644 index 000000000000..34a4315d8acd --- /dev/null +++ b/instrumentation/jsonrpc4j-1.3/javaagent/build.gradle.kts @@ -0,0 +1,34 @@ +plugins { + id("otel.javaagent-instrumentation") +} + +muzzle { + pass { + group.set("com.github.briandilley.jsonrpc4j") + module.set("jsonrpc4j") + versions.set("[1.3.3,)") + assertInverse.set(true) + } +} + +dependencies { + implementation(project(":instrumentation:jsonrpc4j-1.3:library")) + + library("com.github.briandilley.jsonrpc4j:jsonrpc4j:1.3.3") + + testImplementation(project(":instrumentation:jsonrpc4j-1.3:testing")) + + testImplementation("com.fasterxml.jackson.core:jackson-databind:2.13.3") + + testImplementation("org.eclipse.jetty:jetty-server:9.4.49.v20220914") + + testImplementation("org.eclipse.jetty:jetty-servlet:9.4.49.v20220914") + + testImplementation("javax.portlet:portlet-api:2.0") +} + +tasks { + test { + jvmArgs("-Dotel.javaagent.experimental.thread-propagation-debugger.enabled=false") + } +} diff --git a/instrumentation/jsonrpc4j-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jsonrpc4j/v1_3/JsonRpcClientInstrumentation.java b/instrumentation/jsonrpc4j-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jsonrpc4j/v1_3/JsonRpcClientInstrumentation.java new file mode 100644 index 000000000000..605c241a9507 --- /dev/null +++ b/instrumentation/jsonrpc4j-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jsonrpc4j/v1_3/JsonRpcClientInstrumentation.java @@ -0,0 +1,96 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jsonrpc4j.v1_3; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.instrumentation.jsonrpc4j.v1_3.JsonRpcSingletons.CLIENT_INSTRUMENTER; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.jsonrpc4j.v1_3.JsonRpcClientRequest; +import io.opentelemetry.instrumentation.jsonrpc4j.v1_3.JsonRpcClientResponse; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.util.Map; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class JsonRpcClientInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("com.googlecode.jsonrpc4j.IJsonRpcClient"); + } + + @Override + public ElementMatcher typeMatcher() { + // match JsonRpcHttpClient and JsonRpcRestClient + return implementsInterface(named("com.googlecode.jsonrpc4j.IJsonRpcClient")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(named("invoke")) + .and(takesArguments(4)) + .and(takesArgument(0, String.class)) + .and(takesArgument(1, Object.class)) + .and(takesArgument(2, named("java.lang.reflect.Type"))) + .and(takesArgument(3, named("java.util.Map"))) + .and(returns(Object.class)), + this.getClass().getName() + "$InvokeAdvice"); + } + + @SuppressWarnings("unused") + public static class InvokeAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(0) String methodName, + @Advice.Argument(1) Object argument, + @Advice.Argument(3) Map extraHeaders, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + Context parentContext = Context.current(); + JsonRpcClientRequest request = new JsonRpcClientRequest(methodName, argument); + if (!CLIENT_INSTRUMENTER.shouldStart(parentContext, request)) { + return; + } + + context = CLIENT_INSTRUMENTER.start(parentContext, request); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit( + @Advice.Argument(0) String methodName, + @Advice.Argument(1) Object argument, + @Advice.Argument(3) Map extraHeaders, + @Advice.Return Object result, + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + + scope.close(); + CLIENT_INSTRUMENTER.end( + context, + new JsonRpcClientRequest(methodName, argument), + new JsonRpcClientResponse(result), + throwable); + } + } +} diff --git a/instrumentation/jsonrpc4j-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jsonrpc4j/v1_3/JsonRpcInstrumentationModule.java b/instrumentation/jsonrpc4j-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jsonrpc4j/v1_3/JsonRpcInstrumentationModule.java new file mode 100644 index 000000000000..7905681868de --- /dev/null +++ b/instrumentation/jsonrpc4j-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jsonrpc4j/v1_3/JsonRpcInstrumentationModule.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jsonrpc4j.v1_3; + +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class JsonRpcInstrumentationModule extends InstrumentationModule { + public JsonRpcInstrumentationModule() { + super("jsonrpc4j", "jsonrpc4j-1.3"); + } + + @Override + public List typeInstrumentations() { + return asList( + new JsonRpcServerInstrumentation(), + new JsonRpcClientInstrumentation(), + new JsonRpcProxyInstrumentation()); + } +} diff --git a/instrumentation/jsonrpc4j-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jsonrpc4j/v1_3/JsonRpcProxyInstrumentation.java b/instrumentation/jsonrpc4j-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jsonrpc4j/v1_3/JsonRpcProxyInstrumentation.java new file mode 100644 index 000000000000..a04e074d2f00 --- /dev/null +++ b/instrumentation/jsonrpc4j-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jsonrpc4j/v1_3/JsonRpcProxyInstrumentation.java @@ -0,0 +1,123 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jsonrpc4j.v1_3; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; +import static io.opentelemetry.javaagent.instrumentation.jsonrpc4j.v1_3.JsonRpcSingletons.CLIENT_INSTRUMENTER; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPrivate; +import static net.bytebuddy.matcher.ElementMatchers.isStatic; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import com.googlecode.jsonrpc4j.IJsonRpcClient; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.jsonrpc4j.v1_3.JsonRpcClientRequest; +import io.opentelemetry.instrumentation.jsonrpc4j.v1_3.JsonRpcClientResponse; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.Map; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class JsonRpcProxyInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("com.googlecode.jsonrpc4j.ProxyUtil"); + } + + @Override + public ElementMatcher typeMatcher() { + return named("com.googlecode.jsonrpc4j.ProxyUtil"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(isStatic()).and(isPrivate()).and(named("createClientProxy")), + this.getClass().getName() + "$CreateClientProxyAdvice"); + } + + @SuppressWarnings({"unused"}) + public static class CreateClientProxyAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onExit( + @Advice.Argument(0) ClassLoader classLoader, + @Advice.Argument(1) Class proxyInterface, + @Advice.Argument(2) IJsonRpcClient client, + @Advice.Argument(3) Map extraHeaders, + @Advice.Return(readOnly = false) Object proxy) { + + proxy = instrumentCreateClientProxy(classLoader, proxyInterface, client, extraHeaders, proxy); + } + + private static Object proxyObjectMethods(Method method, Object proxyObject, Object[] args) { + String name = method.getName(); + switch (name) { + case "toString": + return proxyObject.getClass().getName() + "@" + System.identityHashCode(proxyObject); + case "hashCode": + return System.identityHashCode(proxyObject); + case "equals": + return proxyObject == args[0]; + default: + throw new IllegalArgumentException( + method.getName() + " is not a member of java.lang.Object"); + } + } + + @SuppressWarnings({"unchecked"}) + public static T instrumentCreateClientProxy( + ClassLoader classLoader, + Class proxyInterface, + IJsonRpcClient client, + Map extraHeaders, + Object proxy) { + + return (T) + Proxy.newProxyInstance( + classLoader, + new Class[] {proxyInterface}, + new InvocationHandler() { + @Override + public Object invoke(Object proxy1, Method method, Object[] args) throws Throwable { + // before invoke + Context parentContext = Context.current(); + JsonRpcClientRequest request = new JsonRpcClientRequest(method, args); + if (!CLIENT_INSTRUMENTER.shouldStart(parentContext, request)) { + return method.invoke(proxy, args); + } + + Context context = CLIENT_INSTRUMENTER.start(parentContext, request); + Scope scope = context.makeCurrent(); + try { + Object result = method.invoke(proxy, args); + // after invoke + scope.close(); + CLIENT_INSTRUMENTER.end( + context, + new JsonRpcClientRequest(method, args), + new JsonRpcClientResponse(result), + null); + return result; + + } catch (Throwable t) { + // after invoke + scope.close(); + CLIENT_INSTRUMENTER.end(context, request, null, t); + throw t; + } + } + }); + } + } +} diff --git a/instrumentation/jsonrpc4j-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jsonrpc4j/v1_3/JsonRpcServerInstrumentation.java b/instrumentation/jsonrpc4j-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jsonrpc4j/v1_3/JsonRpcServerInstrumentation.java new file mode 100644 index 000000000000..da97523bca72 --- /dev/null +++ b/instrumentation/jsonrpc4j-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jsonrpc4j/v1_3/JsonRpcServerInstrumentation.java @@ -0,0 +1,84 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jsonrpc4j.v1_3; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; +import static io.opentelemetry.javaagent.instrumentation.jsonrpc4j.v1_3.JsonRpcSingletons.SERVER_INVOCATION_LISTENER; +import static net.bytebuddy.matcher.ElementMatchers.isConstructor; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import com.googlecode.jsonrpc4j.InvocationListener; +import com.googlecode.jsonrpc4j.JsonRpcBasicServer; +import com.googlecode.jsonrpc4j.MultipleInvocationListener; +import io.opentelemetry.instrumentation.api.util.VirtualField; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.implementation.bytecode.assign.Assigner; +import net.bytebuddy.matcher.ElementMatcher; + +public class JsonRpcServerInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("com.googlecode.jsonrpc4j.JsonRpcBasicServer"); + } + + @Override + public ElementMatcher typeMatcher() { + return named("com.googlecode.jsonrpc4j.JsonRpcBasicServer"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isConstructor(), this.getClass().getName() + "$ConstructorAdvice"); + + transformer.applyAdviceToMethod( + isMethod().and(named("setInvocationListener")), + this.getClass().getName() + "$SetInvocationListenerAdvice"); + } + + @SuppressWarnings("unused") + public static class ConstructorAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void setInvocationListener( + @Advice.This JsonRpcBasicServer jsonRpcServer, + @Advice.FieldValue(value = "invocationListener", readOnly = false) + InvocationListener invocationListener) { + invocationListener = SERVER_INVOCATION_LISTENER; + } + } + + @SuppressWarnings("unused") + public static class SetInvocationListenerAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void setInvocationListener( + @Advice.This JsonRpcBasicServer jsonRpcServer, + @Advice.Argument(value = 0, readOnly = false, typing = Assigner.Typing.DYNAMIC) + InvocationListener invocationListener) { + VirtualField instrumented = + VirtualField.find(JsonRpcBasicServer.class, Boolean.class); + if (!Boolean.TRUE.equals(instrumented.get(jsonRpcServer))) { + if (invocationListener == null) { + invocationListener = SERVER_INVOCATION_LISTENER; + } else if (invocationListener instanceof MultipleInvocationListener) { + ((MultipleInvocationListener) invocationListener) + .addInvocationListener(SERVER_INVOCATION_LISTENER); + } else { + invocationListener = + new MultipleInvocationListener(invocationListener, SERVER_INVOCATION_LISTENER); + } + + instrumented.set(jsonRpcServer, true); + } + } + } +} diff --git a/instrumentation/jsonrpc4j-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jsonrpc4j/v1_3/JsonRpcSingletons.java b/instrumentation/jsonrpc4j-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jsonrpc4j/v1_3/JsonRpcSingletons.java new file mode 100644 index 000000000000..85c9dc33c2a8 --- /dev/null +++ b/instrumentation/jsonrpc4j-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jsonrpc4j/v1_3/JsonRpcSingletons.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jsonrpc4j.v1_3; + +import com.googlecode.jsonrpc4j.InvocationListener; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.jsonrpc4j.v1_3.JsonRpcClientRequest; +import io.opentelemetry.instrumentation.jsonrpc4j.v1_3.JsonRpcClientResponse; +import io.opentelemetry.instrumentation.jsonrpc4j.v1_3.JsonRpcTelemetry; + +public final class JsonRpcSingletons { + + public static final InvocationListener SERVER_INVOCATION_LISTENER; + + public static final Instrumenter CLIENT_INSTRUMENTER; + + static { + JsonRpcTelemetry telemetry = JsonRpcTelemetry.builder(GlobalOpenTelemetry.get()).build(); + + SERVER_INVOCATION_LISTENER = telemetry.newServerInvocationListener(); + CLIENT_INSTRUMENTER = telemetry.getClientInstrumenter(); + } + + private JsonRpcSingletons() {} +} diff --git a/instrumentation/jsonrpc4j-1.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jsonrpc4j/v1_3/AgentJsonRpcTest.java b/instrumentation/jsonrpc4j-1.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jsonrpc4j/v1_3/AgentJsonRpcTest.java new file mode 100644 index 000000000000..cac7e9e20d72 --- /dev/null +++ b/instrumentation/jsonrpc4j-1.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jsonrpc4j/v1_3/AgentJsonRpcTest.java @@ -0,0 +1,144 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jsonrpc4j.v1_3; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.semconv.incubating.RpcIncubatingAttributes.RPC_JSONRPC_VERSION; +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.fasterxml.jackson.databind.ObjectMapper; +import com.googlecode.jsonrpc4j.JsonRpcBasicServer; +import com.googlecode.jsonrpc4j.JsonRpcHttpClient; +import com.googlecode.jsonrpc4j.ProxyUtil; +import com.googlecode.jsonrpc4j.spring.rest.JsonRpcRestClient; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.jsonrpc4j.v1_3.AbstractJsonRpcTest; +import io.opentelemetry.instrumentation.jsonrpc4j.v1_3.CalculatorService; +import io.opentelemetry.instrumentation.jsonrpc4j.v1_3.CalculatorServiceImpl; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class AgentJsonRpcTest extends AbstractJsonRpcTest { + + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Override + protected InstrumentationExtension testing() { + return testing; + } + + @Override + protected JsonRpcBasicServer configureServer(JsonRpcBasicServer server) { + return server; + } + + @Test + void testClient() throws Throwable { + CalculatorService clientProxy = + ProxyUtil.createClientProxy( + this.getClass().getClassLoader(), CalculatorService.class, getHttpClient()); + int res = + testing() + .runWithSpan( + "parent", + () -> { + return clientProxy.add(1, 2); + }); + + assertThat(res).isEqualTo(3); + + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName( + "io.opentelemetry.instrumentation.jsonrpc4j.v1_3.CalculatorService/add") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(RPC_SYSTEM, "jsonrpc"), + equalTo(RPC_JSONRPC_VERSION, "2.0"), + equalTo( + RPC_SERVICE, + "io.opentelemetry.instrumentation.jsonrpc4j.v1_3.CalculatorService"), + equalTo(RPC_METHOD, "add"))), + trace -> trace.hasSpansSatisfyingExactly(span -> span.hasKind(SpanKind.SERVER))); + + testing() + .waitAndAssertMetrics( + "io.opentelemetry.jsonrpc4j-1.3", + "rpc.client.duration", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasUnit("ms") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point.hasAttributesSatisfying( + equalTo(RPC_METHOD, "add"), + equalTo( + RPC_SERVICE, + "io.opentelemetry.instrumentation.jsonrpc4j.v1_3.CalculatorService"), + equalTo(RPC_SYSTEM, "jsonrpc")))))); + } + + private JettyServer jettyServer; + + @BeforeAll + public void setup() throws Exception { + this.jettyServer = createServer(); + } + + private static JettyServer createServer() throws Exception { + JettyServer jettyServer = new JettyServer(CalculatorServiceImpl.class); + jettyServer.startup(); + return jettyServer; + } + + protected JsonRpcRestClient getClient() throws MalformedURLException { + return getClient(JettyServer.SERVLET); + } + + protected JsonRpcRestClient getClient(String servlet) throws MalformedURLException { + return new JsonRpcRestClient(new URL(jettyServer.getCustomServerUrlString(servlet))); + } + + protected JsonRpcHttpClient getHttpClient() throws MalformedURLException { + Map header = new HashMap<>(); + return new JsonRpcHttpClient( + new ObjectMapper(), + new URL(jettyServer.getCustomServerUrlString(JettyServer.SERVLET)), + header); + } + + protected JsonRpcHttpClient getHttpClient(String servlet) throws MalformedURLException { + Map header = new HashMap<>(); + return new JsonRpcHttpClient( + new ObjectMapper(), new URL(jettyServer.getCustomServerUrlString(servlet)), header); + } + + @AfterAll + public void teardown() throws Exception { + jettyServer.stop(); + } +} diff --git a/instrumentation/jsonrpc4j-1.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jsonrpc4j/v1_3/JettyServer.java b/instrumentation/jsonrpc4j-1.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jsonrpc4j/v1_3/JettyServer.java new file mode 100644 index 000000000000..2a8a474da755 --- /dev/null +++ b/instrumentation/jsonrpc4j-1.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jsonrpc4j/v1_3/JettyServer.java @@ -0,0 +1,89 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jsonrpc4j.v1_3; + +import com.googlecode.jsonrpc4j.AnnotationsErrorResolver; +import com.googlecode.jsonrpc4j.JsonRpcServer; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.util.Random; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; + +@SuppressWarnings("WeakerAccess") +public class JettyServer implements AutoCloseable { + + public static final String DEFAULT_LOCAL_HOSTNAME = "127.0.0.1"; + + public static final String SERVLET = "someSunnyServlet"; + private static final String PROTOCOL = "http"; + + private final Class service; + + private Server jetty; + private int port; + + JettyServer(Class service) { + this.service = service; + } + + public String getCustomServerUrlString(String servletName) { + return PROTOCOL + "://" + DEFAULT_LOCAL_HOSTNAME + ":" + port + "/" + servletName; + } + + public void startup() throws Exception { + port = 10000 + new Random().nextInt(30000); + jetty = new Server(port); + ServletContextHandler context = new ServletContextHandler(ServletContextHandler.NO_SESSIONS); + context.setContextPath("/"); + jetty.setHandler(context); + ServletHolder servlet = context.addServlet(JsonRpcTestServlet.class, "/" + SERVLET); + servlet.setInitParameter("class", service.getCanonicalName()); + jetty.start(); + } + + @Override + public void close() throws Exception { + this.stop(); + } + + public void stop() throws Exception { + jetty.stop(); + } + + public static class JsonRpcTestServlet extends HttpServlet { + + static final long serialVersionUID = 1L; + private transient JsonRpcServer jsonRpcServer; + + @Override + public void init() throws ServletException { + try { + Class svcClass = Class.forName(getInitParameter("class")); + Object instance = svcClass.getConstructor().newInstance(); + jsonRpcServer = new JsonRpcServer(instance); + jsonRpcServer.setErrorResolver(AnnotationsErrorResolver.INSTANCE); + } catch (ClassNotFoundException + | NoSuchMethodException + | InstantiationException + | InvocationTargetException + | IllegalAccessException e) { + throw new ServletException(e); + } + } + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + jsonRpcServer.handle(request, response); + } + } +} diff --git a/instrumentation/jsonrpc4j-1.3/library/README.md b/instrumentation/jsonrpc4j-1.3/library/README.md new file mode 100644 index 000000000000..17174b9bf00c --- /dev/null +++ b/instrumentation/jsonrpc4j-1.3/library/README.md @@ -0,0 +1,39 @@ +# Library Instrumentation for jsonrpc4j 1.3.3+ + +Provides OpenTelemetry instrumentation for [jsonrpc4j](https://github.com/briandilley/jsonrpc4j) server. + +## Quickstart + +### Add the following dependencies to your project + +Replace `OPENTELEMETRY_VERSION` with the [latest release](https://search.maven.org/search?q=g:io.opentelemetry.instrumentation%20AND%20a:opentelemetry-jsonrpc4j-1.3). + +For Maven, add the following to your `pom.xml` dependencies: + +```xml + + + io.opentelemetry.instrumentation + opentelemetry-jsonrpc4j-1.3 + OPENTELEMETRY_VERSION + + +``` + +For Gradle, add the following to your dependencies: + +```groovy +implementation("io.opentelemetry.instrumentation:opentelemetry-jsonrpc4j-1.3:OPENTELEMETRY_VERSION") +``` + +### Usage + +The instrumentation library provides the implementation of `InvocationListener` to provide OpenTelemetry-based spans and context propagation. + +```java +// For server-side, attatch the invocation listener to your service. +JsonRpcBasicServer configureServer(OpenTelemetry openTelemetry, JsonRpcBasicServer server) { + JsonRpcTelemetry jsonrpcTelemetry = JsonRpcTelemetry.create(openTelemetry); + return server.setInvocationListener(jsonrpcTelemetry.newServerInvocationListener()); +} +``` diff --git a/instrumentation/jsonrpc4j-1.3/library/build.gradle.kts b/instrumentation/jsonrpc4j-1.3/library/build.gradle.kts new file mode 100644 index 000000000000..345f9d27f44f --- /dev/null +++ b/instrumentation/jsonrpc4j-1.3/library/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + id("otel.library-instrumentation") +} + +val jacksonVersion = "2.13.3" + +dependencies { + library("com.github.briandilley.jsonrpc4j:jsonrpc4j:1.3.3") + + library("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion") + + testImplementation(project(":instrumentation:jsonrpc4j-1.3:testing")) +} diff --git a/instrumentation/jsonrpc4j-1.3/library/src/main/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/JsonRpcClientAttributesExtractor.java b/instrumentation/jsonrpc4j-1.3/library/src/main/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/JsonRpcClientAttributesExtractor.java new file mode 100644 index 000000000000..17cf2914e6dc --- /dev/null +++ b/instrumentation/jsonrpc4j-1.3/library/src/main/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/JsonRpcClientAttributesExtractor.java @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jsonrpc4j.v1_3; + +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import javax.annotation.Nullable; + +// Check https://opentelemetry.io/docs/specs/semconv/rpc/json-rpc/ +final class JsonRpcClientAttributesExtractor + implements AttributesExtractor { + + @Override + public void onStart( + AttributesBuilder attributes, Context parentContext, JsonRpcClientRequest jsonRpcRequest) { + attributes.put("rpc.jsonrpc.version", "2.0"); + } + + @Override + public void onEnd( + AttributesBuilder attributes, + Context context, + JsonRpcClientRequest jsonRpcRequest, + @Nullable JsonRpcClientResponse jsonRpcResponse, + @Nullable Throwable error) {} +} diff --git a/instrumentation/jsonrpc4j-1.3/library/src/main/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/JsonRpcClientAttributesGetter.java b/instrumentation/jsonrpc4j-1.3/library/src/main/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/JsonRpcClientAttributesGetter.java new file mode 100644 index 000000000000..49a9735fc759 --- /dev/null +++ b/instrumentation/jsonrpc4j-1.3/library/src/main/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/JsonRpcClientAttributesGetter.java @@ -0,0 +1,33 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jsonrpc4j.v1_3; + +import io.opentelemetry.instrumentation.api.incubator.semconv.rpc.RpcAttributesGetter; + +// Check +// https://github.com/open-telemetry/semantic-conventions/blob/main/docs/rpc/rpc-metrics.md#attributes +// Check https://opentelemetry.io/docs/specs/semconv/rpc/json-rpc/ +public enum JsonRpcClientAttributesGetter implements RpcAttributesGetter { + INSTANCE; + + @Override + public String getSystem(JsonRpcClientRequest request) { + return "jsonrpc"; + } + + @Override + public String getService(JsonRpcClientRequest request) { + if (request.getMethod() != null) { + return request.getMethod().getDeclaringClass().getName(); + } + return "NOT_AVAILABLE"; + } + + @Override + public String getMethod(JsonRpcClientRequest request) { + return request.getMethodName(); + } +} diff --git a/instrumentation/jsonrpc4j-1.3/library/src/main/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/JsonRpcClientRequest.java b/instrumentation/jsonrpc4j-1.3/library/src/main/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/JsonRpcClientRequest.java new file mode 100644 index 000000000000..a7882ff6abc6 --- /dev/null +++ b/instrumentation/jsonrpc4j-1.3/library/src/main/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/JsonRpcClientRequest.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jsonrpc4j.v1_3; + +import java.lang.reflect.Method; + +public final class JsonRpcClientRequest { + + private final String methodName; + private final Object argument; + private Method method; + + public JsonRpcClientRequest(String methodName, Object argument) { + this.methodName = methodName; + this.argument = argument; + } + + public JsonRpcClientRequest(Method method, Object argument) { + this.method = method; + this.methodName = method.getName(); + this.argument = argument; + } + + public String getMethodName() { + return methodName; + } + + public Object getArgument() { + return argument; + } + + public Method getMethod() { + return method; + } +} diff --git a/instrumentation/jsonrpc4j-1.3/library/src/main/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/JsonRpcClientResponse.java b/instrumentation/jsonrpc4j-1.3/library/src/main/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/JsonRpcClientResponse.java new file mode 100644 index 000000000000..ac2108909b8f --- /dev/null +++ b/instrumentation/jsonrpc4j-1.3/library/src/main/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/JsonRpcClientResponse.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jsonrpc4j.v1_3; + +public final class JsonRpcClientResponse { + + private final Object result; + + public JsonRpcClientResponse(Object result) { + this.result = result; + } + + public Object getResult() { + return result; + } +} diff --git a/instrumentation/jsonrpc4j-1.3/library/src/main/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/JsonRpcClientSpanNameExtractor.java b/instrumentation/jsonrpc4j-1.3/library/src/main/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/JsonRpcClientSpanNameExtractor.java new file mode 100644 index 000000000000..d0970ad4ca7a --- /dev/null +++ b/instrumentation/jsonrpc4j-1.3/library/src/main/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/JsonRpcClientSpanNameExtractor.java @@ -0,0 +1,20 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jsonrpc4j.v1_3; + +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import java.lang.reflect.Method; + +public class JsonRpcClientSpanNameExtractor implements SpanNameExtractor { + @Override + public String extract(JsonRpcClientRequest request) { + if (request.getMethod() == null) { + return request.getMethodName(); + } + Method method = request.getMethod(); + return String.format("%s/%s", method.getDeclaringClass().getName(), method.getName()); + } +} diff --git a/instrumentation/jsonrpc4j-1.3/library/src/main/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/JsonRpcServerAttributesExtractor.java b/instrumentation/jsonrpc4j-1.3/library/src/main/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/JsonRpcServerAttributesExtractor.java new file mode 100644 index 000000000000..adeb8c0e6f3f --- /dev/null +++ b/instrumentation/jsonrpc4j-1.3/library/src/main/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/JsonRpcServerAttributesExtractor.java @@ -0,0 +1,57 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jsonrpc4j.v1_3; + +import com.googlecode.jsonrpc4j.AnnotationsErrorResolver; +import com.googlecode.jsonrpc4j.DefaultErrorResolver; +import com.googlecode.jsonrpc4j.ErrorResolver; +import com.googlecode.jsonrpc4j.MultipleErrorResolver; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import javax.annotation.Nullable; + +// Check https://opentelemetry.io/docs/specs/semconv/rpc/json-rpc/ +final class JsonRpcServerAttributesExtractor + implements AttributesExtractor { + + private static final AttributeKey RPC_JSONRPC_ERROR_CODE = + AttributeKey.longKey("rpc.jsonrpc.error_code"); + + private static final AttributeKey RPC_JSONRPC_ERROR_MESSAGE = + AttributeKey.stringKey("rpc.jsonrpc.error_message"); + + @Override + public void onStart( + AttributesBuilder attributes, + Context parentContext, + JsonRpcServerRequest jsonRpcServerRequest) { + attributes.put("rpc.jsonrpc.version", "2.0"); + } + + @Override + public void onEnd( + AttributesBuilder attributes, + Context context, + JsonRpcServerRequest jsonRpcServerRequest, + @Nullable JsonRpcServerResponse jsonRpcServerResponse, + @Nullable Throwable error) { + // use the DEFAULT_ERROR_RESOLVER to extract error code and message + if (error != null) { + ErrorResolver errorResolver = + new MultipleErrorResolver( + AnnotationsErrorResolver.INSTANCE, DefaultErrorResolver.INSTANCE); + ErrorResolver.JsonError jsonError = + errorResolver.resolveError( + error, jsonRpcServerRequest.getMethod(), jsonRpcServerRequest.getArguments()); + attributes.put(RPC_JSONRPC_ERROR_CODE, jsonError.code); + attributes.put(RPC_JSONRPC_ERROR_MESSAGE, jsonError.message); + } else { + attributes.put(RPC_JSONRPC_ERROR_CODE, ErrorResolver.JsonError.OK.code); + } + } +} diff --git a/instrumentation/jsonrpc4j-1.3/library/src/main/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/JsonRpcServerAttributesGetter.java b/instrumentation/jsonrpc4j-1.3/library/src/main/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/JsonRpcServerAttributesGetter.java new file mode 100644 index 000000000000..9e61b42c073a --- /dev/null +++ b/instrumentation/jsonrpc4j-1.3/library/src/main/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/JsonRpcServerAttributesGetter.java @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jsonrpc4j.v1_3; + +import io.opentelemetry.instrumentation.api.incubator.semconv.rpc.RpcAttributesGetter; + +// Check +// https://github.com/open-telemetry/semantic-conventions/blob/main/docs/rpc/rpc-metrics.md#attributes +// Check https://opentelemetry.io/docs/specs/semconv/rpc/json-rpc/ +public enum JsonRpcServerAttributesGetter implements RpcAttributesGetter { + INSTANCE; + + @Override + public String getSystem(JsonRpcServerRequest request) { + return "jsonrpc"; + } + + @Override + public String getService(JsonRpcServerRequest request) { + return request.getMethod().getDeclaringClass().getName(); + } + + @Override + public String getMethod(JsonRpcServerRequest request) { + return request.getMethod().getName(); + } +} diff --git a/instrumentation/jsonrpc4j-1.3/library/src/main/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/JsonRpcServerRequest.java b/instrumentation/jsonrpc4j-1.3/library/src/main/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/JsonRpcServerRequest.java new file mode 100644 index 000000000000..65c96abea75d --- /dev/null +++ b/instrumentation/jsonrpc4j-1.3/library/src/main/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/JsonRpcServerRequest.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jsonrpc4j.v1_3; + +import com.fasterxml.jackson.databind.JsonNode; +import java.lang.reflect.Method; +import java.util.List; + +public final class JsonRpcServerRequest { + + private final Method method; + private final List arguments; + + JsonRpcServerRequest(Method method, List arguments) { + this.method = method; + this.arguments = arguments; + } + + public Method getMethod() { + return method; + } + + public List getArguments() { + return arguments; + } +} diff --git a/instrumentation/jsonrpc4j-1.3/library/src/main/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/JsonRpcServerRequestGetter.java b/instrumentation/jsonrpc4j-1.3/library/src/main/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/JsonRpcServerRequestGetter.java new file mode 100644 index 000000000000..381b19f5851d --- /dev/null +++ b/instrumentation/jsonrpc4j-1.3/library/src/main/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/JsonRpcServerRequestGetter.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jsonrpc4j.v1_3; + +import io.opentelemetry.context.propagation.TextMapGetter; +import java.util.ArrayList; +import javax.annotation.Nullable; + +enum JsonRpcServerRequestGetter implements TextMapGetter { + INSTANCE; + + @Override + public Iterable keys(JsonRpcServerRequest request) { + return new ArrayList<>(); + } + + @Override + @Nullable + public String get(@Nullable JsonRpcServerRequest request, String key) { + return null; + } +} diff --git a/instrumentation/jsonrpc4j-1.3/library/src/main/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/JsonRpcServerResponse.java b/instrumentation/jsonrpc4j-1.3/library/src/main/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/JsonRpcServerResponse.java new file mode 100644 index 000000000000..1eb99a28afc2 --- /dev/null +++ b/instrumentation/jsonrpc4j-1.3/library/src/main/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/JsonRpcServerResponse.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jsonrpc4j.v1_3; + +import com.fasterxml.jackson.databind.JsonNode; +import java.lang.reflect.Method; +import java.util.List; + +public final class JsonRpcServerResponse { + private final Method method; + private final List params; + private final Object result; + + JsonRpcServerResponse(Method method, List params, Object result) { + this.method = method; + this.params = params; + this.result = result; + } + + public Method getMethod() { + return method; + } + + public List getParams() { + return params; + } + + public Object getResult() { + return result; + } +} diff --git a/instrumentation/jsonrpc4j-1.3/library/src/main/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/JsonRpcServerSpanNameExtractor.java b/instrumentation/jsonrpc4j-1.3/library/src/main/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/JsonRpcServerSpanNameExtractor.java new file mode 100644 index 000000000000..6c95126372a6 --- /dev/null +++ b/instrumentation/jsonrpc4j-1.3/library/src/main/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/JsonRpcServerSpanNameExtractor.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jsonrpc4j.v1_3; + +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import java.lang.reflect.Method; + +public class JsonRpcServerSpanNameExtractor implements SpanNameExtractor { + // Follow https://opentelemetry.io/docs/specs/semconv/rpc/rpc-spans/#span-name + @Override + public String extract(JsonRpcServerRequest request) { + Method method = request.getMethod(); + return String.format("%s/%s", method.getDeclaringClass().getName(), method.getName()); + } +} diff --git a/instrumentation/jsonrpc4j-1.3/library/src/main/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/JsonRpcServerSpanStatusExtractor.java b/instrumentation/jsonrpc4j-1.3/library/src/main/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/JsonRpcServerSpanStatusExtractor.java new file mode 100644 index 000000000000..3cdfaec03ce7 --- /dev/null +++ b/instrumentation/jsonrpc4j-1.3/library/src/main/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/JsonRpcServerSpanStatusExtractor.java @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jsonrpc4j.v1_3; + +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.databind.JsonMappingException; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusBuilder; +import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusExtractor; +import javax.annotation.Nullable; + +public enum JsonRpcServerSpanStatusExtractor + implements SpanStatusExtractor { + INSTANCE; + + /** Extracts the status from the response and sets it to the {@code spanStatusBuilder}. */ + @Override + public void extract( + SpanStatusBuilder spanStatusBuilder, + JsonRpcServerRequest jsonRpcServerRequest, + @Nullable JsonRpcServerResponse jsonRpcServerResponse, + @Nullable Throwable error) { + if (error == null) { + spanStatusBuilder.setStatus(StatusCode.OK); + } + + // treat client invalid input as OK + if (error instanceof JsonParseException || error instanceof JsonMappingException) { + spanStatusBuilder.setStatus(StatusCode.OK); + } + + spanStatusBuilder.setStatus(StatusCode.ERROR); + } +} diff --git a/instrumentation/jsonrpc4j-1.3/library/src/main/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/JsonRpcTelemetry.java b/instrumentation/jsonrpc4j-1.3/library/src/main/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/JsonRpcTelemetry.java new file mode 100644 index 000000000000..f81265157438 --- /dev/null +++ b/instrumentation/jsonrpc4j-1.3/library/src/main/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/JsonRpcTelemetry.java @@ -0,0 +1,46 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jsonrpc4j.v1_3; + +import com.googlecode.jsonrpc4j.InvocationListener; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; + +public final class JsonRpcTelemetry { + public static JsonRpcTelemetry create(OpenTelemetry openTelemetry) { + return builder(openTelemetry).build(); + } + + public static JsonRpcTelemetryBuilder builder(OpenTelemetry openTelemetry) { + return new JsonRpcTelemetryBuilder(openTelemetry); + } + + private final Instrumenter serverInstrumenter; + private final Instrumenter clientInstrumenter; + private final ContextPropagators propagators; + + JsonRpcTelemetry( + Instrumenter serverInstrumenter, + Instrumenter clientInstrumenter, + ContextPropagators propagators) { + this.serverInstrumenter = serverInstrumenter; + this.clientInstrumenter = clientInstrumenter; + this.propagators = propagators; + } + + public InvocationListener newServerInvocationListener() { + return new OpenTelemetryJsonRpcInvocationListener(serverInstrumenter); + } + + public Instrumenter getClientInstrumenter() { + return clientInstrumenter; + } + + public ContextPropagators getPropagators() { + return propagators; + } +} diff --git a/instrumentation/jsonrpc4j-1.3/library/src/main/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/JsonRpcTelemetryBuilder.java b/instrumentation/jsonrpc4j-1.3/library/src/main/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/JsonRpcTelemetryBuilder.java new file mode 100644 index 000000000000..aeb6da585f90 --- /dev/null +++ b/instrumentation/jsonrpc4j-1.3/library/src/main/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/JsonRpcTelemetryBuilder.java @@ -0,0 +1,100 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jsonrpc4j.v1_3; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.rpc.RpcClientAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.rpc.RpcClientMetrics; +import io.opentelemetry.instrumentation.api.incubator.semconv.rpc.RpcServerAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.rpc.RpcServerMetrics; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; +import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import java.util.ArrayList; +import java.util.List; + +public class JsonRpcTelemetryBuilder { + + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.jsonrpc4j-1.3"; + + private final OpenTelemetry openTelemetry; + + private final List< + AttributesExtractor> + additionalClientExtractors = new ArrayList<>(); + private final List< + AttributesExtractor> + additionalServerExtractors = new ArrayList<>(); + + JsonRpcTelemetryBuilder(OpenTelemetry openTelemetry) { + this.openTelemetry = openTelemetry; + } + + /** + * Adds an extra client-only {@link AttributesExtractor} to invoke to set attributes to + * instrumented items. The {@link AttributesExtractor} will be executed after all default + * extractors. + */ + @CanIgnoreReturnValue + public JsonRpcTelemetryBuilder addClientAttributeExtractor( + AttributesExtractor + attributesExtractor) { + additionalClientExtractors.add(attributesExtractor); + return this; + } + + /** + * Adds an extra server-only {@link AttributesExtractor} to invoke to set attributes to + * instrumented items. The {@link AttributesExtractor} will be executed after all default + * extractors. + */ + @CanIgnoreReturnValue + public JsonRpcTelemetryBuilder addServerAttributeExtractor( + AttributesExtractor + attributesExtractor) { + additionalServerExtractors.add(attributesExtractor); + return this; + } + + public JsonRpcTelemetry build() { + SpanNameExtractor clientSpanNameExtractor = + new JsonRpcClientSpanNameExtractor(); + SpanNameExtractor serverSpanNameExtractor = + new JsonRpcServerSpanNameExtractor(); + + InstrumenterBuilder clientInstrumenterBuilder = + Instrumenter.builder(openTelemetry, INSTRUMENTATION_NAME, clientSpanNameExtractor); + + InstrumenterBuilder serverInstrumenterBuilder = + Instrumenter.builder(openTelemetry, INSTRUMENTATION_NAME, serverSpanNameExtractor); + + JsonRpcServerAttributesGetter serverRpcAttributesGetter = + JsonRpcServerAttributesGetter.INSTANCE; + JsonRpcClientAttributesGetter clientRpcAttributesGetter = + JsonRpcClientAttributesGetter.INSTANCE; + + clientInstrumenterBuilder + .addAttributesExtractor(RpcClientAttributesExtractor.create(clientRpcAttributesGetter)) + .addAttributesExtractors(additionalClientExtractors) + .addAttributesExtractor(new JsonRpcClientAttributesExtractor()) + .addOperationMetrics(RpcClientMetrics.get()); + + serverInstrumenterBuilder + .setSpanStatusExtractor(JsonRpcServerSpanStatusExtractor.INSTANCE) + .addAttributesExtractor(RpcServerAttributesExtractor.create(serverRpcAttributesGetter)) + .addAttributesExtractor(new JsonRpcServerAttributesExtractor()) + .addAttributesExtractors(additionalServerExtractors) + .addOperationMetrics(RpcServerMetrics.get()); + + return new JsonRpcTelemetry( + serverInstrumenterBuilder.buildServerInstrumenter(JsonRpcServerRequestGetter.INSTANCE), + clientInstrumenterBuilder.buildInstrumenter(SpanKindExtractor.alwaysClient()), + openTelemetry.getPropagators()); + } +} diff --git a/instrumentation/jsonrpc4j-1.3/library/src/main/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/OpenTelemetryJsonRpcInvocationListener.java b/instrumentation/jsonrpc4j-1.3/library/src/main/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/OpenTelemetryJsonRpcInvocationListener.java new file mode 100644 index 000000000000..70490fad5a01 --- /dev/null +++ b/instrumentation/jsonrpc4j-1.3/library/src/main/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/OpenTelemetryJsonRpcInvocationListener.java @@ -0,0 +1,69 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jsonrpc4j.v1_3; + +import com.fasterxml.jackson.databind.JsonNode; +import com.googlecode.jsonrpc4j.InvocationListener; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import java.lang.reflect.Method; +import java.util.List; + +public final class OpenTelemetryJsonRpcInvocationListener implements InvocationListener { + + private final Instrumenter serverInstrumenter; + + private static final ThreadLocal threadLocalContext = new ThreadLocal<>(); + private static final ThreadLocal threadLocalScope = new ThreadLocal<>(); + + public OpenTelemetryJsonRpcInvocationListener( + Instrumenter serverInstrumenter) { + this.serverInstrumenter = serverInstrumenter; + } + + /** + * This method will be invoked prior to a JSON-RPC service being invoked. + * + * @param method is the method that will be invoked. + * @param arguments are the arguments that will be passed to the method when it is invoked. + */ + @Override + public void willInvoke(Method method, List arguments) { + Context parentContext = Context.current(); + JsonRpcServerRequest request = new JsonRpcServerRequest(method, arguments); + if (!serverInstrumenter.shouldStart(parentContext, request)) { + return; + } + + Context context = serverInstrumenter.start(parentContext, request); + threadLocalContext.set(context); + threadLocalScope.set(context.makeCurrent()); + } + + /** + * This method will be invoked after a JSON-RPC service has been invoked. + * + * @param method is the method that will was invoked. + * @param arguments are the arguments that were be passed to the method when it is invoked. + * @param result is the result of the method invocation. If an error arose, this value will be + * null. + * @param t is the throwable that was thrown from the invocation, if no error arose, this value + * will be null. + * @param duration is approximately the number of milliseconds that elapsed during which the + * method was invoked. + */ + @Override + public void didInvoke( + Method method, List arguments, Object result, Throwable t, long duration) { + JsonRpcServerRequest request = new JsonRpcServerRequest(method, arguments); + JsonRpcServerResponse response = new JsonRpcServerResponse(method, arguments, result); + threadLocalScope.get().close(); + serverInstrumenter.end(threadLocalContext.get(), request, response, t); + threadLocalContext.remove(); + threadLocalScope.remove(); + } +} diff --git a/instrumentation/jsonrpc4j-1.3/library/src/test/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/LibraryJsonRpcTest.java b/instrumentation/jsonrpc4j-1.3/library/src/test/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/LibraryJsonRpcTest.java new file mode 100644 index 000000000000..63e017601813 --- /dev/null +++ b/instrumentation/jsonrpc4j-1.3/library/src/test/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/LibraryJsonRpcTest.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jsonrpc4j.v1_3; + +import com.googlecode.jsonrpc4j.JsonRpcBasicServer; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +public class LibraryJsonRpcTest extends AbstractJsonRpcTest { + + @RegisterExtension + static InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + @Override + protected InstrumentationExtension testing() { + return testing; + } + + @Override + protected JsonRpcBasicServer configureServer(JsonRpcBasicServer server) { + server.setInvocationListener( + JsonRpcTelemetry.builder(testing.getOpenTelemetry()).build().newServerInvocationListener()); + return server; + } +} diff --git a/instrumentation/jsonrpc4j-1.3/testing/build.gradle.kts b/instrumentation/jsonrpc4j-1.3/testing/build.gradle.kts new file mode 100644 index 000000000000..35775d794a20 --- /dev/null +++ b/instrumentation/jsonrpc4j-1.3/testing/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + id("otel.java-conventions") +} + +val jsonrpcVersion = "1.3.3" + +dependencies { + api(project(":testing-common")) + + implementation("com.github.briandilley.jsonrpc4j:jsonrpc4j:$jsonrpcVersion") + + implementation("com.fasterxml.jackson.core:jackson-databind:2.13.3") +} diff --git a/instrumentation/jsonrpc4j-1.3/testing/src/main/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/AbstractJsonRpcTest.java b/instrumentation/jsonrpc4j-1.3/testing/src/main/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/AbstractJsonRpcTest.java new file mode 100644 index 000000000000..51c79dbd9071 --- /dev/null +++ b/instrumentation/jsonrpc4j-1.3/testing/src/main/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/AbstractJsonRpcTest.java @@ -0,0 +1,101 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jsonrpc4j.v1_3; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.semconv.incubating.RpcIncubatingAttributes.RPC_JSONRPC_ERROR_CODE; +import static io.opentelemetry.semconv.incubating.RpcIncubatingAttributes.RPC_JSONRPC_VERSION; +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 java.nio.charset.StandardCharsets.UTF_8; + +import com.googlecode.jsonrpc4j.JsonRpcBasicServer; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.testing.internal.jackson.databind.JsonNode; +import io.opentelemetry.testing.internal.jackson.databind.ObjectMapper; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +@SuppressWarnings("deprecation") // using deprecated semconv +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public abstract class AbstractJsonRpcTest { + + protected abstract InstrumentationExtension testing(); + + protected abstract JsonRpcBasicServer configureServer(JsonRpcBasicServer server); + + @Test + void testServer() throws IOException { + CalculatorService calculator = new CalculatorServiceImpl(); + JsonRpcBasicServer server = + configureServer(new JsonRpcBasicServer(calculator, CalculatorService.class)); + + JsonNode response = + testing() + .runWithSpan( + "parent", + () -> { + InputStream inputStream = + new ByteArrayInputStream( + "{\"jsonrpc\":\"2.0\",\"method\":\"add\",\"params\":[1,2],\"id\":1}" + .getBytes(UTF_8)); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + server.handleRequest(inputStream, outputStream); + + // Read the JsonNode from the InputStream + ObjectMapper objectMapper = new ObjectMapper(); + return objectMapper.readTree( + new ByteArrayInputStream(outputStream.toByteArray())); + }); + + assertThat(response.get("result").asInt()).isEqualTo(3); + + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName( + "io.opentelemetry.instrumentation.jsonrpc4j.v1_3.CalculatorService/add") + .hasKind(SpanKind.SERVER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(RPC_SYSTEM, "jsonrpc"), + equalTo(RPC_JSONRPC_VERSION, "2.0"), + equalTo( + RPC_SERVICE, + "io.opentelemetry.instrumentation.jsonrpc4j.v1_3.CalculatorService"), + equalTo(RPC_METHOD, "add"), + equalTo(RPC_JSONRPC_ERROR_CODE, 0L)))); + testing() + .waitAndAssertMetrics( + "io.opentelemetry.jsonrpc4j-1.3", + "rpc.server.duration", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasUnit("ms") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point.hasAttributesSatisfying( + equalTo(RPC_METHOD, "add"), + equalTo( + RPC_SERVICE, + "io.opentelemetry.instrumentation.jsonrpc4j.v1_3.CalculatorService"), + equalTo(RPC_SYSTEM, "jsonrpc")))))); + } +} diff --git a/instrumentation/jsonrpc4j-1.3/testing/src/main/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/CalculatorService.java b/instrumentation/jsonrpc4j-1.3/testing/src/main/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/CalculatorService.java new file mode 100644 index 000000000000..8821b7d2b858 --- /dev/null +++ b/instrumentation/jsonrpc4j-1.3/testing/src/main/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/CalculatorService.java @@ -0,0 +1,15 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jsonrpc4j.v1_3; + +import com.googlecode.jsonrpc4j.JsonRpcService; + +@JsonRpcService("/calculator") +public interface CalculatorService { + int add(int a, int b) throws Throwable; + + int subtract(int a, int b) throws Throwable; +} diff --git a/instrumentation/jsonrpc4j-1.3/testing/src/main/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/CalculatorServiceImpl.java b/instrumentation/jsonrpc4j-1.3/testing/src/main/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/CalculatorServiceImpl.java new file mode 100644 index 000000000000..c01b9c99996c --- /dev/null +++ b/instrumentation/jsonrpc4j-1.3/testing/src/main/java/io/opentelemetry/instrumentation/jsonrpc4j/v1_3/CalculatorServiceImpl.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jsonrpc4j.v1_3; + +public class CalculatorServiceImpl implements CalculatorService { + @Override + public int add(int a, int b) { + return a + b; + } + + @Override + public int subtract(int a, int b) { + return a - b; + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 0e4cda313a8d..5a7294e49e90 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -345,6 +345,9 @@ include(":instrumentation:jsf:jsf-mojarra-3.0:javaagent") include(":instrumentation:jsf:jsf-myfaces-1.2:javaagent") include(":instrumentation:jsf:jsf-myfaces-3.0:javaagent") include(":instrumentation:jsp-2.3:javaagent") +include(":instrumentation:jsonrpc4j-1.3:javaagent") +include(":instrumentation:jsonrpc4j-1.3:library") +include(":instrumentation:jsonrpc4j-1.3:testing") include(":instrumentation:kafka:kafka-clients:kafka-clients-0.11:bootstrap") include(":instrumentation:kafka:kafka-clients:kafka-clients-0.11:javaagent") include(":instrumentation:kafka:kafka-clients:kafka-clients-0.11:testing")