Skip to content

Commit

Permalink
Add instrumentation for armeria gRPC (#11351)
Browse files Browse the repository at this point in the history
Co-authored-by: Steve Rao <raozihao.rzh@alibaba-inc.com>
  • Loading branch information
laurit and steverao committed May 15, 2024
1 parent 3a9972b commit 66e4c5a
Show file tree
Hide file tree
Showing 8 changed files with 274 additions and 1 deletion.
1 change: 1 addition & 0 deletions docs/supported-libraries.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ These are the supported libraries and frameworks:
| [Apache Tapestry](https://tapestry.apache.org/) | 5.4+ | N/A | Provides `http.route` [2], Controller Spans [3] |
| [Apache Wicket](https://wicket.apache.org/) | 8.0+ | N/A | Provides `http.route` [2] |
| [Armeria](https://armeria.dev) | 1.3+ | [opentelemetry-armeria-1.3](../instrumentation/armeria-1.3/library) | [HTTP Client Spans], [HTTP Client Metrics], [HTTP Server Spans], [HTTP Server Metrics] |
| [Armeria gRPC](https://armeria.dev) | 1.14+ | | [RPC Client Spans], [RPC Client Metrics], [RPC Server Spans], [RPC Server Metrics] |
| [AsyncHttpClient](https://github.com/AsyncHttpClient/async-http-client) | 1.9+ | N/A | [HTTP Client Spans], [HTTP Client Metrics] |
| [AWS Lambda](https://docs.aws.amazon.com/lambda/latest/dg/java-handler.html) | 1.0+ | [opentelemetry-aws-lambda-core-1.0](../instrumentation/aws-lambda/aws-lambda-core-1.0/library),<br>[opentelemetry-aws-lambda-events-2.2](../instrumentation/aws-lambda/aws-lambda-events-2.2/library) | [FaaS Server Spans] |
| [AWS SDK](https://aws.amazon.com/sdk-for-java/) | 1.11.x and 2.2+ | [opentelemetry-aws-sdk-1.11](../instrumentation/aws-sdk/aws-sdk-1.11/library),<br>[opentelemetry-aws-sdk-1.11-autoconfigure](../instrumentation/aws-sdk/aws-sdk-1.11/library-autoconfigure),<br>[opentelemetry-aws-sdk-2.2](../instrumentation/aws-sdk/aws-sdk-2.2/library),<br>[opentelemetry-aws-sdk-2.2-autoconfigure](../instrumentation/aws-sdk/aws-sdk-2.2/library-autoconfigure) | [Messaging Spans], [Database Client Spans], [HTTP Client Spans] |
Expand Down
23 changes: 23 additions & 0 deletions instrumentation/armeria-grpc-1.14/javaagent/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
plugins {
id("otel.javaagent-instrumentation")
}

muzzle {
pass {
group.set("com.linecorp.armeria")
module.set("armeria-grpc")
versions.set("[1.14.0,)")
assertInverse.set(true)
}
}

dependencies {
library("com.linecorp.armeria:armeria-grpc:1.14.0")
implementation(project(":instrumentation:grpc-1.6:library"))

testInstrumentation(project(":instrumentation:netty:netty-4.1:javaagent"))
testInstrumentation(project(":instrumentation:grpc-1.6:javaagent"))

testImplementation(project(":instrumentation:grpc-1.6:testing"))
testLibrary("com.linecorp.armeria:armeria-junit5:1.14.0")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.armeria.grpc.v1_14;

import static net.bytebuddy.matcher.ElementMatchers.isMethod;
import static net.bytebuddy.matcher.ElementMatchers.isPublic;
import static net.bytebuddy.matcher.ElementMatchers.named;

import com.linecorp.armeria.client.grpc.GrpcClientBuilder;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.instrumentation.grpc.v1_6.GrpcTelemetry;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;

public class ArmeriaGrpcClientBuilderInstrumentation implements TypeInstrumentation {

@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return named("com.linecorp.armeria.client.grpc.GrpcClientBuilder");
}

@Override
public void transform(TypeTransformer transformer) {
transformer.applyAdviceToMethod(
isMethod().and(isPublic()).and(named("build")),
ArmeriaGrpcClientBuilderInstrumentation.class.getName() + "$BuildAdvice");
}

@SuppressWarnings("unused")
public static class BuildAdvice {

@Advice.OnMethodEnter
public static void onEnter(@Advice.This GrpcClientBuilder builder) {
builder.intercept(GrpcTelemetry.create(GlobalOpenTelemetry.get()).newClientInterceptor());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.armeria.grpc.v1_14;

import static java.util.Arrays.asList;

import com.google.auto.service.AutoService;
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import java.util.List;

@AutoService(InstrumentationModule.class)
public class ArmeriaGrpcInstrumentationModule extends InstrumentationModule {
public ArmeriaGrpcInstrumentationModule() {
super("armeria", "armeria-1.14", "armeria-grpc-1.14");
}

@Override
public List<TypeInstrumentation> typeInstrumentations() {
return asList(
new ArmeriaGrpcClientBuilderInstrumentation(),
new ArmeriaGrpcServiceBuilderInstrumentation());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.armeria.grpc.v1_14;

import static net.bytebuddy.matcher.ElementMatchers.isMethod;
import static net.bytebuddy.matcher.ElementMatchers.isPublic;
import static net.bytebuddy.matcher.ElementMatchers.named;

import com.linecorp.armeria.server.grpc.GrpcServiceBuilder;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.instrumentation.grpc.v1_6.GrpcTelemetry;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;

public class ArmeriaGrpcServiceBuilderInstrumentation implements TypeInstrumentation {
@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return named("com.linecorp.armeria.server.grpc.GrpcServiceBuilder");
}

@Override
public void transform(TypeTransformer transformer) {
transformer.applyAdviceToMethod(
isMethod().and(isPublic()).and(named("build")),
ArmeriaGrpcServiceBuilderInstrumentation.class.getName() + "$BuildAdvice");
}

@SuppressWarnings("unused")
public static class BuildAdvice {

@Advice.OnMethodEnter
public static void onEnter(@Advice.This GrpcServiceBuilder builder) {
builder.intercept(GrpcTelemetry.create(GlobalOpenTelemetry.get()).newServerInterceptor());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.armeria.grpc.v1_14;

import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo;
import static org.assertj.core.api.Assertions.assertThat;

import com.linecorp.armeria.client.grpc.GrpcClients;
import com.linecorp.armeria.server.ServerBuilder;
import com.linecorp.armeria.server.grpc.GrpcService;
import com.linecorp.armeria.testing.junit5.server.ServerExtension;
import example.GreeterGrpc;
import example.Helloworld;
import io.grpc.Status;
import io.grpc.stub.StreamObserver;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension;
import io.opentelemetry.semconv.ServerAttributes;
import io.opentelemetry.semconv.incubating.MessageIncubatingAttributes;
import io.opentelemetry.semconv.incubating.RpcIncubatingAttributes;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

class ArmeriaGrpcTest {

@RegisterExtension
static final AgentInstrumentationExtension testing = AgentInstrumentationExtension.create();

@RegisterExtension
static final ServerExtension server =
new ServerExtension() {
@Override
protected void configure(ServerBuilder sb) throws Exception {
sb.service(
GrpcService.builder()
.addService(
new GreeterGrpc.GreeterImplBase() {
@Override
public void sayHello(
Helloworld.Request request,
StreamObserver<Helloworld.Response> responseObserver) {
responseObserver.onNext(
Helloworld.Response.newBuilder()
.setMessage("Hello " + request.getName())
.build());
responseObserver.onCompleted();
}
})
.build());
}
};

@Test
void grpcInstrumentation() {
GreeterGrpc.GreeterBlockingStub client =
GrpcClients.builder(server.httpUri()).build(GreeterGrpc.GreeterBlockingStub.class);

Helloworld.Response response =
testing.runWithSpan(
"parent",
() -> client.sayHello(Helloworld.Request.newBuilder().setName("test").build()));

assertThat(response.getMessage()).isEqualTo("Hello test");

testing.waitAndAssertTraces(
trace ->
trace.hasSpansSatisfyingExactly(
span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(),
span ->
span.hasName("example.Greeter/SayHello")
.hasKind(SpanKind.CLIENT)
.hasParent(trace.getSpan(0))
.hasAttributesSatisfyingExactly(
equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "grpc"),
equalTo(RpcIncubatingAttributes.RPC_SERVICE, "example.Greeter"),
equalTo(RpcIncubatingAttributes.RPC_METHOD, "SayHello"),
equalTo(
RpcIncubatingAttributes.RPC_GRPC_STATUS_CODE,
(long) Status.Code.OK.value()),
equalTo(ServerAttributes.SERVER_ADDRESS, "127.0.0.1"),
equalTo(ServerAttributes.SERVER_PORT, (long) server.httpPort()))
.hasEventsSatisfyingExactly(
event ->
event
.hasName("message")
.hasAttributesSatisfyingExactly(
equalTo(MessageIncubatingAttributes.MESSAGE_TYPE, "SENT"),
equalTo(MessageIncubatingAttributes.MESSAGE_ID, 1L)),
event ->
event
.hasName("message")
.hasAttributesSatisfyingExactly(
equalTo(
MessageIncubatingAttributes.MESSAGE_TYPE, "RECEIVED"),
equalTo(MessageIncubatingAttributes.MESSAGE_ID, 2L))),
span ->
span.hasName("example.Greeter/SayHello")
.hasKind(SpanKind.SERVER)
.hasParent(trace.getSpan(1))
.hasAttributesSatisfyingExactly(
equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "grpc"),
equalTo(RpcIncubatingAttributes.RPC_SERVICE, "example.Greeter"),
equalTo(RpcIncubatingAttributes.RPC_METHOD, "SayHello"),
equalTo(
RpcIncubatingAttributes.RPC_GRPC_STATUS_CODE,
(long) Status.Code.OK.value()),
equalTo(ServerAttributes.SERVER_ADDRESS, "127.0.0.1"),
equalTo(ServerAttributes.SERVER_PORT, server.httpPort()))
.hasEventsSatisfyingExactly(
event ->
event
.hasName("message")
.hasAttributesSatisfyingExactly(
equalTo(
MessageIncubatingAttributes.MESSAGE_TYPE, "RECEIVED"),
equalTo(MessageIncubatingAttributes.MESSAGE_ID, 1L)),
event ->
event
.hasName("message")
.hasAttributesSatisfyingExactly(
equalTo(MessageIncubatingAttributes.MESSAGE_TYPE, "SENT"),
equalTo(MessageIncubatingAttributes.MESSAGE_ID, 2L)))));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import io.grpc.ForwardingServerCall;
import io.grpc.ForwardingServerCallListener;
import io.grpc.Grpc;
import io.grpc.InternalMetadata;
import io.grpc.Metadata;
import io.grpc.ServerCall;
import io.grpc.ServerCallHandler;
Expand All @@ -35,6 +36,9 @@ final class TracingServerInterceptor implements ServerInterceptor {
private static final AtomicLongFieldUpdater<TracingServerCall> MESSAGE_ID_UPDATER =
AtomicLongFieldUpdater.newUpdater(TracingServerCall.class, "messageId");

private static final Metadata.Key<String> AUTHORITY_KEY =
InternalMetadata.keyOf(":authority", Metadata.ASCII_STRING_MARSHALLER);

private final Instrumenter<GrpcRequest, Status> instrumenter;
private final boolean captureExperimentalSpanAttributes;

Expand All @@ -49,12 +53,17 @@ public <REQUEST, RESPONSE> ServerCall.Listener<REQUEST> interceptCall(
ServerCall<REQUEST, RESPONSE> call,
Metadata headers,
ServerCallHandler<REQUEST, RESPONSE> next) {
String authority = call.getAuthority();
if (authority == null && headers != null) {
// armeria grpc client exposes authority in a header
authority = headers.get(AUTHORITY_KEY);
}
GrpcRequest request =
new GrpcRequest(
call.getMethodDescriptor(),
headers,
call.getAttributes().get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR),
call.getAuthority());
authority);
Context parentContext = Context.current();
if (!instrumenter.shouldStart(parentContext, request)) {
return next.startCall(call, headers);
Expand Down
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ include(":instrumentation:apache-httpclient:apache-httpclient-5.2:library")
include(":instrumentation:armeria-1.3:javaagent")
include(":instrumentation:armeria-1.3:library")
include(":instrumentation:armeria-1.3:testing")
include(":instrumentation:armeria-grpc-1.14:javaagent")
include(":instrumentation:async-http-client:async-http-client-1.9:javaagent")
include(":instrumentation:async-http-client:async-http-client-2.0:javaagent")
include(":instrumentation:aws-lambda:aws-lambda-core-1.0:javaagent")
Expand Down

0 comments on commit 66e4c5a

Please sign in to comment.