mirror of https://github.com/grpc/grpc-java.git
add OpenTelemetryTracingModule (#11477)
This commit is contained in:
parent
c63e354883
commit
421e2371e9
|
|
@ -0,0 +1,408 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.grpc.opentelemetry;
|
||||||
|
|
||||||
|
import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
|
import static io.grpc.ClientStreamTracer.NAME_RESOLUTION_DELAYED;
|
||||||
|
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import io.grpc.Attributes;
|
||||||
|
import io.grpc.CallOptions;
|
||||||
|
import io.grpc.Channel;
|
||||||
|
import io.grpc.ClientCall;
|
||||||
|
import io.grpc.ClientInterceptor;
|
||||||
|
import io.grpc.ClientStreamTracer;
|
||||||
|
import io.grpc.ForwardingClientCall.SimpleForwardingClientCall;
|
||||||
|
import io.grpc.ForwardingClientCallListener.SimpleForwardingClientCallListener;
|
||||||
|
import io.grpc.Metadata;
|
||||||
|
import io.grpc.MethodDescriptor;
|
||||||
|
import io.grpc.ServerStreamTracer;
|
||||||
|
import io.opentelemetry.api.OpenTelemetry;
|
||||||
|
import io.opentelemetry.api.common.AttributesBuilder;
|
||||||
|
import io.opentelemetry.api.trace.Span;
|
||||||
|
import io.opentelemetry.api.trace.StatusCode;
|
||||||
|
import io.opentelemetry.api.trace.Tracer;
|
||||||
|
import io.opentelemetry.context.Context;
|
||||||
|
import io.opentelemetry.context.propagation.ContextPropagators;
|
||||||
|
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides factories for {@link io.grpc.StreamTracer} that records tracing to OpenTelemetry.
|
||||||
|
*/
|
||||||
|
final class OpenTelemetryTracingModule {
|
||||||
|
private static final Logger logger = Logger.getLogger(OpenTelemetryTracingModule.class.getName());
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
static final String OTEL_TRACING_SCOPE_NAME = "grpc-java";
|
||||||
|
@Nullable
|
||||||
|
private static final AtomicIntegerFieldUpdater<CallAttemptsTracerFactory> callEndedUpdater;
|
||||||
|
@Nullable
|
||||||
|
private static final AtomicIntegerFieldUpdater<ServerTracer> streamClosedUpdater;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When using Atomic*FieldUpdater, some Samsung Android 5.0.x devices encounter a bug in their JDK
|
||||||
|
* reflection API that triggers a NoSuchFieldException. When this occurs, we fallback to
|
||||||
|
* (potentially racy) direct updates of the volatile variables.
|
||||||
|
*/
|
||||||
|
static {
|
||||||
|
AtomicIntegerFieldUpdater<CallAttemptsTracerFactory> tmpCallEndedUpdater;
|
||||||
|
AtomicIntegerFieldUpdater<ServerTracer> tmpStreamClosedUpdater;
|
||||||
|
try {
|
||||||
|
tmpCallEndedUpdater =
|
||||||
|
AtomicIntegerFieldUpdater.newUpdater(CallAttemptsTracerFactory.class, "callEnded");
|
||||||
|
tmpStreamClosedUpdater =
|
||||||
|
AtomicIntegerFieldUpdater.newUpdater(ServerTracer.class, "streamClosed");
|
||||||
|
} catch (Throwable t) {
|
||||||
|
logger.log(Level.SEVERE, "Creating atomic field updaters failed", t);
|
||||||
|
tmpCallEndedUpdater = null;
|
||||||
|
tmpStreamClosedUpdater = null;
|
||||||
|
}
|
||||||
|
callEndedUpdater = tmpCallEndedUpdater;
|
||||||
|
streamClosedUpdater = tmpStreamClosedUpdater;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Tracer otelTracer;
|
||||||
|
private final ContextPropagators contextPropagators;
|
||||||
|
private final MetadataGetter metadataGetter = MetadataGetter.getInstance();
|
||||||
|
private final MetadataSetter metadataSetter = MetadataSetter.getInstance();
|
||||||
|
private final TracingClientInterceptor clientInterceptor = new TracingClientInterceptor();
|
||||||
|
private final ServerTracerFactory serverTracerFactory = new ServerTracerFactory();
|
||||||
|
|
||||||
|
OpenTelemetryTracingModule(OpenTelemetry openTelemetry) {
|
||||||
|
this.otelTracer = checkNotNull(openTelemetry.getTracer(OTEL_TRACING_SCOPE_NAME), "otelTracer");
|
||||||
|
this.contextPropagators = checkNotNull(openTelemetry.getPropagators(), "contextPropagators");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@link CallAttemptsTracerFactory} for a new call.
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
CallAttemptsTracerFactory newClientCallTracer(Span clientSpan, MethodDescriptor<?, ?> method) {
|
||||||
|
return new CallAttemptsTracerFactory(clientSpan, method);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the server tracer factory.
|
||||||
|
*/
|
||||||
|
ServerStreamTracer.Factory getServerTracerFactory() {
|
||||||
|
return serverTracerFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the client interceptor that facilitates otel tracing reporting.
|
||||||
|
*/
|
||||||
|
ClientInterceptor getClientInterceptor() {
|
||||||
|
return clientInterceptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
final class CallAttemptsTracerFactory extends ClientStreamTracer.Factory {
|
||||||
|
volatile int callEnded;
|
||||||
|
private final Span clientSpan;
|
||||||
|
private final String fullMethodName;
|
||||||
|
|
||||||
|
CallAttemptsTracerFactory(Span clientSpan, MethodDescriptor<?, ?> method) {
|
||||||
|
checkNotNull(method, "method");
|
||||||
|
this.fullMethodName = checkNotNull(method.getFullMethodName(), "fullMethodName");
|
||||||
|
this.clientSpan = checkNotNull(clientSpan, "clientSpan");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClientStreamTracer newClientStreamTracer(
|
||||||
|
ClientStreamTracer.StreamInfo info, Metadata headers) {
|
||||||
|
Span attemptSpan = otelTracer.spanBuilder(
|
||||||
|
"Attempt." + fullMethodName.replace('/', '.'))
|
||||||
|
.setParent(Context.current().with(clientSpan))
|
||||||
|
.startSpan();
|
||||||
|
attemptSpan.setAttribute(
|
||||||
|
"previous-rpc-attempts", info.getPreviousAttempts());
|
||||||
|
attemptSpan.setAttribute(
|
||||||
|
"transparent-retry",info.isTransparentRetry());
|
||||||
|
if (info.getCallOptions().getOption(NAME_RESOLUTION_DELAYED) != null) {
|
||||||
|
clientSpan.addEvent("Delayed name resolution complete");
|
||||||
|
}
|
||||||
|
return new ClientTracer(attemptSpan, clientSpan);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Record a finished call and mark the current time as the end time.
|
||||||
|
*
|
||||||
|
* <p>Can be called from any thread without synchronization. Calling it the second time or more
|
||||||
|
* is a no-op.
|
||||||
|
*/
|
||||||
|
void callEnded(io.grpc.Status status) {
|
||||||
|
if (callEndedUpdater != null) {
|
||||||
|
if (callEndedUpdater.getAndSet(this, 1) != 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (callEnded != 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
callEnded = 1;
|
||||||
|
}
|
||||||
|
endSpanWithStatus(clientSpan, status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class ClientTracer extends ClientStreamTracer {
|
||||||
|
private final Span span;
|
||||||
|
private final Span parentSpan;
|
||||||
|
volatile int seqNo;
|
||||||
|
boolean isPendingStream;
|
||||||
|
|
||||||
|
ClientTracer(Span span, Span parentSpan) {
|
||||||
|
this.span = checkNotNull(span, "span");
|
||||||
|
this.parentSpan = checkNotNull(parentSpan, "parent span");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void streamCreated(Attributes transportAtts, Metadata headers) {
|
||||||
|
contextPropagators.getTextMapPropagator().inject(Context.current().with(span), headers,
|
||||||
|
metadataSetter);
|
||||||
|
if (isPendingStream) {
|
||||||
|
span.addEvent("Delayed LB pick complete");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void createPendingStream() {
|
||||||
|
isPendingStream = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void outboundMessageSent(
|
||||||
|
int seqNo, long optionalWireSize, long optionalUncompressedSize) {
|
||||||
|
recordOutboundMessageSentEvent(span, seqNo, optionalWireSize, optionalUncompressedSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void inboundMessageRead(
|
||||||
|
int seqNo, long optionalWireSize, long optionalUncompressedSize) {
|
||||||
|
//TODO(yifeizhuang): needs support from message deframer.
|
||||||
|
if (optionalWireSize != optionalUncompressedSize) {
|
||||||
|
recordInboundCompressedMessage(span, seqNo, optionalWireSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void inboundMessage(int seqNo) {
|
||||||
|
this.seqNo = seqNo;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void inboundUncompressedSize(long bytes) {
|
||||||
|
recordInboundMessageSize(parentSpan, seqNo, bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void streamClosed(io.grpc.Status status) {
|
||||||
|
endSpanWithStatus(span, status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class ServerTracer extends ServerStreamTracer {
|
||||||
|
private final Span span;
|
||||||
|
volatile int streamClosed;
|
||||||
|
private int seqNo;
|
||||||
|
|
||||||
|
ServerTracer(String fullMethodName, @Nullable Span remoteSpan) {
|
||||||
|
checkNotNull(fullMethodName, "fullMethodName");
|
||||||
|
this.span =
|
||||||
|
otelTracer.spanBuilder(generateTraceSpanName(true, fullMethodName))
|
||||||
|
.setParent(remoteSpan == null ? null : Context.current().with(remoteSpan))
|
||||||
|
.startSpan();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Record a finished stream and mark the current time as the end time.
|
||||||
|
*
|
||||||
|
* <p>Can be called from any thread without synchronization. Calling it the second time or more
|
||||||
|
* is a no-op.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void streamClosed(io.grpc.Status status) {
|
||||||
|
if (streamClosedUpdater != null) {
|
||||||
|
if (streamClosedUpdater.getAndSet(this, 1) != 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (streamClosed != 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
streamClosed = 1;
|
||||||
|
}
|
||||||
|
endSpanWithStatus(span, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void outboundMessageSent(
|
||||||
|
int seqNo, long optionalWireSize, long optionalUncompressedSize) {
|
||||||
|
recordOutboundMessageSentEvent(span, seqNo, optionalWireSize, optionalUncompressedSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void inboundMessageRead(
|
||||||
|
int seqNo, long optionalWireSize, long optionalUncompressedSize) {
|
||||||
|
if (optionalWireSize != optionalUncompressedSize) {
|
||||||
|
recordInboundCompressedMessage(span, seqNo, optionalWireSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void inboundMessage(int seqNo) {
|
||||||
|
this.seqNo = seqNo;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void inboundUncompressedSize(long bytes) {
|
||||||
|
recordInboundMessageSize(span, seqNo, bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
final class ServerTracerFactory extends ServerStreamTracer.Factory {
|
||||||
|
@SuppressWarnings("ReferenceEquality")
|
||||||
|
@Override
|
||||||
|
public ServerStreamTracer newServerStreamTracer(String fullMethodName, Metadata headers) {
|
||||||
|
Context context = contextPropagators.getTextMapPropagator().extract(
|
||||||
|
Context.current(), headers, metadataGetter
|
||||||
|
);
|
||||||
|
Span remoteSpan = Span.fromContext(context);
|
||||||
|
if (remoteSpan == Span.getInvalid()) {
|
||||||
|
remoteSpan = null;
|
||||||
|
}
|
||||||
|
return new ServerTracer(fullMethodName, remoteSpan);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
final class TracingClientInterceptor implements ClientInterceptor {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
|
||||||
|
MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {
|
||||||
|
Span clientSpan = otelTracer.spanBuilder(
|
||||||
|
generateTraceSpanName(false, method.getFullMethodName()))
|
||||||
|
.startSpan();
|
||||||
|
|
||||||
|
final CallAttemptsTracerFactory tracerFactory = newClientCallTracer(clientSpan, method);
|
||||||
|
ClientCall<ReqT, RespT> call =
|
||||||
|
next.newCall(
|
||||||
|
method,
|
||||||
|
callOptions.withStreamTracerFactory(tracerFactory));
|
||||||
|
return new SimpleForwardingClientCall<ReqT, RespT>(call) {
|
||||||
|
@Override
|
||||||
|
public void start(Listener<RespT> responseListener, Metadata headers) {
|
||||||
|
delegate().start(
|
||||||
|
new SimpleForwardingClientCallListener<RespT>(responseListener) {
|
||||||
|
@Override
|
||||||
|
public void onClose(io.grpc.Status status, Metadata trailers) {
|
||||||
|
tracerFactory.callEnded(status);
|
||||||
|
super.onClose(status, trailers);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
headers);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attribute named "message-size" always means the message size the application sees.
|
||||||
|
// If there was compression, additional event reports "message-size-compressed".
|
||||||
|
//
|
||||||
|
// An example trace with message compression:
|
||||||
|
//
|
||||||
|
// Sending:
|
||||||
|
// |-- Event 'Outbound message sent', attributes('sequence-numer' = 0, 'message-size' = 7854,
|
||||||
|
// 'message-size-compressed' = 5493) ----|
|
||||||
|
//
|
||||||
|
// Receiving:
|
||||||
|
// |-- Event 'Inbound compressed message', attributes('sequence-numer' = 0,
|
||||||
|
// 'message-size-compressed' = 5493 ) ----|
|
||||||
|
// |-- Event 'Inbound message received', attributes('sequence-numer' = 0,
|
||||||
|
// 'message-size' = 7854) ----|
|
||||||
|
//
|
||||||
|
// An example trace with no message compression:
|
||||||
|
//
|
||||||
|
// Sending:
|
||||||
|
// |-- Event 'Outbound message sent', attributes('sequence-numer' = 0, 'message-size' = 7854) ---|
|
||||||
|
//
|
||||||
|
// Receiving:
|
||||||
|
// |-- Event 'Inbound message received', attributes('sequence-numer' = 0,
|
||||||
|
// 'message-size' = 7854) ----|
|
||||||
|
private void recordOutboundMessageSentEvent(Span span,
|
||||||
|
int seqNo, long optionalWireSize, long optionalUncompressedSize) {
|
||||||
|
AttributesBuilder attributesBuilder = io.opentelemetry.api.common.Attributes.builder();
|
||||||
|
attributesBuilder.put("sequence-number", seqNo);
|
||||||
|
if (optionalUncompressedSize != -1) {
|
||||||
|
attributesBuilder.put("message-size", optionalUncompressedSize);
|
||||||
|
}
|
||||||
|
if (optionalWireSize != -1 && optionalWireSize != optionalUncompressedSize) {
|
||||||
|
attributesBuilder.put("message-size-compressed", optionalWireSize);
|
||||||
|
}
|
||||||
|
span.addEvent("Outbound message sent", attributesBuilder.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void recordInboundCompressedMessage(Span span, int seqNo, long optionalWireSize) {
|
||||||
|
AttributesBuilder attributesBuilder = io.opentelemetry.api.common.Attributes.builder();
|
||||||
|
attributesBuilder.put("sequence-number", seqNo);
|
||||||
|
attributesBuilder.put("message-size-compressed", optionalWireSize);
|
||||||
|
span.addEvent("Inbound compressed message", attributesBuilder.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void recordInboundMessageSize(Span span, int seqNo, long bytes) {
|
||||||
|
AttributesBuilder attributesBuilder = io.opentelemetry.api.common.Attributes.builder();
|
||||||
|
attributesBuilder.put("sequence-number", seqNo);
|
||||||
|
attributesBuilder.put("message-size", bytes);
|
||||||
|
span.addEvent("Inbound message received", attributesBuilder.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String generateErrorStatusDescription(io.grpc.Status status) {
|
||||||
|
if (status.getDescription() != null) {
|
||||||
|
return status.getCode() + ": " + status.getDescription();
|
||||||
|
} else {
|
||||||
|
return status.getCode().toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void endSpanWithStatus(Span span, io.grpc.Status status) {
|
||||||
|
if (status.isOk()) {
|
||||||
|
span.setStatus(StatusCode.OK);
|
||||||
|
} else {
|
||||||
|
span.setStatus(StatusCode.ERROR, generateErrorStatusDescription(status));
|
||||||
|
}
|
||||||
|
span.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a full method name to a tracing span name.
|
||||||
|
*
|
||||||
|
* @param isServer {@code false} if the span is on the client-side, {@code true} if on the
|
||||||
|
* server-side
|
||||||
|
* @param fullMethodName the method name as returned by
|
||||||
|
* {@link MethodDescriptor#getFullMethodName}.
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
static String generateTraceSpanName(boolean isServer, String fullMethodName) {
|
||||||
|
String prefix = isServer ? "Recv" : "Sent";
|
||||||
|
return prefix + "." + fullMethodName.replace('/', '.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,582 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.grpc.opentelemetry;
|
||||||
|
|
||||||
|
import static io.grpc.ClientStreamTracer.NAME_RESOLUTION_DELAYED;
|
||||||
|
import static io.grpc.opentelemetry.OpenTelemetryTracingModule.OTEL_TRACING_SCOPE_NAME;
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.inOrder;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableSet;
|
||||||
|
import io.grpc.Attributes;
|
||||||
|
import io.grpc.CallOptions;
|
||||||
|
import io.grpc.Channel;
|
||||||
|
import io.grpc.ClientCall;
|
||||||
|
import io.grpc.ClientInterceptor;
|
||||||
|
import io.grpc.ClientInterceptors;
|
||||||
|
import io.grpc.ClientStreamTracer;
|
||||||
|
import io.grpc.Metadata;
|
||||||
|
import io.grpc.MethodDescriptor;
|
||||||
|
import io.grpc.ServerCall;
|
||||||
|
import io.grpc.ServerCallHandler;
|
||||||
|
import io.grpc.ServerServiceDefinition;
|
||||||
|
import io.grpc.ServerStreamTracer;
|
||||||
|
import io.grpc.Status;
|
||||||
|
import io.grpc.opentelemetry.OpenTelemetryTracingModule.CallAttemptsTracerFactory;
|
||||||
|
import io.grpc.testing.GrpcServerRule;
|
||||||
|
import io.opentelemetry.api.OpenTelemetry;
|
||||||
|
import io.opentelemetry.api.trace.Span;
|
||||||
|
import io.opentelemetry.api.trace.SpanBuilder;
|
||||||
|
import io.opentelemetry.api.trace.SpanId;
|
||||||
|
import io.opentelemetry.api.trace.StatusCode;
|
||||||
|
import io.opentelemetry.api.trace.TraceId;
|
||||||
|
import io.opentelemetry.api.trace.Tracer;
|
||||||
|
import io.opentelemetry.context.Context;
|
||||||
|
import io.opentelemetry.context.Scope;
|
||||||
|
import io.opentelemetry.context.propagation.ContextPropagators;
|
||||||
|
import io.opentelemetry.context.propagation.TextMapPropagator;
|
||||||
|
import io.opentelemetry.sdk.testing.junit4.OpenTelemetryRule;
|
||||||
|
import io.opentelemetry.sdk.trace.data.EventData;
|
||||||
|
import io.opentelemetry.sdk.trace.data.SpanData;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.junit.runners.JUnit4;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.Captor;
|
||||||
|
import org.mockito.InOrder;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.MockitoJUnit;
|
||||||
|
import org.mockito.junit.MockitoRule;
|
||||||
|
|
||||||
|
@RunWith(JUnit4.class)
|
||||||
|
public class OpenTelemetryTracingModuleTest {
|
||||||
|
@Rule
|
||||||
|
public final MockitoRule mocks = MockitoJUnit.rule();
|
||||||
|
|
||||||
|
private static final ClientStreamTracer.StreamInfo STREAM_INFO =
|
||||||
|
ClientStreamTracer.StreamInfo.newBuilder()
|
||||||
|
.setCallOptions(CallOptions.DEFAULT.withOption(NAME_RESOLUTION_DELAYED, 10L)).build();
|
||||||
|
private static final CallOptions.Key<String> CUSTOM_OPTION =
|
||||||
|
CallOptions.Key.createWithDefault("option1", "default");
|
||||||
|
private static final CallOptions CALL_OPTIONS =
|
||||||
|
CallOptions.DEFAULT.withOption(CUSTOM_OPTION, "customvalue");
|
||||||
|
|
||||||
|
private static class StringInputStream extends InputStream {
|
||||||
|
final String string;
|
||||||
|
|
||||||
|
StringInputStream(String string) {
|
||||||
|
this.string = string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read() {
|
||||||
|
// InProcessTransport doesn't actually read bytes from the InputStream. The InputStream is
|
||||||
|
// passed to the InProcess server and consumed by MARSHALLER.parse().
|
||||||
|
throw new UnsupportedOperationException("Should not be called");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final MethodDescriptor.Marshaller<String> MARSHALLER =
|
||||||
|
new MethodDescriptor.Marshaller<String>() {
|
||||||
|
@Override
|
||||||
|
public InputStream stream(String value) {
|
||||||
|
return new StringInputStream(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String parse(InputStream stream) {
|
||||||
|
return ((StringInputStream) stream).string;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private final MethodDescriptor<String, String> method =
|
||||||
|
MethodDescriptor.<String, String>newBuilder()
|
||||||
|
.setType(MethodDescriptor.MethodType.UNKNOWN)
|
||||||
|
.setRequestMarshaller(MARSHALLER)
|
||||||
|
.setResponseMarshaller(MARSHALLER)
|
||||||
|
.setFullMethodName("package1.service2/method3")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public final OpenTelemetryRule openTelemetryRule = OpenTelemetryRule.create();
|
||||||
|
@Rule
|
||||||
|
public final GrpcServerRule grpcServerRule = new GrpcServerRule().directExecutor();
|
||||||
|
private Tracer tracerRule;
|
||||||
|
@Mock
|
||||||
|
private Tracer mockTracer;
|
||||||
|
@Mock
|
||||||
|
TextMapPropagator mockPropagator;
|
||||||
|
@Mock
|
||||||
|
private Span mockClientSpan;
|
||||||
|
@Mock
|
||||||
|
private Span mockAttemptSpan;
|
||||||
|
@Mock
|
||||||
|
private ServerCall.Listener<String> mockServerCallListener;
|
||||||
|
@Mock
|
||||||
|
private ClientCall.Listener<String> mockClientCallListener;
|
||||||
|
@Mock
|
||||||
|
private SpanBuilder mockSpanBuilder;
|
||||||
|
@Mock
|
||||||
|
private OpenTelemetry mockOpenTelemetry;
|
||||||
|
@Captor
|
||||||
|
private ArgumentCaptor<String> eventNameCaptor;
|
||||||
|
@Captor
|
||||||
|
private ArgumentCaptor<io.opentelemetry.api.common.Attributes> attributesCaptor;
|
||||||
|
@Captor
|
||||||
|
private ArgumentCaptor<Status> statusCaptor;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() {
|
||||||
|
tracerRule = openTelemetryRule.getOpenTelemetry().getTracer(OTEL_TRACING_SCOPE_NAME);
|
||||||
|
when(mockOpenTelemetry.getTracer(OTEL_TRACING_SCOPE_NAME)).thenReturn(mockTracer);
|
||||||
|
when(mockOpenTelemetry.getPropagators()).thenReturn(ContextPropagators.create(mockPropagator));
|
||||||
|
when(mockSpanBuilder.startSpan()).thenReturn(mockAttemptSpan);
|
||||||
|
when(mockSpanBuilder.setParent(any())).thenReturn(mockSpanBuilder);
|
||||||
|
when(mockTracer.spanBuilder(any())).thenReturn(mockSpanBuilder);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use mock instead of OpenTelemetryRule to verify inOrder and propagator.
|
||||||
|
@Test
|
||||||
|
public void clientBasicTracingMocking() {
|
||||||
|
OpenTelemetryTracingModule tracingModule = new OpenTelemetryTracingModule(mockOpenTelemetry);
|
||||||
|
CallAttemptsTracerFactory callTracer =
|
||||||
|
tracingModule.newClientCallTracer(mockClientSpan, method);
|
||||||
|
Metadata headers = new Metadata();
|
||||||
|
ClientStreamTracer clientStreamTracer = callTracer.newClientStreamTracer(STREAM_INFO, headers);
|
||||||
|
clientStreamTracer.createPendingStream();
|
||||||
|
clientStreamTracer.streamCreated(Attributes.EMPTY, headers);
|
||||||
|
|
||||||
|
verify(mockTracer).spanBuilder(eq("Attempt.package1.service2.method3"));
|
||||||
|
verify(mockPropagator).inject(any(), eq(headers), eq(MetadataSetter.getInstance()));
|
||||||
|
verify(mockClientSpan, never()).end();
|
||||||
|
verify(mockAttemptSpan, never()).end();
|
||||||
|
|
||||||
|
clientStreamTracer.outboundMessage(0);
|
||||||
|
clientStreamTracer.outboundMessageSent(0, 882, -1);
|
||||||
|
clientStreamTracer.inboundMessage(0);
|
||||||
|
clientStreamTracer.outboundMessage(1);
|
||||||
|
clientStreamTracer.outboundMessageSent(1, -1, 27);
|
||||||
|
clientStreamTracer.inboundMessageRead(0, 255, 90);
|
||||||
|
|
||||||
|
clientStreamTracer.streamClosed(Status.OK);
|
||||||
|
callTracer.callEnded(Status.OK);
|
||||||
|
|
||||||
|
InOrder inOrder = inOrder(mockClientSpan, mockAttemptSpan);
|
||||||
|
inOrder.verify(mockAttemptSpan)
|
||||||
|
.setAttribute("previous-rpc-attempts", 0);
|
||||||
|
inOrder.verify(mockAttemptSpan)
|
||||||
|
.setAttribute("transparent-retry", false);
|
||||||
|
inOrder.verify(mockClientSpan).addEvent("Delayed name resolution complete");
|
||||||
|
inOrder.verify(mockAttemptSpan).addEvent("Delayed LB pick complete");
|
||||||
|
inOrder.verify(mockAttemptSpan, times(3)).addEvent(
|
||||||
|
eventNameCaptor.capture(), attributesCaptor.capture()
|
||||||
|
);
|
||||||
|
List<String> events = eventNameCaptor.getAllValues();
|
||||||
|
List<io.opentelemetry.api.common.Attributes> attributes = attributesCaptor.getAllValues();
|
||||||
|
assertEquals(
|
||||||
|
"Outbound message sent" ,
|
||||||
|
events.get(0));
|
||||||
|
assertEquals(
|
||||||
|
io.opentelemetry.api.common.Attributes.builder()
|
||||||
|
.put("sequence-number", 0)
|
||||||
|
.put("message-size-compressed", 882)
|
||||||
|
.build(),
|
||||||
|
attributes.get(0));
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
"Outbound message sent" ,
|
||||||
|
events.get(1));
|
||||||
|
assertEquals(
|
||||||
|
io.opentelemetry.api.common.Attributes.builder()
|
||||||
|
.put("sequence-number", 1)
|
||||||
|
.put("message-size", 27)
|
||||||
|
.build(),
|
||||||
|
attributes.get(1));
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
"Inbound compressed message" ,
|
||||||
|
events.get(2));
|
||||||
|
assertEquals(
|
||||||
|
io.opentelemetry.api.common.Attributes.builder()
|
||||||
|
.put("sequence-number", 0)
|
||||||
|
.put("message-size-compressed", 255)
|
||||||
|
.build(),
|
||||||
|
attributes.get(2));
|
||||||
|
|
||||||
|
inOrder.verify(mockAttemptSpan).setStatus(StatusCode.OK);
|
||||||
|
inOrder.verify(mockAttemptSpan).end();
|
||||||
|
inOrder.verify(mockClientSpan).setStatus(StatusCode.OK);
|
||||||
|
inOrder.verify(mockClientSpan).end();
|
||||||
|
inOrder.verifyNoMoreInteractions();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void clientBasicTracingRule() {
|
||||||
|
OpenTelemetryTracingModule tracingModule = new OpenTelemetryTracingModule(
|
||||||
|
openTelemetryRule.getOpenTelemetry());
|
||||||
|
Span clientSpan = tracerRule.spanBuilder("test-client-span").startSpan();
|
||||||
|
CallAttemptsTracerFactory callTracer =
|
||||||
|
tracingModule.newClientCallTracer(clientSpan, method);
|
||||||
|
Metadata headers = new Metadata();
|
||||||
|
ClientStreamTracer clientStreamTracer = callTracer.newClientStreamTracer(STREAM_INFO, headers);
|
||||||
|
clientStreamTracer.createPendingStream();
|
||||||
|
clientStreamTracer.streamCreated(Attributes.EMPTY, headers);
|
||||||
|
clientStreamTracer.outboundMessage(0);
|
||||||
|
clientStreamTracer.outboundMessageSent(0, 882, -1);
|
||||||
|
clientStreamTracer.inboundMessage(0);
|
||||||
|
clientStreamTracer.outboundMessage(1);
|
||||||
|
clientStreamTracer.outboundMessageSent(1, -1, 27);
|
||||||
|
clientStreamTracer.inboundMessageRead(0, 255, -1);
|
||||||
|
clientStreamTracer.inboundUncompressedSize(288);
|
||||||
|
clientStreamTracer.inboundMessageRead(1, 128, 128);
|
||||||
|
clientStreamTracer.inboundMessage(1);
|
||||||
|
clientStreamTracer.inboundUncompressedSize(128);
|
||||||
|
|
||||||
|
clientStreamTracer.streamClosed(Status.OK);
|
||||||
|
callTracer.callEnded(Status.OK);
|
||||||
|
|
||||||
|
List<SpanData> spans = openTelemetryRule.getSpans();
|
||||||
|
assertEquals(spans.size(), 2);
|
||||||
|
SpanData attemptSpanData = spans.get(0);
|
||||||
|
SpanData clientSpanData = spans.get(1);
|
||||||
|
assertEquals(attemptSpanData.getName(), "Attempt.package1.service2.method3");
|
||||||
|
assertEquals(clientSpanData.getName(), "test-client-span");
|
||||||
|
assertEquals(headers.keys(), ImmutableSet.of("traceparent"));
|
||||||
|
String spanContext = headers.get(
|
||||||
|
Metadata.Key.of("traceparent", Metadata.ASCII_STRING_MARSHALLER));
|
||||||
|
assertEquals(spanContext.substring(3, 3 + TraceId.getLength()),
|
||||||
|
spans.get(1).getSpanContext().getTraceId());
|
||||||
|
|
||||||
|
// parent(client) span data
|
||||||
|
List<EventData> clientSpanEvents = clientSpanData.getEvents();
|
||||||
|
assertEquals(clientSpanEvents.size(), 3);
|
||||||
|
assertEquals(
|
||||||
|
"Delayed name resolution complete",
|
||||||
|
clientSpanEvents.get(0).getName());
|
||||||
|
assertTrue(clientSpanEvents.get(0).getAttributes().isEmpty());
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
"Inbound message received" ,
|
||||||
|
clientSpanEvents.get(1).getName());
|
||||||
|
assertEquals(
|
||||||
|
io.opentelemetry.api.common.Attributes.builder()
|
||||||
|
.put("sequence-number", 0)
|
||||||
|
.put("message-size", 288)
|
||||||
|
.build(),
|
||||||
|
clientSpanEvents.get(1).getAttributes());
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
"Inbound message received" ,
|
||||||
|
clientSpanEvents.get(2).getName());
|
||||||
|
assertEquals(
|
||||||
|
io.opentelemetry.api.common.Attributes.builder()
|
||||||
|
.put("sequence-number", 1)
|
||||||
|
.put("message-size", 128)
|
||||||
|
.build(),
|
||||||
|
clientSpanEvents.get(2).getAttributes());
|
||||||
|
assertEquals(clientSpanData.hasEnded(), true);
|
||||||
|
|
||||||
|
// child(attempt) span data
|
||||||
|
List<EventData> attemptSpanEvents = attemptSpanData.getEvents();
|
||||||
|
assertEquals(clientSpanEvents.size(), 3);
|
||||||
|
assertEquals(
|
||||||
|
"Delayed LB pick complete",
|
||||||
|
attemptSpanEvents.get(0).getName());
|
||||||
|
assertTrue(clientSpanEvents.get(0).getAttributes().isEmpty());
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
"Outbound message sent" ,
|
||||||
|
attemptSpanEvents.get(1).getName());
|
||||||
|
assertEquals(
|
||||||
|
io.opentelemetry.api.common.Attributes.builder()
|
||||||
|
.put("sequence-number", 0)
|
||||||
|
.put("message-size-compressed", 882)
|
||||||
|
.build(),
|
||||||
|
attemptSpanEvents.get(1).getAttributes());
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
"Outbound message sent" ,
|
||||||
|
attemptSpanEvents.get(2).getName());
|
||||||
|
assertEquals(
|
||||||
|
io.opentelemetry.api.common.Attributes.builder()
|
||||||
|
.put("sequence-number", 1)
|
||||||
|
.put("message-size", 27)
|
||||||
|
.build(),
|
||||||
|
attemptSpanEvents.get(2).getAttributes());
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
"Inbound compressed message" ,
|
||||||
|
attemptSpanEvents.get(3).getName());
|
||||||
|
assertEquals(
|
||||||
|
io.opentelemetry.api.common.Attributes.builder()
|
||||||
|
.put("sequence-number", 0)
|
||||||
|
.put("message-size-compressed", 255)
|
||||||
|
.build(),
|
||||||
|
attemptSpanEvents.get(3).getAttributes());
|
||||||
|
|
||||||
|
assertEquals(attemptSpanData.hasEnded(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void clientInterceptor() {
|
||||||
|
testClientInterceptors(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void clientInterceptorNonDefaultOtelContext() {
|
||||||
|
testClientInterceptors(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void testClientInterceptors(boolean nonDefaultOtelContext) {
|
||||||
|
final AtomicReference<Metadata> capturedMetadata = new AtomicReference<>();
|
||||||
|
grpcServerRule.getServiceRegistry().addService(
|
||||||
|
ServerServiceDefinition.builder("package1.service2").addMethod(
|
||||||
|
method, new ServerCallHandler<String, String>() {
|
||||||
|
@Override
|
||||||
|
public ServerCall.Listener<String> startCall(
|
||||||
|
ServerCall<String, String> call, Metadata headers) {
|
||||||
|
capturedMetadata.set(headers);
|
||||||
|
call.sendHeaders(new Metadata());
|
||||||
|
call.sendMessage("Hello");
|
||||||
|
call.close(
|
||||||
|
Status.PERMISSION_DENIED.withDescription("No you don't"), new Metadata());
|
||||||
|
return mockServerCallListener;
|
||||||
|
}
|
||||||
|
}).build());
|
||||||
|
|
||||||
|
final AtomicReference<CallOptions> capturedCallOptions = new AtomicReference<>();
|
||||||
|
ClientInterceptor callOptionsCaptureInterceptor = new ClientInterceptor() {
|
||||||
|
@Override
|
||||||
|
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
|
||||||
|
MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {
|
||||||
|
capturedCallOptions.set(callOptions);
|
||||||
|
return next.newCall(method, callOptions);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
OpenTelemetryTracingModule tracingModule = new OpenTelemetryTracingModule(
|
||||||
|
openTelemetryRule.getOpenTelemetry());
|
||||||
|
Channel interceptedChannel =
|
||||||
|
ClientInterceptors.intercept(
|
||||||
|
grpcServerRule.getChannel(), callOptionsCaptureInterceptor,
|
||||||
|
tracingModule.getClientInterceptor());
|
||||||
|
Span parentSpan = tracerRule.spanBuilder("test-parent-span").startSpan();
|
||||||
|
ClientCall<String, String> call;
|
||||||
|
|
||||||
|
if (nonDefaultOtelContext) {
|
||||||
|
try (Scope scope = io.opentelemetry.context.Context.current().with(parentSpan)
|
||||||
|
.makeCurrent()) {
|
||||||
|
call = interceptedChannel.newCall(method, CALL_OPTIONS);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
call = interceptedChannel.newCall(method, CALL_OPTIONS);
|
||||||
|
}
|
||||||
|
assertEquals("customvalue", capturedCallOptions.get().getOption(CUSTOM_OPTION));
|
||||||
|
assertEquals(1, capturedCallOptions.get().getStreamTracerFactories().size());
|
||||||
|
assertTrue(
|
||||||
|
capturedCallOptions.get().getStreamTracerFactories().get(0)
|
||||||
|
instanceof CallAttemptsTracerFactory);
|
||||||
|
|
||||||
|
// Make the call
|
||||||
|
Metadata headers = new Metadata();
|
||||||
|
call.start(mockClientCallListener, headers);
|
||||||
|
|
||||||
|
// End the call
|
||||||
|
call.halfClose();
|
||||||
|
call.request(1);
|
||||||
|
parentSpan.end();
|
||||||
|
|
||||||
|
verify(mockClientCallListener).onClose(statusCaptor.capture(), any(Metadata.class));
|
||||||
|
Status status = statusCaptor.getValue();
|
||||||
|
assertEquals(Status.Code.PERMISSION_DENIED, status.getCode());
|
||||||
|
assertEquals("No you don't", status.getDescription());
|
||||||
|
|
||||||
|
List<SpanData> spans = openTelemetryRule.getSpans();
|
||||||
|
assertEquals(spans.size(), 3);
|
||||||
|
|
||||||
|
SpanData clientSpan = spans.get(1);
|
||||||
|
SpanData attemptSpan = spans.get(0);
|
||||||
|
if (nonDefaultOtelContext) {
|
||||||
|
assertEquals(clientSpan.getParentSpanContext(), parentSpan.getSpanContext());
|
||||||
|
} else {
|
||||||
|
assertEquals(clientSpan.getParentSpanContext(),
|
||||||
|
Span.fromContext(Context.root()).getSpanContext());
|
||||||
|
}
|
||||||
|
String spanContext = capturedMetadata.get().get(
|
||||||
|
Metadata.Key.of("traceparent", Metadata.ASCII_STRING_MARSHALLER));
|
||||||
|
// W3C format: 00-<trace id>-<span id>-<trace flag>
|
||||||
|
assertEquals(spanContext.substring(3, 3 + TraceId.getLength()),
|
||||||
|
attemptSpan.getSpanContext().getTraceId());
|
||||||
|
assertEquals(spanContext.substring(3 + TraceId.getLength() + 1,
|
||||||
|
3 + TraceId.getLength() + 1 + SpanId.getLength()),
|
||||||
|
attemptSpan.getSpanContext().getSpanId());
|
||||||
|
|
||||||
|
assertEquals(attemptSpan.getParentSpanContext(), clientSpan.getSpanContext());
|
||||||
|
assertTrue(clientSpan.hasEnded());
|
||||||
|
assertEquals(clientSpan.getStatus().getStatusCode(), StatusCode.ERROR);
|
||||||
|
assertEquals(clientSpan.getStatus().getDescription(), "PERMISSION_DENIED: No you don't");
|
||||||
|
assertTrue(attemptSpan.hasEnded());
|
||||||
|
assertTrue(attemptSpan.hasEnded());
|
||||||
|
assertEquals(attemptSpan.getStatus().getStatusCode(), StatusCode.ERROR);
|
||||||
|
assertEquals(attemptSpan.getStatus().getDescription(), "PERMISSION_DENIED: No you don't");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void clientStreamNeverCreatedStillRecordTracing() {
|
||||||
|
OpenTelemetryTracingModule tracingModule = new OpenTelemetryTracingModule(mockOpenTelemetry);
|
||||||
|
CallAttemptsTracerFactory callTracer =
|
||||||
|
tracingModule.newClientCallTracer(mockClientSpan, method);
|
||||||
|
|
||||||
|
callTracer.callEnded(Status.DEADLINE_EXCEEDED.withDescription("3 seconds"));
|
||||||
|
verify(mockClientSpan).end();
|
||||||
|
verify(mockClientSpan).setStatus(eq(StatusCode.ERROR),
|
||||||
|
eq("DEADLINE_EXCEEDED: 3 seconds"));
|
||||||
|
verifyNoMoreInteractions(mockClientSpan);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void serverBasicTracingNoHeaders() {
|
||||||
|
OpenTelemetryTracingModule tracingModule = new OpenTelemetryTracingModule(
|
||||||
|
openTelemetryRule.getOpenTelemetry());
|
||||||
|
ServerStreamTracer.Factory tracerFactory = tracingModule.getServerTracerFactory();
|
||||||
|
ServerStreamTracer serverStreamTracer =
|
||||||
|
tracerFactory.newServerStreamTracer(method.getFullMethodName(), new Metadata());
|
||||||
|
assertSame(Span.fromContext(Context.current()), Span.getInvalid());
|
||||||
|
|
||||||
|
serverStreamTracer.outboundMessage(0);
|
||||||
|
serverStreamTracer.outboundMessageSent(0, 882, 998);
|
||||||
|
serverStreamTracer.inboundMessage(0);
|
||||||
|
serverStreamTracer.outboundMessage(1);
|
||||||
|
serverStreamTracer.outboundMessageSent(1, -1, 27);
|
||||||
|
serverStreamTracer.inboundMessageRead(0, 90, -1);
|
||||||
|
serverStreamTracer.inboundUncompressedSize(255);
|
||||||
|
|
||||||
|
serverStreamTracer.streamClosed(Status.CANCELLED);
|
||||||
|
|
||||||
|
List<SpanData> spans = openTelemetryRule.getSpans();
|
||||||
|
assertEquals(spans.size(), 1);
|
||||||
|
assertEquals(spans.get(0).getName(), "Recv.package1.service2.method3");
|
||||||
|
assertEquals(spans.get(0).getParentSpanContext(), Span.getInvalid().getSpanContext());
|
||||||
|
|
||||||
|
List<EventData> events = spans.get(0).getEvents();
|
||||||
|
assertEquals(events.size(), 4);
|
||||||
|
assertEquals(
|
||||||
|
"Outbound message sent" ,
|
||||||
|
events.get(0).getName());
|
||||||
|
assertEquals(
|
||||||
|
io.opentelemetry.api.common.Attributes.builder()
|
||||||
|
.put("sequence-number", 0)
|
||||||
|
.put("message-size-compressed", 882)
|
||||||
|
.put("message-size", 998)
|
||||||
|
.build(),
|
||||||
|
events.get(0).getAttributes());
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
"Outbound message sent" ,
|
||||||
|
events.get(1).getName());
|
||||||
|
assertEquals(
|
||||||
|
io.opentelemetry.api.common.Attributes.builder()
|
||||||
|
.put("sequence-number", 1)
|
||||||
|
.put("message-size", 27)
|
||||||
|
.build(),
|
||||||
|
events.get(1).getAttributes());
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
"Inbound compressed message" ,
|
||||||
|
events.get(2).getName());
|
||||||
|
assertEquals(
|
||||||
|
io.opentelemetry.api.common.Attributes.builder()
|
||||||
|
.put("sequence-number", 0)
|
||||||
|
.put("message-size-compressed", 90)
|
||||||
|
.build(),
|
||||||
|
events.get(2).getAttributes());
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
"Inbound message received" ,
|
||||||
|
events.get(3).getName());
|
||||||
|
assertEquals(
|
||||||
|
io.opentelemetry.api.common.Attributes.builder()
|
||||||
|
.put("sequence-number", 0)
|
||||||
|
.put("message-size", 255)
|
||||||
|
.build(),
|
||||||
|
events.get(3).getAttributes());
|
||||||
|
|
||||||
|
assertEquals(spans.get(0).hasEnded(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void grpcTraceBinPropagator() {
|
||||||
|
when(mockOpenTelemetry.getPropagators()).thenReturn(
|
||||||
|
ContextPropagators.create(GrpcTraceBinContextPropagator.defaultInstance()));
|
||||||
|
ArgumentCaptor<Context> contextArgumentCaptor = ArgumentCaptor.forClass(Context.class);
|
||||||
|
OpenTelemetryTracingModule tracingModule = new OpenTelemetryTracingModule(mockOpenTelemetry);
|
||||||
|
Span testClientSpan = tracerRule.spanBuilder("test-client-span").startSpan();
|
||||||
|
CallAttemptsTracerFactory callTracer =
|
||||||
|
tracingModule.newClientCallTracer(testClientSpan, method);
|
||||||
|
Span testAttemptSpan = tracerRule.spanBuilder("test-attempt-span").startSpan();
|
||||||
|
when(mockSpanBuilder.startSpan()).thenReturn(testAttemptSpan);
|
||||||
|
|
||||||
|
Metadata headers = new Metadata();
|
||||||
|
ClientStreamTracer clientStreamTracer = callTracer.newClientStreamTracer(STREAM_INFO, headers);
|
||||||
|
clientStreamTracer.streamCreated(Attributes.EMPTY, headers);
|
||||||
|
clientStreamTracer.streamClosed(Status.CANCELLED);
|
||||||
|
|
||||||
|
Metadata.Key<byte[]> key = Metadata.Key.of(
|
||||||
|
GrpcTraceBinContextPropagator.GRPC_TRACE_BIN_HEADER, Metadata.BINARY_BYTE_MARSHALLER);
|
||||||
|
assertTrue(Arrays.equals(BinaryFormat.getInstance().toBytes(testAttemptSpan.getSpanContext()),
|
||||||
|
headers.get(key)
|
||||||
|
));
|
||||||
|
verify(mockSpanBuilder).setParent(contextArgumentCaptor.capture());
|
||||||
|
assertEquals(testClientSpan, Span.fromContext(contextArgumentCaptor.getValue()));
|
||||||
|
|
||||||
|
Span serverSpan = tracerRule.spanBuilder("test-server-span").startSpan();
|
||||||
|
when(mockSpanBuilder.startSpan()).thenReturn(serverSpan);
|
||||||
|
ServerStreamTracer.Factory tracerFactory = tracingModule.getServerTracerFactory();
|
||||||
|
ServerStreamTracer serverStreamTracer =
|
||||||
|
tracerFactory.newServerStreamTracer(method.getFullMethodName(), headers);
|
||||||
|
serverStreamTracer.streamClosed(Status.CANCELLED);
|
||||||
|
|
||||||
|
verify(mockSpanBuilder, times(2))
|
||||||
|
.setParent(contextArgumentCaptor.capture());
|
||||||
|
assertEquals(testAttemptSpan.getSpanContext(),
|
||||||
|
Span.fromContext(contextArgumentCaptor.getValue()).getSpanContext());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void generateTraceSpanName() {
|
||||||
|
assertEquals(
|
||||||
|
"Sent.io.grpc.Foo", OpenTelemetryTracingModule.generateTraceSpanName(
|
||||||
|
false, "io.grpc/Foo"));
|
||||||
|
assertEquals(
|
||||||
|
"Recv.io.grpc.Bar", OpenTelemetryTracingModule.generateTraceSpanName(
|
||||||
|
true, "io.grpc/Bar"));
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue