/** * Copyright 2015-2016 The OpenZipkin 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 zipkin.junit; import java.io.IOException; import java.util.Arrays; import java.util.List; import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; import okio.Buffer; import okio.ByteString; import okio.GzipSink; import okio.GzipSource; import org.junit.Rule; import org.junit.Test; import zipkin.Annotation; import zipkin.Codec; import zipkin.Span; import static java.lang.String.format; import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown; import static zipkin.TestObjects.TRACE; public class ZipkinRuleTest { @Rule public ZipkinRule zipkin = new ZipkinRule(); OkHttpClient client = new OkHttpClient(); long traceId = TRACE.get(0).traceId; @Test public void getTraces_storedViaPost() throws IOException { // write the span to the zipkin using http assertThat(postSpans(TRACE).code()).isEqualTo(202); // read the traces directly assertThat(zipkin.getTraces()) .containsOnly(TRACE); } /** The rule is here to help debugging. Even partial spans should be returned */ @Test public void getTraces_whenMissingTimestamps() throws IOException { Span span = Span.builder().traceId(traceId).id(traceId).name("foo").build(); // write the span to the zipkin using http assertThat(postSpans(asList(span)).code()).isEqualTo(202); // read the traces directly assertThat(zipkin.getTraces()) .containsOnly(asList(span)); } @Test public void healthIsOK() throws IOException { Response getResponse = client.newCall(new Request.Builder() .url(zipkin.httpUrl() + "/health").build() ).execute(); assertThat(getResponse.code()).isEqualTo(200); assertThat(getResponse.body().string()).isEqualTo("OK\n"); } @Test public void storeSpans_readbackHttp() throws IOException { // write the span to zipkin directly zipkin.storeSpans(TRACE); // read trace id using the the http api Response getResponse = client.newCall(new Request.Builder() .url(format("%s/api/v1/trace/%016x", zipkin.httpUrl(), traceId)).build() ).execute(); assertThat(getResponse.code()).isEqualTo(200); } /** The raw query can show affects like redundant rows in the data store. */ @Test public void storeSpans_readbackRaw() throws IOException { // write the span to zipkin directly zipkin.storeSpans(TRACE); zipkin.storeSpans(TRACE); // Default will merge by span id Response defaultResponse = client.newCall(new Request.Builder() .url(format("%s/api/v1/trace/%016x", zipkin.httpUrl(), traceId)).build() ).execute(); assertThat(Codec.JSON.readSpans(defaultResponse.body().bytes())).hasSize(TRACE.size()); // In the in-memory (or cassandra) stores, a raw read will show duplicate span rows. Response rawResponse = client.newCall(new Request.Builder() .url(format("%s/api/v1/trace/%016x?raw", zipkin.httpUrl(), traceId)).build() ).execute(); assertThat(Codec.JSON.readSpans(rawResponse.body().bytes())).hasSize(TRACE.size() * 2); } @Test public void getBy128BitTraceId() throws Exception { Span span = TRACE.get(0).toBuilder().traceIdHigh(traceId).build(); zipkin.storeSpans(asList(span)); Response getResponse = client.newCall(new Request.Builder() .url(format("%s/api/v1/trace/%016x%016x", zipkin.httpUrl(), traceId, traceId)).build() ).execute(); assertThat(getResponse.code()).isEqualTo(200); } @Test public void httpRequestCountIncrements() throws IOException { postSpans(TRACE); postSpans(TRACE); assertThat(zipkin.httpRequestCount()).isEqualTo(2); } /** * Normally, a span can be reported twice: for client and server. However, there are bugs that * happened where several updates went to the same span id. {@link ZipkinRule#collectorMetrics} * can be used to help ensure a span isn't reported more times than expected. */ @Test public void collectorMetrics_spans() throws IOException { postSpans(TRACE); postSpans(TRACE); assertThat(zipkin.collectorMetrics().spans()).isEqualTo(TRACE.size() * 2); } @Test public void postSpans_disconnectDuringBody() throws IOException { zipkin.enqueueFailure(HttpFailure.disconnectDuringBody()); try { postSpans(TRACE); failBecauseExceptionWasNotThrown(IOException.class); } catch (IOException expected) { // not always a ConnectException! } // Zipkin didn't store the spans, as they shouldn't have been readable, due to disconnect assertThat(zipkin.getTraces()).isEmpty(); // The failure shouldn't affect later requests assertThat(postSpans(TRACE).code()).isEqualTo(202); } @Test public void postSpans_sendErrorResponse400() throws IOException { zipkin.enqueueFailure(HttpFailure.sendErrorResponse(400, "Invalid Format")); Response response = postSpans(TRACE); assertThat(response.code()).isEqualTo(400); assertThat(response.body().string()).isEqualTo("Invalid Format"); // Zipkin didn't store the spans, as they shouldn't have been readable, due to the error assertThat(zipkin.getTraces()).isEmpty(); // The failure shouldn't affect later requests assertThat(postSpans(TRACE).code()).isEqualTo(202); } @Test public void gzippedSpans() throws IOException { byte[] spansInJson = Codec.JSON.writeSpans(TRACE); Buffer sink = new Buffer(); GzipSink gzipSink = new GzipSink(sink); gzipSink.write(new Buffer().write(spansInJson), spansInJson.length); gzipSink.close(); ByteString gzippedJson = sink.readByteString(); client.newCall(new Request.Builder() .url(zipkin.httpUrl() + "/api/v1/spans") .addHeader("Content-Encoding", "gzip") .post(RequestBody.create(MediaType.parse("application/json"), gzippedJson)).build() ).execute(); assertThat(zipkin.getTraces()).containsOnly(TRACE); assertThat(zipkin.collectorMetrics().bytes()).isEqualTo(spansInJson.length); } @Test public void gzippedSpans_invalidIs400() throws IOException { Response response = client.newCall(new Request.Builder() .url(zipkin.httpUrl() + "/api/v1/spans") .addHeader("Content-Encoding", "gzip") .post(RequestBody.create(MediaType.parse("application/json"), "hello".getBytes())).build() ).execute(); assertThat(response.code()).isEqualTo(400); } @Test public void readSpans_gzippedResponse() throws Exception { char[] annotation2K = new char[2048]; Arrays.fill(annotation2K, 'a'); List<Span> trace = asList(TRACE.get(0).toBuilder().addAnnotation( Annotation.create(System.currentTimeMillis(), new String(annotation2K), null)).build()); zipkin.storeSpans(trace); Response response = client.newCall(new Request.Builder() .url(format("%s/api/v1/trace/%016x", zipkin.httpUrl(), traceId)) .addHeader("Accept-Encoding", "gzip").build() ).execute(); assertThat(response.code()).isEqualTo(200); assertThat(response.body().contentLength()).isLessThan(annotation2K.length); Buffer result = new Buffer(); GzipSource source = new GzipSource(response.body().source()); while (source.read(result, Integer.MAX_VALUE) != -1) ; byte[] unzipped = result.readByteArray(); assertThat(Codec.JSON.readSpans(unzipped)).isEqualTo(trace); } Response postSpans(List<Span> spans) throws IOException { byte[] spansInJson = Codec.JSON.writeSpans(spans); return client.newCall(new Request.Builder() .url(zipkin.httpUrl() + "/api/v1/spans") .post(RequestBody.create(MediaType.parse("application/json"), spansInJson)).build() ).execute(); } }