diff --git a/instrumentation/grpc-1.22.0/src/main/java/io/grpc/stub/ClientCalls_Instrumentation.java b/instrumentation/grpc-1.22.0/src/main/java/io/grpc/stub/ClientCalls_Instrumentation.java index 10b1cf02e5..7865c3171d 100644 --- a/instrumentation/grpc-1.22.0/src/main/java/io/grpc/stub/ClientCalls_Instrumentation.java +++ b/instrumentation/grpc-1.22.0/src/main/java/io/grpc/stub/ClientCalls_Instrumentation.java @@ -9,11 +9,8 @@ import com.newrelic.api.agent.NewRelic; import com.newrelic.api.agent.Segment; -import com.newrelic.api.agent.Token; import com.newrelic.api.agent.Trace; -import com.newrelic.api.agent.weaver.NewField; import com.newrelic.api.agent.weaver.Weave; -import com.newrelic.api.agent.weaver.WeaveAllConstructors; import com.newrelic.api.agent.weaver.Weaver; import com.nr.agent.instrumentation.grpc.GrpcConfig; import io.grpc.ClientCall_Instrumentation; @@ -22,7 +19,8 @@ public final class ClientCalls_Instrumentation { private static void startCall(ClientCall_Instrumentation call, - ClientCall_Instrumentation.Listener responseListener, boolean streamingResponse) { + ClientCall_Instrumentation.Listener responseListener, + boolean streamingResponse) { Segment segment = NewRelic.getAgent().getTransaction().startSegment("gRPC", "External"); call.segment = segment; Weaver.callOriginal(); diff --git a/instrumentation/grpc-1.30.0/build.gradle b/instrumentation/grpc-1.30.0/build.gradle new file mode 100644 index 0000000000..96a70f6f3c --- /dev/null +++ b/instrumentation/grpc-1.30.0/build.gradle @@ -0,0 +1,55 @@ +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.13' + + } +} +apply plugin: 'com.google.protobuf' +compileJava.options.bootstrapClasspath = null + +sourceCompatibility = JavaVersion.VERSION_1_8 +targetCompatibility = JavaVersion.VERSION_1_8 +compileTestJava { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +dependencies { + implementation(project(":agent-bridge")) + implementation("io.grpc:grpc-all:1.30.1") + implementation("com.google.protobuf:protobuf-java:3.7.0") + implementation("io.grpc:grpc-protobuf:1.30.1") + implementation("io.perfmark:perfmark-api:0.23.0") + +} + +jar { + manifest { attributes 'Implementation-Title': 'com.newrelic.instrumentation.grpc-1.30.0' } +} + +verifyInstrumentation { + passesOnly 'io.grpc:grpc-all:[1.30.0,)' +} + +def grpcVersion = '1.30.1' // CURRENT_GRPC_VERSION +def protobufVersion = '3.6.1' +def protocVersion = protobufVersion + +// to generate the proto classes, run ./gradlew generateTestProto +protobuf { + protoc { artifact = "com.google.protobuf:protoc:${protocVersion}" } + plugins { + grpc { artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}" } + } + generateProtoTasks { + all()*.plugins { grpc {} } + } +} + +site { + title 'gRPC' + type 'Messaging' +} diff --git a/instrumentation/grpc-1.30.0/src/main/java/com/nr/agent/instrumentation/grpc/GrpcConfig.java b/instrumentation/grpc-1.30.0/src/main/java/com/nr/agent/instrumentation/grpc/GrpcConfig.java new file mode 100644 index 0000000000..9ff40de150 --- /dev/null +++ b/instrumentation/grpc-1.30.0/src/main/java/com/nr/agent/instrumentation/grpc/GrpcConfig.java @@ -0,0 +1,21 @@ +/* + * + * * Copyright 2020 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.nr.agent.instrumentation.grpc; + +import com.newrelic.api.agent.NewRelic; + +public class GrpcConfig { + + public static final boolean disributedTracingEnabled = NewRelic.getAgent().getConfig().getValue("grpc.distributed_tracing.enabled", true); + + public static final boolean errorsEnabled = NewRelic.getAgent().getConfig().getValue("grpc.errors.enabled", true); + + private GrpcConfig() { + } + +} diff --git a/instrumentation/grpc-1.30.0/src/main/java/com/nr/agent/instrumentation/grpc/GrpcRequest.java b/instrumentation/grpc-1.30.0/src/main/java/com/nr/agent/instrumentation/grpc/GrpcRequest.java new file mode 100644 index 0000000000..31f343cc3f --- /dev/null +++ b/instrumentation/grpc-1.30.0/src/main/java/com/nr/agent/instrumentation/grpc/GrpcRequest.java @@ -0,0 +1,100 @@ +/* + * + * * Copyright 2020 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.nr.agent.instrumentation.grpc; + +import com.newrelic.api.agent.ExtendedRequest; +import com.newrelic.api.agent.HeaderType; +import io.grpc.Metadata; +import io.grpc.internal.GrpcUtil; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; + +public class GrpcRequest extends ExtendedRequest { + + private final String fullMethodName; + private final String authority; + private final Metadata metadata; + + public GrpcRequest(String fullMethodName, String authority, Metadata metadata) { + this.fullMethodName = fullMethodName; + this.authority = authority; + this.metadata = metadata; + } + + @Override + public String getMethod() { + return GrpcUtil.HTTP_METHOD; + } + + @Override + public String getRequestURI() { + try { + String path = "/" + fullMethodName; + return new URI("grpc", authority, path, null, null).toASCIIString(); + } catch (URISyntaxException e) { + return null; + } + } + + @Override + public String getRemoteUser() { + return null; + } + + @Override + public Enumeration getParameterNames() { + return null; + } + + @Override + public String[] getParameterValues(String name) { + return new String[0]; + } + + @Override + public Object getAttribute(String name) { + return null; + } + + @Override + public String getCookieValue(String name) { + return null; + } + + @Override + public HeaderType getHeaderType() { + return HeaderType.HTTP; + } + + @Override + public String getHeader(String name) { + return metadata.get(Metadata.Key.of(name, Metadata.ASCII_STRING_MARSHALLER)); + } + + @Override + public List getHeaders(String name) { + return iterableToList(metadata.getAll(Metadata.Key.of(name, Metadata.ASCII_STRING_MARSHALLER))); + } + + private List iterableToList(Iterable headers) { + if (headers == null) { + return null; + } + + List result = new ArrayList<>(); + for (String header : headers) { + result.add(header); + } + return result; + } +} diff --git a/instrumentation/grpc-1.30.0/src/main/java/com/nr/agent/instrumentation/grpc/GrpcResponse.java b/instrumentation/grpc-1.30.0/src/main/java/com/nr/agent/instrumentation/grpc/GrpcResponse.java new file mode 100644 index 0000000000..768c201144 --- /dev/null +++ b/instrumentation/grpc-1.30.0/src/main/java/com/nr/agent/instrumentation/grpc/GrpcResponse.java @@ -0,0 +1,58 @@ +/* + * + * * Copyright 2020 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.nr.agent.instrumentation.grpc; + +import com.newrelic.api.agent.ExtendedResponse; +import com.newrelic.api.agent.HeaderType; +import io.grpc.Metadata; +import io.grpc.Status; + +public class GrpcResponse extends ExtendedResponse { + + private final Status status; + private final Metadata headers; + + public GrpcResponse(Status status, Metadata headers) { + this.status = status; + this.headers = headers; + } + + @Override + public long getContentLength() { + // There isn't a good value here for gRPC + return -1; + } + + @Override + public int getStatus() throws Exception { + return status.getCode().value(); + } + + @Override + public String getStatusMessage() throws Exception { + return status.getDescription(); + } + + @Override + public String getContentType() { + return null; + } + + @Override + public HeaderType getHeaderType() { + return HeaderType.HTTP; + } + + @Override + public void setHeader(String name, String value) { + if (GrpcConfig.disributedTracingEnabled) { + headers.put(Metadata.Key.of(name, Metadata.ASCII_STRING_MARSHALLER), value); + } + } + +} diff --git a/instrumentation/grpc-1.30.0/src/main/java/com/nr/agent/instrumentation/grpc/InboundHeadersWrapper.java b/instrumentation/grpc-1.30.0/src/main/java/com/nr/agent/instrumentation/grpc/InboundHeadersWrapper.java new file mode 100644 index 0000000000..2b8d5c5d0c --- /dev/null +++ b/instrumentation/grpc-1.30.0/src/main/java/com/nr/agent/instrumentation/grpc/InboundHeadersWrapper.java @@ -0,0 +1,67 @@ +/* + * + * * Copyright 2020 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.nr.agent.instrumentation.grpc; + +import com.newrelic.api.agent.ExtendedInboundHeaders; +import com.newrelic.api.agent.HeaderType; +import io.grpc.Metadata; + +import java.util.ArrayList; +import java.util.List; + +public class InboundHeadersWrapper extends ExtendedInboundHeaders { + + private final Metadata metadata; + private final Metadata trailers; + + public InboundHeadersWrapper(Metadata metadata, Metadata trailers) { + this.metadata = metadata; + this.trailers = trailers; + } + + @Override + public String getHeader(String key) { + String value = null; + if (metadata != null) { + value = metadata.get(Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER)); + } + // If we didn't find the value in the headers be sure to check the trailers as a fallback + if (value == null && trailers != null) { + value = trailers.get(Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER)); + } + return value; + } + + @Override + public List getHeaders(String name) { + List result = iterableToList(metadata.getAll(Metadata.Key.of(name, Metadata.ASCII_STRING_MARSHALLER))); + if ((result == null || result.isEmpty()) && trailers != null) { + // If we didn't find the value in the headers be sure to check the trailers as a fallback + result = iterableToList(trailers.getAll(Metadata.Key.of(name, Metadata.ASCII_STRING_MARSHALLER))); + } + return result; + } + + private List iterableToList(Iterable headers) { + if (headers == null) { + return null; + } + + List result = new ArrayList<>(); + for (String header : headers) { + result.add(header); + } + return result; + } + + @Override + public HeaderType getHeaderType() { + return HeaderType.HTTP; + } + +} diff --git a/instrumentation/grpc-1.30.0/src/main/java/com/nr/agent/instrumentation/grpc/OutboundHeadersWrapper.java b/instrumentation/grpc-1.30.0/src/main/java/com/nr/agent/instrumentation/grpc/OutboundHeadersWrapper.java new file mode 100644 index 0000000000..3359805d0a --- /dev/null +++ b/instrumentation/grpc-1.30.0/src/main/java/com/nr/agent/instrumentation/grpc/OutboundHeadersWrapper.java @@ -0,0 +1,33 @@ +/* + * + * * Copyright 2020 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.nr.agent.instrumentation.grpc; + +import com.newrelic.api.agent.HeaderType; +import com.newrelic.api.agent.OutboundHeaders; +import io.grpc.Metadata; + +public class OutboundHeadersWrapper implements OutboundHeaders { + + private final Metadata metadata; + + public OutboundHeadersWrapper(Metadata metadata) { + this.metadata = metadata; + } + + @Override + public HeaderType getHeaderType() { + return HeaderType.HTTP; + } + + @Override + public void setHeader(String key, String value) { + if (GrpcConfig.disributedTracingEnabled) { + metadata.put(Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER), value); + } + } +} diff --git a/instrumentation/grpc-1.30.0/src/main/java/io/grpc/Channel_Instrumentation.java b/instrumentation/grpc-1.30.0/src/main/java/io/grpc/Channel_Instrumentation.java new file mode 100644 index 0000000000..02883bf6fd --- /dev/null +++ b/instrumentation/grpc-1.30.0/src/main/java/io/grpc/Channel_Instrumentation.java @@ -0,0 +1,32 @@ +/* + * + * * Copyright 2020 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package io.grpc; + +import com.newrelic.api.agent.NewRelic; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; + +@Weave(type = MatchType.BaseClass, originalName = "io.grpc.Channel") +public class Channel_Instrumentation { + + public ClientCall_Instrumentation newCall( + MethodDescriptor methodDescriptor, CallOptions callOptions) { + ClientCall_Instrumentation result = Weaver.callOriginal(); + result.methodDescriptor = methodDescriptor; + result.authority = authority(); + if (methodDescriptor != null && methodDescriptor.getType() != null) { + NewRelic.addCustomParameter("grpc.type", methodDescriptor.getType().name()); + } + return result; + } + + public String authority() { + return Weaver.callOriginal(); + } +} diff --git a/instrumentation/grpc-1.30.0/src/main/java/io/grpc/ClientCall_Instrumentation.java b/instrumentation/grpc-1.30.0/src/main/java/io/grpc/ClientCall_Instrumentation.java new file mode 100644 index 0000000000..df181d26d9 --- /dev/null +++ b/instrumentation/grpc-1.30.0/src/main/java/io/grpc/ClientCall_Instrumentation.java @@ -0,0 +1,126 @@ +/* + * + * * Copyright 2020 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package io.grpc; + +import com.newrelic.agent.bridge.AgentBridge; +import com.newrelic.api.agent.ExternalParameters; +import com.newrelic.api.agent.HttpParameters; +import com.newrelic.api.agent.NewRelic; +import com.newrelic.api.agent.Segment; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.NewField; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; +import com.nr.agent.instrumentation.grpc.GrpcConfig; +import com.nr.agent.instrumentation.grpc.InboundHeadersWrapper; +import com.nr.agent.instrumentation.grpc.OutboundHeadersWrapper; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.logging.Level; + +@Weave(type = MatchType.BaseClass, originalName = "io.grpc.ClientCall") +public class ClientCall_Instrumentation { + + @NewField + public Segment segment = null; + + @NewField + public String authority = null; + + @NewField + public MethodDescriptor methodDescriptor; + + public void start(Listener responseListener, Metadata headers) { + if (segment != null) { + responseListener.segment = segment; + responseListener.authority = authority; + responseListener.methodDescriptor = methodDescriptor; + OutboundHeadersWrapper wrapper = new OutboundHeadersWrapper(headers); + segment.addOutboundRequestHeaders(wrapper); + } + Weaver.callOriginal(); + } + + public void halfClose() { + Weaver.callOriginal(); + cleanUpNewFields(); + } + + public void cancel(String message, Throwable cause) { + if (GrpcConfig.errorsEnabled && cause != null) { + NewRelic.noticeError(cause); + } + if (segment != null) { + segment.end(); + } + Weaver.callOriginal(); + cleanUpNewFields(); + } + + // This is important to keep here in order to avoid a memory leak + private void cleanUpNewFields() { + this.segment = null; + this.authority = null; + this.methodDescriptor = null; + } + + @Weave(type = MatchType.BaseClass, originalName = "io.grpc.ClientCall$Listener") + public abstract static class Listener { + + @NewField + public Segment segment = null; + @NewField + public String authority; + @NewField + public MethodDescriptor methodDescriptor; + @NewField + private Metadata headers = null; + + public void onHeaders(Metadata headers) { + this.headers = headers; + Weaver.callOriginal(); + } + + public void onClose(Status status, Metadata trailers) { + if (GrpcConfig.errorsEnabled && status.getCause() != null) { + // If an error occurred during the close of this call we should record it + NewRelic.noticeError(status.getCause()); + } + + Weaver.callOriginal(); + InboundHeadersWrapper wrapper = new InboundHeadersWrapper(headers, trailers); + + URI uri = null; + if (authority != null && methodDescriptor != null) { + try { + uri = new URI("grpc", authority, "/" + methodDescriptor.getFullMethodName(), null, null); + } catch (URISyntaxException e) { + AgentBridge.getAgent().getLogger().log(Level.FINER, "Exception with uri: " + e.getMessage()); + } + if (uri != null) { + ExternalParameters params = HttpParameters.library("gRPC") + .uri(uri) + .procedure(methodDescriptor.getFullMethodName()) + .inboundHeaders(wrapper) + .build(); + if (segment != null) { + segment.reportAsExternal(params); + segment.end(); + } + } + } + + // This is important to keep here in order to avoid a memory leak + this.segment = null; + this.headers = null; + this.authority = null; + this.methodDescriptor = null; + } + } +} diff --git a/instrumentation/grpc-1.30.0/src/main/java/io/grpc/ServerCallListener_Instrumentation.java b/instrumentation/grpc-1.30.0/src/main/java/io/grpc/ServerCallListener_Instrumentation.java new file mode 100644 index 0000000000..1315eda6a7 --- /dev/null +++ b/instrumentation/grpc-1.30.0/src/main/java/io/grpc/ServerCallListener_Instrumentation.java @@ -0,0 +1,33 @@ +/* + * + * * Copyright 2020 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package io.grpc; + +import com.newrelic.api.agent.Token; +import com.newrelic.api.agent.Trace; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.NewField; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; + +@Weave(originalName = "io.grpc.ServerCall$Listener", type = MatchType.BaseClass) +public abstract class ServerCallListener_Instrumentation { + + @NewField + public Token token; + + @Trace(async = true) + public void onHalfClose() { + // onHalfClose gets executed right before we enter customer code. This helps ensure that they will have a transaction available on the thread + if (token != null) { + token.linkAndExpire(); + this.token = null; + } + Weaver.callOriginal(); + } + +} diff --git a/instrumentation/grpc-1.30.0/src/main/java/io/grpc/internal/ServerCallImpl_Instrumentation.java b/instrumentation/grpc-1.30.0/src/main/java/io/grpc/internal/ServerCallImpl_Instrumentation.java new file mode 100644 index 0000000000..1adc1f838d --- /dev/null +++ b/instrumentation/grpc-1.30.0/src/main/java/io/grpc/internal/ServerCallImpl_Instrumentation.java @@ -0,0 +1,24 @@ +/* + * + * * Copyright 2020 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package io.grpc.internal; + +import com.newrelic.agent.bridge.AgentBridge; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; +import io.grpc.ServerCallListener_Instrumentation; + +@Weave(originalName = "io.grpc.internal.ServerCallImpl") +final class ServerCallImpl_Instrumentation { + + ServerStreamListener newServerStreamListener(ServerCallListener_Instrumentation listener) { + // This is the point where a request comes into grpc + // Store a token on the listener so we can bring the transaction into customer code + listener.token = AgentBridge.getAgent().getTransaction().getToken(); + return Weaver.callOriginal(); + } +} diff --git a/instrumentation/grpc-1.30.0/src/main/java/io/grpc/internal/ServerImpl_Instrumentation.java b/instrumentation/grpc-1.30.0/src/main/java/io/grpc/internal/ServerImpl_Instrumentation.java new file mode 100644 index 0000000000..55431a0757 --- /dev/null +++ b/instrumentation/grpc-1.30.0/src/main/java/io/grpc/internal/ServerImpl_Instrumentation.java @@ -0,0 +1,52 @@ +/* + * + * * Copyright 2020 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package io.grpc.internal; + +import com.newrelic.agent.bridge.AgentBridge; +import com.newrelic.api.agent.NewRelic; +import com.newrelic.api.agent.Trace; +import com.newrelic.api.agent.TransactionNamePriority; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; +import com.nr.agent.instrumentation.grpc.GrpcRequest; +import io.grpc.Context; +import io.grpc.Metadata; +import io.grpc.MethodDescriptor; +import io.grpc.ServerMethodDefinition; +import io.perfmark.Tag; + +@Weave(originalName = "io.grpc.internal.ServerImpl") +public class ServerImpl_Instrumentation { + + @Weave(originalName = "io.grpc.internal.ServerImpl$ServerTransportListenerImpl") + private static final class ServerTransportListenerImpl_Instrumentation { + + @Trace(dispatcher = true) + private ServerStreamListener startCall(ServerStream_Instrumentation stream, String fullMethodName, + ServerMethodDefinition methodDef, Metadata headers, + Context.CancellableContext context, StatsTraceContext statsTraceCtx, Tag tag) { + MethodDescriptor methodDescriptor = methodDef.getMethodDescriptor(); + NewRelic.getAgent().getTransaction().setWebRequest(new GrpcRequest(fullMethodName, stream.getAuthority(), headers)); + + stream.token = AgentBridge.getAgent().getTransaction().getToken(); + + if (fullMethodName != null && !fullMethodName.isEmpty()) { + NewRelic.addCustomParameter("request.method", fullMethodName); + NewRelic.getAgent().getTracedMethod().setMetricName("gRPC", "ServerCallHandler", "startCall", fullMethodName); + NewRelic.getAgent().getTransaction().setTransactionName(TransactionNamePriority.FRAMEWORK_LOW, false, "gRPC", fullMethodName); + + if (methodDescriptor != null) { + NewRelic.addCustomParameter("grpc.type", methodDescriptor.getType().name()); + } + } + + return Weaver.callOriginal(); + } + } + +} diff --git a/instrumentation/grpc-1.30.0/src/main/java/io/grpc/internal/ServerStream_Instrumentation.java b/instrumentation/grpc-1.30.0/src/main/java/io/grpc/internal/ServerStream_Instrumentation.java new file mode 100644 index 0000000000..6ed40dba40 --- /dev/null +++ b/instrumentation/grpc-1.30.0/src/main/java/io/grpc/internal/ServerStream_Instrumentation.java @@ -0,0 +1,84 @@ +/* + * + * * Copyright 2020 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package io.grpc.internal; + +import com.newrelic.agent.bridge.Token; +import com.newrelic.api.agent.NewRelic; +import com.newrelic.api.agent.Trace; +import com.newrelic.api.agent.Transaction; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.NewField; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; +import com.nr.agent.instrumentation.grpc.GrpcConfig; +import com.nr.agent.instrumentation.grpc.GrpcResponse; +import io.grpc.Metadata; +import io.grpc.Status; + +// This class follows a request through the server side so we can hook in here to capture the outgoing request +@Weave(type = MatchType.Interface, originalName = "io.grpc.internal.ServerStream") +public abstract class ServerStream_Instrumentation { + + @NewField + public Token token; + + @Trace(async = true) + public void close(Status status, Metadata trailers) { + if (token != null) { + token.link(); + Transaction transaction = NewRelic.getAgent().getTransaction(); + transaction.setWebResponse(new GrpcResponse(status, trailers)); + transaction.addOutboundResponseHeaders(); + transaction.markResponseSent(); + } + + if (status != null) { + NewRelic.addCustomParameter("response.status", status.getCode().value()); + if (GrpcConfig.errorsEnabled && status.getCause() != null) { + // If an error occurred during the close of this server call we should record it + NewRelic.noticeError(status.getCause()); + } + } + + Weaver.callOriginal(); + + if (token != null) { + token.expire(); + token = null; + } + } + + // server had an internal error + @Trace(async = true) + public void cancel(Status status) { + if (token != null) { + token.link(); + Transaction transaction = token.getTransaction(); + transaction.setWebResponse(new GrpcResponse(status, new Metadata())); + transaction.addOutboundResponseHeaders(); + transaction.markResponseSent(); + } + + if (status != null) { + NewRelic.addCustomParameter("response.status", status.getCode().value()); + if (GrpcConfig.errorsEnabled && status.getCause() != null) { + // If an error occurred during the close of this server call we should record it + NewRelic.noticeError(status.getCause()); + } + } + + Weaver.callOriginal(); + + if (token != null) { + token.expire(); + token = null; + } + } + + public abstract String getAuthority(); +} diff --git a/instrumentation/grpc-1.30.0/src/main/java/io/grpc/stub/ClientCalls_Instrumentation.java b/instrumentation/grpc-1.30.0/src/main/java/io/grpc/stub/ClientCalls_Instrumentation.java new file mode 100644 index 0000000000..7f50b0a81c --- /dev/null +++ b/instrumentation/grpc-1.30.0/src/main/java/io/grpc/stub/ClientCalls_Instrumentation.java @@ -0,0 +1,62 @@ +/* + * + * * Copyright 2020 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package io.grpc.stub; + +import com.newrelic.api.agent.NewRelic; +import com.newrelic.api.agent.Segment; +import com.newrelic.api.agent.Trace; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; +import com.nr.agent.instrumentation.grpc.GrpcConfig; +import io.grpc.ClientCall_Instrumentation; + +@Weave(originalName = "io.grpc.stub.ClientCalls") +public final class ClientCalls_Instrumentation { + + private static void startCall( + ClientCall_Instrumentation call, + ClientCalls_Instrumentation.StartableListener_Instrumentation responseListener) { + Segment segment = NewRelic.getAgent().getTransaction().startSegment("gRPC", "External"); + call.segment = segment; + Weaver.callOriginal(); + } + + @Weave(type = MatchType.BaseClass, originalName = "io.grpc.stub.ClientCalls$StartableListener") + private abstract static class StartableListener_Instrumentation extends ClientCall_Instrumentation.Listener { + abstract void onStart(); + } + + @Weave(originalName = "io.grpc.stub.ClientCalls$CallToStreamObserverAdapter") + private static final class CallToStreamObserverAdapter { + + private final ClientCall_Instrumentation call = Weaver.callOriginal(); + + @Trace(async = true) + public void onError(Throwable t) { + if (call != null && call.segment != null) { + call.segment.getTransaction().getToken().linkAndExpire(); + } + if (GrpcConfig.errorsEnabled) { + NewRelic.noticeError(t); + } + Weaver.callOriginal(); + } + + @Trace(async = true) + public void onCompleted() { + if (call != null && call.segment != null) { + call.segment.getTransaction().getToken().linkAndExpire(); + } + Weaver.callOriginal(); + } + + } + +} + diff --git a/instrumentation/grpc-1.30.0/src/test/java/BasicRequestsTest.java b/instrumentation/grpc-1.30.0/src/test/java/BasicRequestsTest.java new file mode 100644 index 0000000000..c71ac786b4 --- /dev/null +++ b/instrumentation/grpc-1.30.0/src/test/java/BasicRequestsTest.java @@ -0,0 +1,130 @@ +/* + * + * * Copyright 2020 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +import app.TestClient; +import app.TestServer; +import com.newrelic.agent.introspec.CatHelper; +import com.newrelic.agent.introspec.ErrorEvent; +import com.newrelic.agent.introspec.InstrumentationTestConfig; +import com.newrelic.agent.introspec.InstrumentationTestRunner; +import com.newrelic.agent.introspec.Introspector; +import com.newrelic.agent.introspec.TraceSegment; +import com.newrelic.agent.introspec.TransactionEvent; +import com.newrelic.agent.introspec.TransactionTrace; +import com.newrelic.agent.service.ServiceFactory; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Collection; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@RunWith(InstrumentationTestRunner.class) +@InstrumentationTestConfig(includePrefixes = { "io.grpc", "com.nr.agent.instrumentation.grpc" }) +public class BasicRequestsTest { + + private static TestServer server; + private static TestClient client; + + @BeforeClass + public static void before() throws Exception { + server = new TestServer(); + server.start(); + client = new TestClient("localhost", server.getPort()); + } + + @AfterClass + public static void after() throws InterruptedException { + if (client != null) { + client.shutdown(); + } + if (server != null) { + server.stop(); + } + } + + @Test + public void testBlockingRequest() { + client.helloBlocking("Blocking"); + + String fullMethod = "helloworld.Greeter/SayHello"; + String clientTxName = "OtherTransaction/Custom/app.TestClient/helloBlocking"; + String serverTxName = "WebTransaction/gRPC/helloworld.Greeter/SayHello"; + ValidationHelper.validateGrpcInteraction(server, clientTxName, serverTxName, fullMethod, "UNARY", "Blocking"); + } + + @Test + public void testFutureRequest() throws Exception { + client.helloFuture("Future"); + + String fullMethod = "helloworld.Greeter/SayHello"; + String clientTxName = "OtherTransaction/Custom/app.TestClient/helloFuture"; + String serverTxName = "WebTransaction/gRPC/helloworld.Greeter/SayHello"; + ValidationHelper.validateGrpcInteraction(server, clientTxName, serverTxName, fullMethod, "UNARY", "Future"); + } + + @Test + public void testAsyncRequest() { + client.helloAsync("Async"); + + String fullMethod = "helloworld.Greeter/SayHello"; + String clientTxName = "OtherTransaction/Custom/app.TestClient/helloAsync"; + String serverTxName = "WebTransaction/gRPC/helloworld.Greeter/SayHello"; + ValidationHelper.validateGrpcInteraction(server, clientTxName, serverTxName, fullMethod, "UNARY", "Async"); + } + + @Test + public void testStreamingRequest() { + client.helloStreaming("Streaming"); + + String fullMethod = "manualflowcontrol.StreamingGreeter/SayHelloStreaming"; + String clientTxName = "OtherTransaction/Custom/app.TestClient/helloStreaming"; + String serverTxName = "WebTransaction/gRPC/" + fullMethod; + ValidationHelper.validateGrpcInteraction(server, clientTxName, serverTxName, fullMethod, "BIDI_STREAMING", "Streaming"); + } + + @Test + public void testUncaughtException() { + try { + client.throwException("Blocking"); + } catch (Exception e) { + } + + String fullMethod = "helloworld.Greeter/ThrowException"; + String clientTxName = "OtherTransaction/Custom/app.TestClient/throwException"; + String serverTxName = "WebTransaction/gRPC/helloworld.Greeter/ThrowException"; + ValidationHelper.validateExceptionGrpcInteraction(server, clientTxName, serverTxName, fullMethod, "UNARY", "Blocking", 2); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Collection errorEvents = introspector.getErrorEvents(); + CatHelper.verifyOneSuccessfulCat(introspector, clientTxName, serverTxName); + assertEquals(2, errorEvents.size()); + } + + @Test + public void testCaughtException() { + try { + client.throwCaughtException("Blocking"); + } catch (Exception e) { + } + + String fullMethod = "helloworld.Greeter/ThrowCaughtException"; + String clientTxName = "OtherTransaction/Custom/app.TestClient/throwCaughtException"; + String serverTxName = "WebTransaction/gRPC/helloworld.Greeter/ThrowCaughtException"; + ValidationHelper.validateExceptionGrpcInteraction(server, clientTxName, serverTxName, fullMethod, "UNARY", "Blocking", 10); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Collection errorEvents = introspector.getErrorEvents(); + CatHelper.verifyOneSuccessfulCat(introspector, clientTxName, serverTxName); + assertEquals(1, errorEvents.size()); + } + +} diff --git a/instrumentation/grpc-1.30.0/src/test/java/GrpcErrorsTest.java b/instrumentation/grpc-1.30.0/src/test/java/GrpcErrorsTest.java new file mode 100644 index 0000000000..0a183fd0c9 --- /dev/null +++ b/instrumentation/grpc-1.30.0/src/test/java/GrpcErrorsTest.java @@ -0,0 +1,88 @@ +/* + * + * * Copyright 2020 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +import app.TestClient; +import app.TestServer; +import com.newrelic.agent.introspec.CatHelper; +import com.newrelic.agent.introspec.ErrorEvent; +import com.newrelic.agent.introspec.InstrumentationTestConfig; +import com.newrelic.agent.introspec.InstrumentationTestRunner; +import com.newrelic.agent.introspec.Introspector; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Collection; + +import static org.junit.Assert.assertEquals; + +@RunWith(InstrumentationTestRunner.class) +@InstrumentationTestConfig(includePrefixes = { "io.grpc", "com.nr.agent.instrumentation.grpc" }, configName = "no_grpc_errors.yml") +public class GrpcErrorsTest { + + private static TestServer server; + private static TestClient client; + + @BeforeClass + public static void before() throws Exception { + server = new TestServer(); + server.start(); + client = new TestClient("localhost", server.getPort()); + } + + @AfterClass + public static void after() throws InterruptedException { + if (client != null) { + client.shutdown(); + } + if (server != null) { + server.stop(); + } + } + + @Test + public void testUncaughtException() { + try { + client.throwException("Blocking"); + } catch (Exception e) { + } + String fullMethod = "helloworld.Greeter/ThrowException"; + String clientTxName = "OtherTransaction/Custom/app.TestClient/throwException"; + String serverTxName = "WebTransaction/gRPC/helloworld.Greeter/ThrowException"; + ValidationHelper.validateExceptionGrpcInteraction(server, clientTxName, serverTxName, fullMethod, "UNARY", "Blocking", 2); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Collection errorEvents = introspector.getErrorEvents(); + CatHelper.verifyOneSuccessfulCat(introspector, clientTxName, serverTxName); + + // Even though gRPC errors are disabled this will still generate an error event + // because the uncaught exception triggers the agent's built-in error capture + assertEquals(1, errorEvents.size()); + } + + @Test + public void testCaughtException() { + try { + client.throwCaughtException("Blocking"); + } catch (Exception e) { + } + + String fullMethod = "helloworld.Greeter/ThrowCaughtException"; + String clientTxName = "OtherTransaction/Custom/app.TestClient/throwCaughtException"; + String serverTxName = "WebTransaction/gRPC/helloworld.Greeter/ThrowCaughtException"; + ValidationHelper.validateExceptionGrpcInteraction(server, clientTxName, serverTxName, fullMethod, "UNARY", "Blocking", 10); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Collection errorEvents = introspector.getErrorEvents(); + CatHelper.verifyOneSuccessfulCat(introspector, clientTxName, serverTxName); + + // Disabling the gRPC error capture prevents error events from being created + assertEquals(0, errorEvents.size()); + } + +} diff --git a/instrumentation/grpc-1.30.0/src/test/java/ValidationHelper.java b/instrumentation/grpc-1.30.0/src/test/java/ValidationHelper.java new file mode 100644 index 0000000000..f867ede24d --- /dev/null +++ b/instrumentation/grpc-1.30.0/src/test/java/ValidationHelper.java @@ -0,0 +1,139 @@ +/* + * + * * Copyright 2020 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +import app.TestServer; +import com.newrelic.agent.introspec.CatHelper; +import com.newrelic.agent.introspec.InstrumentationTestRunner; +import com.newrelic.agent.introspec.Introspector; +import com.newrelic.agent.introspec.TraceSegment; +import com.newrelic.agent.introspec.TransactionEvent; +import com.newrelic.agent.introspec.TransactionTrace; +import com.newrelic.agent.service.ServiceFactory; + +import java.util.Collection; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +class ValidationHelper { + + static void validateGrpcInteraction(TestServer server, String clientTxName, String serverTxName, String fullMethod, String grpcType, String name) { + checkTransactions(2); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + + String externalTxSegmentName = "ExternalTransaction/localhost/" + getCrossProcessId() + "/" + serverTxName; + + // Verify that CAT works + CatHelper.verifyOneSuccessfulCat(introspector, clientTxName, serverTxName); + + // Client side + Collection clientTransactionTrace = introspector.getTransactionTracesForTransaction(clientTxName); + assertEquals(1, clientTransactionTrace.size()); + TransactionTrace trace = clientTransactionTrace.iterator().next(); + boolean foundSegment = false; + for (TraceSegment segment : trace.getInitialTraceSegment().getChildren()) { + // Verify external request exists and is valid + if (segment.getClassName().equals("External")) { + assertEquals(externalTxSegmentName, segment.getName()); + assertEquals("grpc://localhost:" + server.getPort() + "/" + fullMethod, segment.getUri()); // This is the value of "http.url" + assertEquals(1, segment.getCallCount()); + assertEquals("", segment.getMethodName()); + assertEquals(grpcType, segment.getTracerAttributes().get("grpc.type")); + assertEquals("gRPC", segment.getTracerAttributes().get("component")); + assertEquals(fullMethod, segment.getTracerAttributes().get("http.method")); + foundSegment = true; + } + } + assertTrue("Unable to find client side External/ segment", foundSegment); + + // Server side + Collection serverTransactionTrace = introspector.getTransactionTracesForTransaction(serverTxName); + assertEquals(1, serverTransactionTrace.size()); + TransactionTrace serverTrace = serverTransactionTrace.iterator().next(); + TraceSegment rootSegment = serverTrace.getInitialTraceSegment(); + assertTrue(rootSegment.getName().endsWith(fullMethod)); + assertEquals(1, rootSegment.getCallCount()); + assertEquals(fullMethod, rootSegment.getTracerAttributes().get("request.method")); + assertEquals(0, rootSegment.getTracerAttributes().get("response.status")); + assertEquals(grpcType, rootSegment.getTracerAttributes().get("grpc.type")); + + // Custom attributes (to test tracing into customer code) + Collection serverTxEvents = introspector.getTransactionEvents(serverTxName); + assertEquals(1, serverTxEvents.size()); + TransactionEvent serverTxEvent = serverTxEvents.iterator().next(); + assertNotNull(serverTxEvent); + if (grpcType.equals("BIDI_STREAMING")) { + // For the streaming case, we just ensure that the transaction is available to use for a streaming handler but we do not propagate it + assertEquals("true", serverTxEvent.getAttributes().get("customParameter")); + } else { + assertEquals(name, serverTxEvent.getAttributes().get("sayHelloBefore")); + assertEquals(name, serverTxEvent.getAttributes().get("sayHelloAfter")); + } + + assertEquals(0, serverTxEvent.getAttributes().get("response.status")); + assertEquals("grpc://localhost:" + server.getPort() + "/" + fullMethod, serverTxEvent.getAttributes().get("request.uri")); + } + + static void validateExceptionGrpcInteraction(TestServer server, String clientTxName, String serverTxName, String fullMethod, String grpcType, String name, + int status) { + checkTransactions(2); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + + String externalTxSegmentName = "ExternalTransaction/localhost/" + getCrossProcessId() + "/" + serverTxName; + + // Client side + Collection clientTransactionTrace = introspector.getTransactionTracesForTransaction(clientTxName); + assertEquals(1, clientTransactionTrace.size()); + TransactionTrace trace = clientTransactionTrace.iterator().next(); + boolean foundSegment = false; + for (TraceSegment segment : trace.getInitialTraceSegment().getChildren()) { + // Verify external request exists and is valid + if (segment.getClassName().equals("External")) { + assertEquals(externalTxSegmentName, segment.getName()); + assertEquals("grpc://localhost:" + server.getPort() + "/" + fullMethod, segment.getUri()); + assertEquals(1, segment.getCallCount()); + assertEquals("", segment.getMethodName()); + assertEquals(grpcType, segment.getTracerAttributes().get("grpc.type")); + assertEquals("gRPC", segment.getTracerAttributes().get("component")); + assertEquals(fullMethod, segment.getTracerAttributes().get("http.method")); + foundSegment = true; + } + } + assertTrue("Unable to find client side External/ segment", foundSegment); + + // Server side + Collection serverTransactionTrace = introspector.getTransactionTracesForTransaction(serverTxName); + assertEquals(1, serverTransactionTrace.size()); + TransactionTrace serverTrace = serverTransactionTrace.iterator().next(); + TraceSegment rootSegment = serverTrace.getInitialTraceSegment(); + assertTrue(rootSegment.getName().endsWith(fullMethod)); + assertEquals(1, rootSegment.getCallCount()); + assertEquals(fullMethod, rootSegment.getTracerAttributes().get("request.method")); + assertEquals(status, rootSegment.getTracerAttributes().get("response.status")); + assertEquals(grpcType, rootSegment.getTracerAttributes().get("grpc.type")); + + // Custom attributes (to test tracing into customer code) + Collection serverTxEvents = introspector.getTransactionEvents(serverTxName); + assertEquals(1, serverTxEvents.size()); + TransactionEvent serverTxEvent = serverTxEvents.iterator().next(); + assertNotNull(serverTxEvent); + assertEquals(status, serverTxEvent.getAttributes().get("response.status")); + assertEquals("grpc://localhost:" + server.getPort() + "/" + fullMethod, serverTxEvent.getAttributes().get("request.uri")); + } + + private static void checkTransactions(int expected) { + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + assertEquals(expected, introspector.getFinishedTransactionCount(30000)); + } + + private static String getCrossProcessId() { + return ServiceFactory.getConfigService().getDefaultAgentConfig().getCrossProcessConfig().getCrossProcessId(); + } +} diff --git a/instrumentation/grpc-1.30.0/src/test/java/app/NewRelicClientInterceptor.java b/instrumentation/grpc-1.30.0/src/test/java/app/NewRelicClientInterceptor.java new file mode 100644 index 0000000000..59fb07eae8 --- /dev/null +++ b/instrumentation/grpc-1.30.0/src/test/java/app/NewRelicClientInterceptor.java @@ -0,0 +1,37 @@ +/* + * + * * Copyright 2020 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package app; + +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.ForwardingClientCall; +import io.grpc.ForwardingClientCallListener; +import io.grpc.Metadata; +import io.grpc.MethodDescriptor; + +public class NewRelicClientInterceptor implements ClientInterceptor { + + @Override + public ClientCall interceptCall(MethodDescriptor method, CallOptions callOptions, Channel next) { + return new ForwardingClientCall.SimpleForwardingClientCall(next.newCall(method, callOptions)) { + + @Override + public void start(Listener responseListener, Metadata headers) { + super.start(new ForwardingClientCallListener.SimpleForwardingClientCallListener(responseListener) { + @Override + public void onHeaders(Metadata headers) { + super.onHeaders(headers); + } + }, headers); + } + }; + } + +} diff --git a/instrumentation/grpc-1.30.0/src/test/java/app/NewRelicServerInterceptor.java b/instrumentation/grpc-1.30.0/src/test/java/app/NewRelicServerInterceptor.java new file mode 100644 index 0000000000..1a9d19a116 --- /dev/null +++ b/instrumentation/grpc-1.30.0/src/test/java/app/NewRelicServerInterceptor.java @@ -0,0 +1,28 @@ +/* + * + * * Copyright 2020 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package app; + +import io.grpc.ForwardingServerCall; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; + +public class NewRelicServerInterceptor implements ServerInterceptor { + + @Override + public ServerCall.Listener interceptCall(final ServerCall call, Metadata headers, ServerCallHandler next) { + return next.startCall(new ForwardingServerCall.SimpleForwardingServerCall(call) { + @Override + public void sendHeaders(Metadata responseHeaders) { + super.sendHeaders(responseHeaders); + } + }, headers); + } + +} diff --git a/instrumentation/grpc-1.30.0/src/test/java/app/TestClient.java b/instrumentation/grpc-1.30.0/src/test/java/app/TestClient.java new file mode 100644 index 0000000000..546312079b --- /dev/null +++ b/instrumentation/grpc-1.30.0/src/test/java/app/TestClient.java @@ -0,0 +1,128 @@ +/* + * + * * Copyright 2020 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package app; + +import com.google.common.util.concurrent.ListenableFuture; +import com.newrelic.api.agent.NewRelic; +import com.newrelic.api.agent.Segment; +import com.newrelic.api.agent.Trace; +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import io.grpc.examples.helloworld.GreeterGrpc; +import io.grpc.examples.helloworld.HelloReply; +import io.grpc.examples.helloworld.HelloRequest; +import io.grpc.examples.manualflowcontrol.StreamingGreeterGrpc; +import io.grpc.stub.StreamObserver; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +public class TestClient { + private final ManagedChannel channel; + private final GreeterGrpc.GreeterBlockingStub greeterStub; + private final GreeterGrpc.GreeterFutureStub greeterFutureStub; + private final GreeterGrpc.GreeterStub asyncGreeterStub; + private final StreamingGreeterGrpc.StreamingGreeterStub streamingGreeterStub; + + public TestClient(String host, int port) { + this(ManagedChannelBuilder.forAddress(host, port) + .intercept(new NewRelicClientInterceptor()) + .usePlaintext() + .build()); + } + + TestClient(ManagedChannel channel) { + this.channel = channel; + greeterStub = GreeterGrpc.newBlockingStub(channel); + greeterFutureStub = GreeterGrpc.newFutureStub(channel); + asyncGreeterStub = GreeterGrpc.newStub(channel); + streamingGreeterStub = StreamingGreeterGrpc.newStub(channel); + } + + public void shutdown() throws InterruptedException { + channel.shutdown().awaitTermination(5, TimeUnit.SECONDS); + } + + @Trace(dispatcher = true) + public void helloBlocking(String name) { + HelloRequest request = HelloRequest.newBuilder().setName(name).build(); + greeterStub.sayHello(request); + } + + @Trace(dispatcher = true) + public void helloFuture(String name) throws InterruptedException, ExecutionException, TimeoutException { + HelloRequest request = HelloRequest.newBuilder().setName(name).build(); + ListenableFuture helloReplyListenableFuture = greeterFutureStub.sayHello(request); + helloReplyListenableFuture.get(10, TimeUnit.SECONDS); + } + + @Trace(dispatcher = true) + public void helloAsync(String name) { + final Segment segment = NewRelic.getAgent().getTransaction().startSegment("helloAsync"); + + HelloRequest request = HelloRequest.newBuilder().setName(name).build(); + asyncGreeterStub.sayHello(request, new StreamObserver() { + @Override + public void onNext(HelloReply value) { + System.out.println("Next: " + value); + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onCompleted() { + segment.end(); + } + }); + } + + @Trace(dispatcher = true) + public void helloStreaming(String name) { + final Segment segment = NewRelic.getAgent().getTransaction().startSegment("helloStreaming"); + + StreamObserver stream = streamingGreeterStub.sayHelloStreaming( + new StreamObserver() { + @Override + public void onNext(io.grpc.examples.manualflowcontrol.HelloReply value) { + System.out.println("Next: " + value); + } + + @Override + public void onError(Throwable t) { + + } + + @Override + public void onCompleted() { + segment.end(); + } + }); + + io.grpc.examples.manualflowcontrol.HelloRequest request = io.grpc.examples.manualflowcontrol.HelloRequest.newBuilder().setName(name).build(); + stream.onNext(request); + stream.onCompleted(); + } + + @Trace(dispatcher = true) + public void throwException(String name) { + HelloRequest request = HelloRequest.newBuilder().setName(name).build(); + greeterStub.throwException(request); + } + + @Trace(dispatcher = true) + public void throwCaughtException(String name) { + HelloRequest request = HelloRequest.newBuilder().setName(name).build(); + try { + greeterStub.throwCaughtException(request); + } catch (Exception e) { + } + } +} diff --git a/instrumentation/grpc-1.30.0/src/test/java/app/TestServer.java b/instrumentation/grpc-1.30.0/src/test/java/app/TestServer.java new file mode 100644 index 0000000000..c511b977be --- /dev/null +++ b/instrumentation/grpc-1.30.0/src/test/java/app/TestServer.java @@ -0,0 +1,129 @@ +/* + * + * * Copyright 2020 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package app; + +import com.newrelic.agent.bridge.AgentBridge; +import com.newrelic.agent.introspec.InstrumentationTestRunner; +import com.newrelic.api.agent.NewRelic; +import io.grpc.Server; +import io.grpc.ServerBuilder; +import io.grpc.Status; +import io.grpc.examples.helloworld.GreeterGrpc; +import io.grpc.examples.helloworld.HelloReply; +import io.grpc.examples.helloworld.HelloRequest; +import io.grpc.examples.manualflowcontrol.StreamingGreeterGrpc; +import io.grpc.stub.StreamObserver; + +import java.io.IOException; + +public class TestServer { + private Server server; + + private int port; + + public void start() throws IOException { + port = InstrumentationTestRunner.getIntrospector().getRandomPort(); + server = ServerBuilder.forPort(port) + .addService(new GreeterImpl()) + .addService(new StreamingGreeterImpl()) + .intercept(new NewRelicServerInterceptor()) + .build() + .start(); + Runtime.getRuntime().addShutdownHook(new Thread() { + @Override + public void run() { + System.err.println("*** shutting down gRPC server since JVM is shutting down"); + TestServer.this.stop(); + System.err.println("*** server shut down"); + } + }); + } + + public void stop() { + if (server != null) { + server.shutdown(); + } + } + + public void blockUntilShutdown() throws InterruptedException { + if (server != null) { + server.awaitTermination(); + } + } + + public int getPort() { + return port; + } + + static class GreeterImpl extends GreeterGrpc.GreeterImplBase { + + @Override + public void sayHello(HelloRequest req, StreamObserver responseObserver) { + try { + NewRelic.addCustomParameter("sayHelloBefore", req.getName()); + HelloReply reply = HelloReply.newBuilder().setMessage("Hello " + req.getName()).build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + NewRelic.addCustomParameter("sayHelloAfter", req.getName()); + } + catch (Exception e){ + responseObserver.onError(Status.ABORTED.asException()); + } + } + + @Override + public void throwException(HelloRequest req, StreamObserver responseObserver) { + int myNum = 7/0; + } + + @Override + public void throwCaughtException(HelloRequest req, StreamObserver responseObserver) { + try { + int myNum = 7/0; + } + catch (Exception e){ + responseObserver.onError(Status.ABORTED.asException()); + } + } + } + + static class StreamingGreeterImpl extends StreamingGreeterGrpc.StreamingGreeterImplBase { + + @Override + public StreamObserver sayHelloStreaming( + final StreamObserver responseObserver) { + NewRelic.addCustomParameter("customParameter", "true"); + return new StreamObserver() { + @Override + public void onNext(io.grpc.examples.manualflowcontrol.HelloRequest value) { + try { + io.grpc.examples.manualflowcontrol.HelloReply reply = io.grpc.examples.manualflowcontrol.HelloReply + .newBuilder() + .setMessage("Hello " + value.getName()) + .build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (Exception e){ + responseObserver.onError(Status.ABORTED.asException()); + } + } + + @Override + public void onError(Throwable t) { + + } + + @Override + public void onCompleted() { + + } + }; + } + + } +} diff --git a/instrumentation/grpc-1.30.0/src/test/proto/hello_streaming.proto b/instrumentation/grpc-1.30.0/src/test/proto/hello_streaming.proto new file mode 100644 index 0000000000..325b9093b0 --- /dev/null +++ b/instrumentation/grpc-1.30.0/src/test/proto/hello_streaming.proto @@ -0,0 +1,37 @@ +// Copyright 2015 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +syntax = "proto3"; + +option java_multiple_files = true; +option java_package = "io.grpc.examples.manualflowcontrol"; +option java_outer_classname = "HelloStreamingProto"; +option objc_class_prefix = "HLWS"; + +package manualflowcontrol; + +// The greeting service definition. +service StreamingGreeter { + // Streams a many greetings + rpc SayHelloStreaming (stream HelloRequest) returns (stream HelloReply) {} +} + +// The request message containing the user's name. +message HelloRequest { + string name = 1; +} + +// The response message containing the greetings +message HelloReply { + string message = 1; +} diff --git a/instrumentation/grpc-1.30.0/src/test/proto/helloworld.proto b/instrumentation/grpc-1.30.0/src/test/proto/helloworld.proto new file mode 100644 index 0000000000..3e6d215a1a --- /dev/null +++ b/instrumentation/grpc-1.30.0/src/test/proto/helloworld.proto @@ -0,0 +1,39 @@ +// Copyright 2015 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +syntax = "proto3"; + +option java_multiple_files = true; +option java_package = "io.grpc.examples.helloworld"; +option java_outer_classname = "HelloWorldProto"; +option objc_class_prefix = "HLW"; + +package helloworld; + +// The greeting service definition. +service Greeter { + // Sends a greeting + rpc SayHello (HelloRequest) returns (HelloReply) {} + rpc ThrowException (HelloRequest) returns (HelloReply) {} + rpc ThrowCaughtException (HelloRequest) returns (HelloReply) {} +} + +// The request message containing the user's name. +message HelloRequest { + string name = 1; +} + +// The response message containing the greetings +message HelloReply { + string message = 1; +} diff --git a/instrumentation/grpc-1.30.0/src/test/resources/no_grpc_errors.yml b/instrumentation/grpc-1.30.0/src/test/resources/no_grpc_errors.yml new file mode 100644 index 0000000000..1978cece58 --- /dev/null +++ b/instrumentation/grpc-1.30.0/src/test/resources/no_grpc_errors.yml @@ -0,0 +1,2 @@ +common: &default_settings + grpc.errors.enabled: false diff --git a/settings.gradle b/settings.gradle index cc016a7e2c..bce1a66604 100644 --- a/settings.gradle +++ b/settings.gradle @@ -74,6 +74,7 @@ include 'instrumentation:grails-2' include 'instrumentation:grails-async-2.3' include 'instrumentation:grpc-1.4.0' include 'instrumentation:grpc-1.22.0' +include 'instrumentation:grpc-1.30.0' include 'instrumentation:hibernate-3.3' include 'instrumentation:hibernate-3.5' include 'instrumentation:hibernate-4.0'