package brave.http; import brave.SpanCustomizer; import brave.Tracer; import brave.Tracer.SpanInScope; import brave.internal.HexCodec; import brave.sampler.Sampler; import java.io.IOException; import java.util.concurrent.TimeUnit; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.RecordedRequest; import okhttp3.mockwebserver.SocketPolicy; import org.junit.After; import org.junit.Before; import org.junit.Test; import zipkin.Constants; import zipkin.Endpoint; import zipkin.TraceKeys; import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; public abstract class ITHttpClient<C> extends ITHttp { protected C client; @Before public void setup() { httpTracing = HttpTracing.create(tracingBuilder(Sampler.ALWAYS_SAMPLE).build()); client = newClient(server.getPort()); } /** Make sure the client you return has retries disabled. */ protected abstract C newClient(int port); protected abstract void closeClient(C client) throws IOException; protected abstract void get(C client, String pathIncludingQuery) throws Exception; protected abstract void post(C client, String pathIncludingQuery, String body) throws Exception; protected abstract void getAsync(C client, String pathIncludingQuery) throws Exception; @After public void close() throws IOException { closeClient(client); } @Test public void propagatesSpan() throws Exception { server.enqueue(new MockResponse()); get(client, "/foo"); RecordedRequest request = server.takeRequest(); assertThat(request.getHeaders().toMultimap()) .containsKeys("x-b3-traceId", "x-b3-spanId") .containsEntry("x-b3-sampled", asList("1")); } @Test public void makesChildOfCurrentSpan() throws Exception { Tracer tracer = httpTracing.tracing().tracer(); server.enqueue(new MockResponse()); brave.Span parent = tracer.newTrace().name("test").start(); try (SpanInScope ws = tracer.withSpanInScope(parent)) { get(client, "/foo"); } finally { parent.finish(); } RecordedRequest request = server.takeRequest(); assertThat(request.getHeader("x-b3-traceId")) .isEqualTo(parent.context().traceIdString()); assertThat(request.getHeader("x-b3-parentspanid")) .isEqualTo(HexCodec.toLowerHex(parent.context().spanId())); } /** * This tests that the parent is determined at the time the request was made, not when the request * was executed. */ @Test public void usesParentFromInvocationTime() throws Exception { Tracer tracer = httpTracing.tracing().tracer(); server.enqueue(new MockResponse().setBodyDelay(1, TimeUnit.SECONDS)); server.enqueue(new MockResponse()); brave.Span parent = tracer.newTrace().name("test").start(); try (SpanInScope ws = tracer.withSpanInScope(parent)) { getAsync(client, "/foo"); getAsync(client, "/foo"); } finally { parent.finish(); } brave.Span otherSpan = tracer.newTrace().name("test2").start(); try (SpanInScope ws = tracer.withSpanInScope(otherSpan)) { for (int i = 0; i < 2; i++) { RecordedRequest request = server.takeRequest(); assertThat(request.getHeader("x-b3-traceId")) .isEqualTo(parent.context().traceIdString()); assertThat(request.getHeader("x-b3-parentspanid")) .isEqualTo(HexCodec.toLowerHex(parent.context().spanId())); } } finally { otherSpan.finish(); } } /** Unlike Brave 3, Brave 4 propagates trace ids even when unsampled */ @Test public void propagates_sampledFalse() throws Exception { close(); httpTracing = HttpTracing.create(tracingBuilder(Sampler.NEVER_SAMPLE).build()); client = newClient(server.getPort()); server.enqueue(new MockResponse()); get(client, "/foo"); RecordedRequest request = server.takeRequest(); assertThat(request.getHeaders().toMultimap()) .containsKeys("x-b3-traceId", "x-b3-spanId") .doesNotContainKey("x-b3-parentSpanId") .containsEntry("x-b3-sampled", asList("0")); } @Test public void customSampler() throws Exception { close(); httpTracing = httpTracing.toBuilder().clientSampler(HttpSampler.NEVER_SAMPLE).build(); client = newClient(server.getPort()); server.enqueue(new MockResponse()); get(client, "/foo"); RecordedRequest request = server.takeRequest(); assertThat(request.getHeaders().toMultimap()) .containsEntry("x-b3-sampled", asList("0")); } @Test public void reportsClientAnnotationsToZipkin() throws Exception { server.enqueue(new MockResponse()); get(client, "/foo"); assertThat(spans) .flatExtracting(s -> s.annotations) .extracting(a -> a.value) .containsExactly("cs", "cr"); } @Test public void reportsServerAddress() throws Exception { server.enqueue(new MockResponse()); get(client, "/foo"); assertThat(spans) .flatExtracting(s -> s.binaryAnnotations) .filteredOn(b -> b.key.equals(Constants.SERVER_ADDR)) .extracting(b -> b.endpoint) .containsExactly(Endpoint.builder() .serviceName("") .ipv4(127 << 24 | 1) .port(server.getPort()).build() ); } @Test public void defaultSpanNameIsMethodName() throws Exception { server.enqueue(new MockResponse()); get(client, "/foo"); assertThat(spans) .extracting(s -> s.name) .containsExactly("get"); } @Test public void supportsPortableCustomization() throws Exception { String uri = "/foo?z=2&yAA=1"; close(); httpTracing = httpTracing.toBuilder() .clientParser(new HttpClientParser() { @Override public <Req> void request(HttpAdapter<Req, ?> adapter, Req req, SpanCustomizer customizer) { customizer.name(adapter.method(req).toLowerCase() + " " + adapter.path(req)); customizer.tag(TraceKeys.HTTP_URL, adapter.url(req)); // just the path is logged by default } }) .build().clientOf("remote-service"); client = newClient(server.getPort()); server.enqueue(new MockResponse()); get(client, uri); assertThat(spans) .extracting(s -> s.name) .containsExactly("get /foo"); assertThat(spans) .flatExtracting(s -> s.binaryAnnotations) .filteredOn(b -> b.key.equals(Constants.SERVER_ADDR)) .extracting(b -> b.endpoint.serviceName) .containsExactly("remote-service"); assertReportedTagsInclude(TraceKeys.HTTP_URL, url(uri)); } @Test public void addsStatusCodeWhenNotOk() throws Exception { server.enqueue(new MockResponse().setResponseCode(400)); try { get(client, "/foo"); } catch (RuntimeException e) { // some clients think 400 is an error } assertReportedTagsInclude(TraceKeys.HTTP_STATUS_CODE, "400"); assertReportedTagsInclude(Constants.ERROR, "400"); } @Test public void redirect() throws Exception { Tracer tracer = httpTracing.tracing().tracer(); server.enqueue(new MockResponse().setResponseCode(302) .addHeader("Location: " + url("/bar"))); server.enqueue(new MockResponse().setResponseCode(404)); // hehe to a bad location! brave.Span parent = tracer.newTrace().name("test").start(); try (Tracer.SpanInScope ws = tracer.withSpanInScope(parent)) { get(client, "/foo"); } catch (RuntimeException e) { // some think 404 is an exception } finally { parent.finish(); } assertReportedTagsInclude(TraceKeys.HTTP_PATH, "/foo", "/bar"); } @Test public void post() throws Exception { String path = "/post"; String body = "body"; server.enqueue(new MockResponse()); post(client, path, body); assertThat(server.takeRequest().getBody().readUtf8()) .isEqualTo(body); assertThat(spans) .extracting(s -> s.name) .containsExactly("post"); } @Test public void reportsSpanOnTransportException() throws Exception { server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START)); try { get(client, "/foo"); } catch (Exception e) { // ok, but the span should include an error! } assertThat(spans).hasSize(1); } @Test public void addsErrorTagOnTransportException() throws Exception { reportsSpanOnTransportException(); assertThat(spans) .flatExtracting(s -> s.binaryAnnotations) .extracting(b -> b.key) .contains(Constants.ERROR); } @Test public void httpPathTagExcludesQueryParams() throws Exception { String path = "/foo?z=2&yAA=1"; server.enqueue(new MockResponse()); get(client, path); assertReportedTagsInclude(TraceKeys.HTTP_PATH, "/foo"); } protected String url(String pathIncludingQuery) { return "http://127.0.0.1:" + server.getPort() + pathIncludingQuery; } }