/*
* Copyright 2016 LINE Corporation
*
* LINE Corporation licenses this file to you 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 com.linecorp.armeria.client.tracing;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import javax.annotation.Nullable;
import com.github.kristofa.brave.Brave;
import com.github.kristofa.brave.ClientRequestAdapter;
import com.github.kristofa.brave.ClientResponseAdapter;
import com.github.kristofa.brave.KeyValueAnnotation;
import com.github.kristofa.brave.SpanId;
import com.twitter.zipkin.gen.Endpoint;
import com.twitter.zipkin.gen.Span;
import com.linecorp.armeria.client.Client;
import com.linecorp.armeria.client.ClientRequestContext;
import com.linecorp.armeria.client.DecoratingClient;
import com.linecorp.armeria.client.SimpleDecoratingClient;
import com.linecorp.armeria.common.Request;
import com.linecorp.armeria.common.Response;
import com.linecorp.armeria.common.RpcRequest;
import com.linecorp.armeria.common.logging.RequestLog;
import com.linecorp.armeria.common.logging.RequestLogAvailability;
/**
* An abstract {@link DecoratingClient} that traces outgoing {@link Request}s.
*
* <p>This class depends on <a href="https://github.com/openzipkin/brave">Brave</a>, a distributed tracing
* library.
*
* @param <I> the {@link Request} type
* @param <O> the {@link Response} type
*/
public abstract class AbstractTracingClient<I extends Request, O extends Response>
extends SimpleDecoratingClient<I, O> {
private final ClientTracingInterceptor clientInterceptor;
/**
* Creates a new instance.
*/
protected AbstractTracingClient(Client<? super I, ? extends O> delegate, Brave brave) {
super(delegate);
clientInterceptor = new ClientTracingInterceptor(brave);
}
@Override
public O execute(ClientRequestContext ctx, I req) throws Exception {
// create new request adapter to catch generated spanId
final String method = req instanceof RpcRequest ? ((RpcRequest) req).method() : ctx.method();
final InternalClientRequestAdapter requestAdapter =
new InternalClientRequestAdapter(
Endpoint.builder()
.serviceName(ctx.endpoint().authority())
.build(),
method);
final Span span = clientInterceptor.openSpan(requestAdapter);
// new client options with trace data
putTraceData(ctx, req, requestAdapter.getSpanId());
if (span == null) {
// skip tracing
return delegate().execute(ctx, req);
}
// The actual remote invocation is done asynchronously.
// So we have to clear the span from current thread.
clientInterceptor.clearSpan();
ctx.log().addListener(log -> closeSpan(ctx, span, log), RequestLogAvailability.COMPLETE);
return delegate().execute(ctx, req);
}
/**
* Puts trace data into the specified {@link Request} or {@link ClientRequestContext}.
*/
protected abstract void putTraceData(ClientRequestContext ctx, I req, @Nullable SpanId spanId);
/**
* Returns the client-side annotations that should be added to a Zipkin span.
*/
protected List<KeyValueAnnotation> annotations(ClientRequestContext ctx, RequestLog log) {
final KeyValueAnnotation clientUriAnnotation = KeyValueAnnotation.create(
"client.uri", log.scheme().uriText() + "://" + log.host() + ctx.path() + '#' + log.method());
final List<KeyValueAnnotation> annotations = new ArrayList<>(3);
annotations.add(clientUriAnnotation);
final Throwable cause = log.responseCause();
final String clientResultText = cause == null ? "success" : "failure";
annotations.add(KeyValueAnnotation.create("client.result", clientResultText));
if (cause != null) {
annotations.add(KeyValueAnnotation.create("client.cause", cause.toString()));
}
return annotations;
}
private void closeSpan(ClientRequestContext ctx, Span span, RequestLog log) {
final Object requestContent = log.requestContent();
if (requestContent instanceof RpcRequest) {
span.setName(((RpcRequest) requestContent).method());
}
clientInterceptor.closeSpan(span, createResponseAdapter(ctx, log));
}
/**
* Creates a new {@link ClientResponseAdapter} from the specified request-response information.
*/
protected ClientResponseAdapter createResponseAdapter(ClientRequestContext ctx, RequestLog req) {
final List<KeyValueAnnotation> annotations = annotations(ctx, req);
return () -> annotations;
}
/**
* A {@link ClientRequestAdapter} holding a {@link SpanId} that was passed from brave.
*/
private static class InternalClientRequestAdapter implements ClientRequestAdapter {
private final Endpoint endpoint;
private final String spanName;
private SpanId spanId;
InternalClientRequestAdapter(Endpoint endpoint, String spanName) {
this.endpoint = endpoint;
this.spanName = spanName;
}
@Override
public Endpoint serverAddress() {
return endpoint;
}
@Nullable
public SpanId getSpanId() {
return spanId;
}
@Override
public String getSpanName() {
return spanName;
}
@Override
public void addSpanIdToRequest(@Nullable SpanId spanId) {
this.spanId = spanId;
}
@Override
public Collection<KeyValueAnnotation> requestAnnotations() {
return Collections.emptyList();
}
}
}