/**
* Copyright 2015-2017 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.server;
import okio.Buffer;
import okio.GzipSink;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpHeaders;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.ConfigurableWebApplicationContext;
import zipkin.Codec;
import zipkin.Span;
import zipkin.storage.InMemoryStorage;
import static java.lang.String.format;
import static java.util.Arrays.asList;
import static org.hamcrest.CoreMatchers.startsWith;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static zipkin.TestObjects.TRACE;
import static zipkin.TestObjects.span;
import static zipkin.internal.Util.UTF_8;
@SpringBootTest(classes = ZipkinServer.class)
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@TestPropertySource(properties = {"zipkin.store.type=mem", "spring.config.name=zipkin-server"})
public class ZipkinServerIntegrationTest {
@Autowired
ConfigurableWebApplicationContext context;
@Autowired
InMemoryStorage storage;
@Autowired
ActuateCollectorMetrics metrics;
MockMvc mockMvc;
@Before
public void init() {
mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
storage.clear();
metrics.forTransport("http").reset();
}
@Test
public void writeSpans_noContentTypeIsJson() throws Exception {
byte[] body = Codec.JSON.writeSpans(TRACE);
performAsync(post("/api/v1/spans").content(body))
.andExpect(status().isAccepted());
}
@Test
public void writeSpans_updatesMetrics() throws Exception {
byte[] body = Codec.JSON.writeSpans(TRACE);
mockMvc.perform(post("/api/v1/spans").content(body));
mockMvc.perform(post("/api/v1/spans").content(body));
mockMvc
.perform(get("/metrics"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.['counter.zipkin_collector.messages.http']").value(2))
.andExpect(jsonPath("$.['counter.zipkin_collector.bytes.http']").value(body.length * 2))
.andExpect(jsonPath("$.['gauge.zipkin_collector.message_bytes.http']")
.value(Double.valueOf(body.length))) // most recent size
.andExpect(jsonPath("$.['counter.zipkin_collector.spans.http']").value(TRACE.size() * 2))
.andExpect(jsonPath("$.['gauge.zipkin_collector.message_spans.http']")
.value(Double.valueOf(TRACE.size()))); // most recent count
}
@Test
public void tracesQueryRequiresNoParameters() throws Exception {
byte[] body = Codec.JSON.writeSpans(TRACE);
performAsync(post("/api/v1/spans").content(body));
mockMvc.perform(get("/api/v1/traces"))
.andExpect(status().isOk())
.andExpect(content().string("[" + new String(body, UTF_8) + "]"));
}
@Test
public void writeSpans_malformedJsonIsBadRequest() throws Exception {
byte[] body = {'h', 'e', 'l', 'l', 'o'};
performAsync(post("/api/v1/spans").content(body))
.andExpect(status().isBadRequest())
.andExpect(content().string(startsWith("Malformed reading List<Span> from json: hello")));
}
@Test
public void writeSpans_malformedUpdatesMetrics() throws Exception {
byte[] body = {'h', 'e', 'l', 'l', 'o'};
mockMvc.perform(post("/api/v1/spans").content(body));
mockMvc
.perform(get("/metrics"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.['counter.zipkin_collector.messages.http']").value(1))
.andExpect(jsonPath("$.['counter.zipkin_collector.messages_dropped.http']").value(1));
}
@Test
public void writeSpans_malformedGzipIsBadRequest() throws Exception {
byte[] body = {'h', 'e', 'l', 'l', 'o'};
performAsync(post("/api/v1/spans").content(body).header("Content-Encoding", "gzip"))
.andExpect(status().isBadRequest())
.andExpect(content().string(startsWith("Cannot gunzip spans")));
}
@Test
public void writeSpans_contentTypeXThrift() throws Exception {
byte[] body = Codec.THRIFT.writeSpans(TRACE);
performAsync(post("/api/v1/spans").content(body).contentType("application/x-thrift"))
.andExpect(status().isAccepted());
}
@Test
public void writeSpans_malformedThriftIsBadRequest() throws Exception {
byte[] body = {'h', 'e', 'l', 'l', 'o'};
performAsync(post("/api/v1/spans").content(body).contentType("application/x-thrift"))
.andExpect(status().isBadRequest())
.andExpect(content().string(startsWith("Malformed reading List<Span> from TBinary")));
}
@Test
public void healthIsOK() throws Exception {
mockMvc
.perform(get("/health"))
.andExpect(status().isOk());
}
public void writeSpans_gzipEncoded() throws Exception {
byte[] body = Codec.JSON.writeSpans(TRACE);
Buffer sink = new Buffer();
GzipSink gzipSink = new GzipSink(sink);
gzipSink.write(new Buffer().write(body), body.length);
gzipSink.close();
byte[] gzippedBody = sink.readByteArray();
mockMvc
.perform(post("/api/v1/spans").content(gzippedBody).header("Content-Encoding", "gzip"))
.andExpect(status().isAccepted());
}
@Test
public void readsRawTrace() throws Exception {
Span span = TRACE.get(0);
// write the span to the server, twice
performAsync(post("/api/v1/spans").content(Codec.JSON.writeSpans(asList(span))))
.andExpect(status().isAccepted());
performAsync(post("/api/v1/spans").content(Codec.JSON.writeSpans(asList(span))))
.andExpect(status().isAccepted());
// sleep as the the storage operation is async
Thread.sleep(1500);
// Default will merge by span id
mockMvc.perform(get(format("/api/v1/trace/%016x", span.traceId)))
.andExpect(status().isOk())
.andExpect(content().string(new String(Codec.JSON.writeSpans(asList(span)), UTF_8)));
// In the in-memory (or cassandra) stores, a raw read will show duplicate span rows.
mockMvc.perform(get(format("/api/v1/trace/%016x?raw", span.traceId)))
.andExpect(status().isOk())
.andExpect(content().string(new String(Codec.JSON.writeSpans(asList(span, span)), UTF_8)));
}
@Test
public void getBy128BitId() throws Exception {
Span span1 = TRACE.get(0).toBuilder().traceIdHigh(1L).build();
Span span2 = span1.toBuilder().traceIdHigh(2L).build();
performAsync(post("/api/v1/spans").content(Codec.JSON.writeSpans(asList(span1, span2))))
.andExpect(status().isAccepted());
// sleep as the the storage operation is async
Thread.sleep(1500);
// Tosses high bits
mockMvc.perform(get(format("/api/v1/trace/%016x%016x", span2.traceIdHigh, span2.traceId)))
.andExpect(status().isOk())
.andExpect(content().string(new String(Codec.JSON.writeSpans(asList(span2)), UTF_8)));
}
/** The zipkin-ui is a single-page app. This prevents reloading all resources on each click. */
@Test
public void setsMaxAgeOnUiResources() throws Exception {
mockMvc.perform(get("/favicon.ico"))
.andExpect(header().string("Cache-Control", "max-age=31536000"));
mockMvc.perform(get("/config.json"))
.andExpect(header().string("Cache-Control", "max-age=600"));
mockMvc.perform(get("/index.html"))
.andExpect(header().string("Cache-Control", "max-age=60"));
}
@Test
public void doesntSetCacheControlOnNameEndpointsWhenLessThan4Services() throws Exception {
performAsync(post("/api/v1/spans")
.content("[" + new String(Codec.JSON.writeSpan(TRACE.get(0)), UTF_8) + "]"));
mockMvc.perform(get("/api/v1/services"))
.andExpect(status().isOk())
.andExpect(header().doesNotExist("Cache-Control"));
mockMvc.perform(get("/api/v1/spans?serviceName=web"))
.andExpect(status().isOk())
.andExpect(header().doesNotExist("Cache-Control"));
}
@Test
public void setsCacheControlOnNameEndpointsWhenMoreThan3Services() throws Exception {
mockMvc.perform(post("/api/v1/spans").content(Codec.JSON.writeSpans(TRACE)));
mockMvc.perform(post("/api/v1/spans").content(Codec.JSON.writeSpans(asList(span(1)))));
mockMvc.perform(get("/api/v1/services"))
.andExpect(status().isOk())
.andExpect(header().string("Cache-Control", "max-age=300, must-revalidate"));
mockMvc.perform(get("/api/v1/spans?serviceName=web"))
.andExpect(status().isOk())
.andExpect(header().string("Cache-Control", "max-age=300, must-revalidate"));
}
@Test
public void shouldAllowAnyOriginByDefault() throws Exception {
mockMvc.perform(get("/api/v1/traces")
.header(HttpHeaders.ORIGIN, "foo.example.com"))
.andExpect(status().isOk());
}
ResultActions performAsync(MockHttpServletRequestBuilder request) throws Exception {
return mockMvc.perform(asyncDispatch(mockMvc.perform(request).andReturn()));
}
}