/* * 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.it.tracing; import static com.linecorp.armeria.common.thrift.ThriftSerializationFormats.BINARY; import static org.assertj.core.api.Assertions.assertThat; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import org.apache.thrift.async.AsyncMethodCallback; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import com.github.kristofa.brave.Brave; import com.github.kristofa.brave.Sampler; import com.linecorp.armeria.client.ClientBuilder; import com.linecorp.armeria.client.Clients; import com.linecorp.armeria.client.tracing.HttpTracingClient; import com.linecorp.armeria.common.http.HttpRequest; import com.linecorp.armeria.common.http.HttpResponse; import com.linecorp.armeria.common.tracing.HelloService; import com.linecorp.armeria.common.tracing.HelloService.AsyncIface; import com.linecorp.armeria.server.ServerBuilder; import com.linecorp.armeria.server.Service; import com.linecorp.armeria.server.thrift.THttpService; import com.linecorp.armeria.server.tracing.HttpTracingService; import com.linecorp.armeria.testing.server.ServerRule; import zipkin.Span; import zipkin.reporter.Reporter; public class HttpTracingIntegrationTest { private static final ReporterImpl spanReporter = new ReporterImpl(); private HelloService.Iface fooClient; private HelloService.Iface fooClientWithoutTracing; private HelloService.AsyncIface barClient; private HelloService.AsyncIface quxClient; @Rule public final ServerRule server = new ServerRule() { @Override protected void configure(ServerBuilder sb) throws Exception { sb.serviceAt("/foo", decorate("service/foo", THttpService.of( (AsyncIface) (name, resultHandler) -> barClient.hello("Miss. " + name, new DelegatingCallback(resultHandler))))); sb.serviceAt("/bar", decorate("service/bar", THttpService.of( (AsyncIface) (name, resultHandler) -> { if (name.startsWith("Miss. ")) { name = "Ms. " + name.substring(6); } quxClient.hello(name, new DelegatingCallback(resultHandler)); }))); sb.serviceAt("/qux", decorate("service/qux", THttpService.of( (AsyncIface) (name, resultHandler) -> resultHandler.onComplete("Hello, " + name + '!')))); } }; @Before public void setupClients() { fooClient = new ClientBuilder(server.uri(BINARY, "/foo")) .decorator(HttpRequest.class, HttpResponse.class, HttpTracingClient.newDecorator(newBrave("client/foo"))) .build(HelloService.Iface.class); fooClientWithoutTracing = Clients.newClient(server.uri(BINARY, "/foo"), HelloService.Iface.class); barClient = newClient("/bar"); quxClient = newClient("/qux"); } @After public void shouldHaveNoExtraSpans() { assertThat(spanReporter.spans).isEmpty(); } private static HttpTracingService decorate(String name, Service<HttpRequest, HttpResponse> service) { return HttpTracingService.newDecorator(newBrave(name)).apply(service); } private HelloService.AsyncIface newClient(String path) { return new ClientBuilder(server.uri(BINARY, path)) .decorator(HttpRequest.class, HttpResponse.class, HttpTracingClient.newDecorator(newBrave("client" + path))) .build(HelloService.AsyncIface.class); } private static Brave newBrave(String name) { return new Brave.Builder(name).reporter(spanReporter) .traceSampler(Sampler.ALWAYS_SAMPLE).build(); } @Test(timeout = 10000) public void testClientInitiatedTrace() throws Exception { assertThat(fooClient.hello("Lee")).isEqualTo("Hello, Ms. Lee!"); final Span[] spans = spanReporter.take(6); final long traceId = spans[0].traceId; assertThat(spans).allMatch(s -> s.traceId == traceId); // client/foo and service/foo should have no parents. assertThat(spans[0].parentId).isNull(); assertThat(spans[1].parentId).isNull(); // client/foo and service/foo should have the ID values identical with their traceIds. assertThat(spans[0].id).isEqualTo(traceId); assertThat(spans[1].id).isEqualTo(traceId); // The spans that do no cross the network boundary should have the same ID. for (int i = 0; i < spans.length; i += 2) { assertThat(spans[i].id).isEqualTo(spans[i + 1].id); } // Check the parentIds. for (int i = 2; i < spans.length; i += 2) { final long expectedParentId = spans[i - 2].id; assertThat(spans[i].parentId).isEqualTo(expectedParentId); assertThat(spans[i + 1].parentId).isEqualTo(expectedParentId); } // Check the service names. assertThat(spans[0].annotations).allMatch(a -> "client/foo".equals(a.endpoint.serviceName)); assertThat(spans[1].annotations).allMatch(a -> "service/foo".equals(a.endpoint.serviceName)); assertThat(spans[2].annotations).allMatch(a -> "client/bar".equals(a.endpoint.serviceName)); assertThat(spans[3].annotations).allMatch(a -> "service/bar".equals(a.endpoint.serviceName)); assertThat(spans[4].annotations).allMatch(a -> "client/qux".equals(a.endpoint.serviceName)); assertThat(spans[5].annotations).allMatch(a -> "service/qux".equals(a.endpoint.serviceName)); // Check the span names. assertThat(spans).allMatch(s -> "hello".equals(s.name)); } @Test(timeout = 10000) public void testServiceInitiatedTrace() throws Exception { assertThat(fooClientWithoutTracing.hello("Lee")).isEqualTo("Hello, Ms. Lee!"); final Span[] spans = spanReporter.take(5); final long traceId = spans[0].traceId; assertThat(spans).allMatch(s -> s.traceId == traceId); // service/foo should have no parent. assertThat(spans[0].parentId).isNull(); // service/foo should have the ID value identical with its traceId. assertThat(spans[0].id).isEqualTo(traceId); // The spans that do no cross the network boundary should have the same ID. for (int i = 1; i < spans.length; i += 2) { assertThat(spans[i].id).isEqualTo(spans[i + 1].id); } // Check the parentIds for (int i = 1; i < spans.length; i += 2) { final long expectedParentId = spans[i - 1].id; assertThat(spans[i].parentId).isEqualTo(expectedParentId); assertThat(spans[i + 1].parentId).isEqualTo(expectedParentId); } // Check the service names. assertThat(spans[0].annotations).allMatch(a -> "service/foo".equals(a.endpoint.serviceName)); assertThat(spans[1].annotations).allMatch(a -> "client/bar".equals(a.endpoint.serviceName)); assertThat(spans[2].annotations).allMatch(a -> "service/bar".equals(a.endpoint.serviceName)); assertThat(spans[3].annotations).allMatch(a -> "client/qux".equals(a.endpoint.serviceName)); assertThat(spans[4].annotations).allMatch(a -> "service/qux".equals(a.endpoint.serviceName)); // Check the span names. assertThat(spans).allMatch(s -> "hello".equals(s.name)); } private static class DelegatingCallback implements AsyncMethodCallback<String> { private final AsyncMethodCallback<Object> resultHandler; @SuppressWarnings({ "unchecked", "rawtypes" }) DelegatingCallback(AsyncMethodCallback resultHandler) { this.resultHandler = resultHandler; } @Override public void onComplete(String response) { resultHandler.onComplete(response); } @Override public void onError(Exception exception) { resultHandler.onError(exception); } } private static class ReporterImpl implements Reporter<Span> { private final BlockingQueue<Span> spans = new LinkedBlockingQueue<>(); @Override public void report(Span span) { spans.add(span); } Span[] take(int numSpans) throws InterruptedException { final List<Span> taken = new ArrayList<>(); while (taken.size() < numSpans) { taken.add(spans.take()); } // Reverse the collected spans to sort the spans by request time. Collections.reverse(taken); return taken.toArray(new Span[numSpans]); } } }