package brave.http; import brave.SpanCustomizer; import brave.internal.HexCodec; import brave.sampler.Sampler; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; import org.junit.AssumptionViolatedException; import org.junit.Before; import org.junit.Test; import zipkin.Constants; import zipkin.Span; import zipkin.TraceKeys; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.groups.Tuple.tuple; public abstract class ITHttpServer extends ITHttp { OkHttpClient client = new OkHttpClient(); @Before public void setup() throws Exception { httpTracing = HttpTracing.create(tracingBuilder(Sampler.ALWAYS_SAMPLE).build()); init(); } /** recreate the server if needed */ protected abstract void init() throws Exception; protected abstract String url(String path); @Test public void usesExistingTraceId() throws Exception { String path = "/foo"; final String traceId = "463ac35c9f6413ad"; final String parentId = traceId; final String spanId = "48485a3953bb6124"; Request request = new Request.Builder().url(url(path)) .header("X-B3-TraceId", traceId) .header("X-B3-ParentSpanId", parentId) .header("X-B3-SpanId", spanId) .header("X-B3-Sampled", "1") .build(); try (Response response = client.newCall(request).execute()) { assertThat(response.isSuccessful()).isTrue(); } assertThat(spans).allSatisfy(s -> { assertThat(HexCodec.toLowerHex(s.traceId)).isEqualTo(traceId); assertThat(HexCodec.toLowerHex(s.parentId)).isEqualTo(parentId); assertThat(HexCodec.toLowerHex(s.id)).isEqualTo(spanId); }); } @Test public void samplingDisabled() throws Exception { httpTracing = HttpTracing.create(tracingBuilder(Sampler.NEVER_SAMPLE).build()); init(); String path = "/foo"; Request request = new Request.Builder().url(url(path)).build(); try (Response response = client.newCall(request).execute()) { assertThat(response.isSuccessful()).isTrue(); } assertThat(spans) .isEmpty(); } /** * Tests that the span propagates between under asynchronous callbacks (even if explicitly) */ @Test public void async() throws Exception { String path = "/async"; Request request = new Request.Builder().url(url(path)).build(); try (Response response = client.newCall(request).execute()) { if (response.code() == 404) throw new AssumptionViolatedException(path + " not supported"); assertThat(response.isSuccessful()).isTrue(); } catch (AssumptionViolatedException e) { throw e; } assertThat(spans).hasSize(1); } /** * This ensures thread-state is propagated from trace interceptors to user code. The endpoint * "/child" is expected to create a local span. When this works, it should be a child of the * "current span", in this case the span representing an incoming server request. When thread * state isn't managed properly, the child span will appear as a new trace. */ @Test public void createsChildSpan() throws Exception { String path = "/child"; Request request = new Request.Builder().url(url(path)).build(); try (Response response = client.newCall(request).execute()) { if (response.code() == 404) throw new AssumptionViolatedException(path + " not supported"); } catch (AssumptionViolatedException e) { throw e; } catch (Exception e) { // ok, but the span should include an error! } assertThat(spans).hasSize(2); Span child = spans.pop(); Span parent = spans.pop(); assertThat(parent.traceId).isEqualTo(child.traceId); assertThat(parent.id).isEqualTo(child.parentId); assertThat(parent.timestamp).isLessThan(child.timestamp); assertThat(parent.duration).isGreaterThan(child.duration); } @Test public void reportsClientAddress() throws Exception { String path = "/foo"; Request request = new Request.Builder().url(url(path)).build(); try (Response response = client.newCall(request).execute()) { assertThat(response.isSuccessful()).isTrue(); } assertThat(spans) .flatExtracting(s -> s.binaryAnnotations) .extracting(b -> b.key) .contains(Constants.CLIENT_ADDR); } @Test public void reportsClientAddress_XForwardedFor() throws Exception { String path = "/foo"; Request request = new Request.Builder().url(url(path)) .header("X-Forwarded-For", "1.2.3.4") .build(); try (Response response = client.newCall(request).execute()) { assertThat(response.isSuccessful()).isTrue(); } assertThat(spans) .flatExtracting(s -> s.binaryAnnotations) .extracting(b -> b.key, b -> b.endpoint.ipv4) .contains(tuple(Constants.CLIENT_ADDR, 1 << 24 | 2 << 16 | 3 << 8 | 4)); } @Test public void reportsServerAnnotationsToZipkin() throws Exception { String path = "/foo"; Request request = new Request.Builder().url(url(path)).build(); try (Response response = client.newCall(request).execute()) { assertThat(response.isSuccessful()).isTrue(); } assertThat(spans) .flatExtracting(s -> s.annotations) .extracting(a -> a.value) .containsExactly("sr", "ss"); } @Test public void defaultSpanNameIsMethodName() throws Exception { String path = "/foo"; Request request = new Request.Builder().url(url(path)).build(); try (Response response = client.newCall(request).execute()) { assertThat(response.isSuccessful()).isTrue(); } assertThat(spans) .extracting(s -> s.name) .containsExactly("get"); } @Test public void supportsPortableCustomization() throws Exception { String uri = "/foo?z=2&yAA=1"; httpTracing = httpTracing.toBuilder().serverParser(new HttpServerParser() { @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(); init(); Request request = new Request.Builder().url(url(uri)).build(); try (Response response = client.newCall(request).execute()) { assertThat(response.isSuccessful()).isTrue(); } assertThat(spans) .extracting(s -> s.name) .containsExactly("get /foo"); assertReportedTagsInclude(TraceKeys.HTTP_URL, url(uri)); } @Test public void addsStatusCode_badRequest() throws Exception { String path = "/badrequest"; Request request = new Request.Builder().url(url(path)).build(); try (Response response = client.newCall(request).execute()) { } catch (RuntimeException e) { // some servers think 400 is an error } assertReportedTagsInclude(TraceKeys.HTTP_STATUS_CODE, "400"); assertReportedTagsInclude(Constants.ERROR, "400"); } @Test public void reportsSpanOnException() throws Exception { reportsSpanOnException("/exception"); } @Test public void reportsSpanOnException_async() throws Exception { reportsSpanOnException("/exceptionAsync"); } private void reportsSpanOnException(String path) { Request request = new Request.Builder().url(url(path)).build(); try (Response response = client.newCall(request).execute()) { if (response.code() == 404) throw new AssumptionViolatedException(path + " not supported"); } catch (AssumptionViolatedException e) { throw e; } catch (Exception e) { // ok, but the span should include an error! } assertThat(spans).hasSize(1); } @Test public void addsErrorTagOnException() throws Exception { addsErrorTagOnException("/exception"); } @Test public void addsErrorTagOnException_async() throws Exception { addsErrorTagOnException("/exceptionAsync"); } private void addsErrorTagOnException(String path) { reportsSpanOnException(path); try { Thread.sleep(1000L); } catch (InterruptedException e) { e.printStackTrace(); } assertThat(spans) .flatExtracting(s -> s.binaryAnnotations) .extracting(b -> b.key) .contains(Constants.ERROR); } @Test public void httpPathTagExcludesQueryParams() throws Exception { String uri = "/foo?z=2&yAA=1"; Request request = new Request.Builder().url(url(uri)).build(); try (Response response = client.newCall(request).execute()) { assertThat(response.isSuccessful()).isTrue(); } assertReportedTagsInclude(TraceKeys.HTTP_PATH, "/foo"); } }