Fixes missing zipkin service name and polishes span converters.
Missing service name: Zipkin service names were logged as null, which is invalid and led to them showing up as "unknown" in the zipkin ui. This was due to a wiring bug, and a special-case, which this change fixes. The special-case was when a sleuth span had no annotations. Since zipkin service names are attached to annotations, they are only queryable when annotations exist. When there are no annotations, we add the "lc" binary annotation, which makes that span attached to the correct service in zipkin. Polishing: Zipkin timestamps were not always set as microseconds. This fixes that. The de-facto label in zipkin for unknown service is "unknown". This fixes the code, which formerly fell back to "application". This also removes complexity in assigning timestamp and duration as we no longer need to make pseudo-annotations "acquire" and "release". Finally, this adds tests about above consistently to both scs-zipkin and scs-zipkin-stream.
This commit is contained in:
@@ -37,6 +37,7 @@ import org.springframework.util.StringUtils;
|
||||
import io.zipkin.Annotation;
|
||||
import io.zipkin.BinaryAnnotation;
|
||||
import io.zipkin.BinaryAnnotation.Type;
|
||||
import io.zipkin.Constants;
|
||||
import io.zipkin.Endpoint;
|
||||
import io.zipkin.Span.Builder;
|
||||
import io.zipkin.SpanStore;
|
||||
@@ -74,14 +75,29 @@ public class ZipkinMessageListener {
|
||||
* <li>Create binary annotations based on data from Span object.
|
||||
* </ul>
|
||||
*/
|
||||
public io.zipkin.Span convert(Span span, Host host) {
|
||||
// VisibleForTesting
|
||||
static io.zipkin.Span convert(Span span, Host host) {
|
||||
Builder zipkinSpan = new io.zipkin.Span.Builder();
|
||||
|
||||
Endpoint ep = Endpoint.create(host.getServiceName(), host.getIpv4(),
|
||||
host.getPort().shortValue());
|
||||
List<Annotation> annotationList = createZipkinAnnotations(span, ep);
|
||||
List<BinaryAnnotation> binaryAnnotationList = createZipkinBinaryAnnotations(span,
|
||||
ep);
|
||||
|
||||
// A zipkin span without any annotations cannot be queried, add special "lc" to avoid that.
|
||||
if (span.getTimelineAnnotations().isEmpty() && span.getAnnotations().isEmpty()) {
|
||||
// TODO: javadocs say this isn't nullable!
|
||||
String processId = span.getProcessId() != null
|
||||
? span.getProcessId().toLowerCase()
|
||||
: "unknown";
|
||||
zipkinSpan.addBinaryAnnotation(
|
||||
BinaryAnnotation.create(Constants.LOCAL_COMPONENT, processId, ep)
|
||||
);
|
||||
} else {
|
||||
addZipkinAnnotations(zipkinSpan, span, ep);
|
||||
addZipkinBinaryAnnotations(zipkinSpan, span, ep);
|
||||
}
|
||||
|
||||
zipkinSpan.timestamp(span.getBegin() * 1000);
|
||||
zipkinSpan.duration((span.getEnd() - span.getBegin()) * 1000);
|
||||
zipkinSpan.traceId(hash(span.getTraceId()));
|
||||
if (span.getParents().size() > 0) {
|
||||
if (span.getParents().size() > 1) {
|
||||
@@ -94,26 +110,21 @@ public class ZipkinMessageListener {
|
||||
if (StringUtils.hasText(span.getName())) {
|
||||
zipkinSpan.name(span.getName());
|
||||
}
|
||||
for (Annotation annotation : annotationList) {
|
||||
zipkinSpan.addAnnotation(annotation);
|
||||
}
|
||||
for (BinaryAnnotation annotation : binaryAnnotationList) {
|
||||
zipkinSpan.addBinaryAnnotation(annotation);
|
||||
}
|
||||
return zipkinSpan.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add annotations from the sleuth Span.
|
||||
*/
|
||||
private List<Annotation> createZipkinAnnotations(Span span, Endpoint endpoint) {
|
||||
List<Annotation> annotationList = new ArrayList<>();
|
||||
private static void addZipkinAnnotations(Builder zipkinSpan, Span span, Endpoint endpoint) {
|
||||
for (TimelineAnnotation ta : span.getTimelineAnnotations()) {
|
||||
Annotation zipkinAnnotation = createZipkinAnnotation(ta.getMsg(),
|
||||
ta.getTime(), endpoint, true);
|
||||
annotationList.add(zipkinAnnotation);
|
||||
Annotation zipkinAnnotation = new Annotation.Builder()
|
||||
.endpoint(endpoint)
|
||||
.timestamp(ta.getTime() * 1000) // Zipkin is in microseconds
|
||||
.value(ta.getMsg())
|
||||
.build();
|
||||
zipkinSpan.addAnnotation(zipkinAnnotation);
|
||||
}
|
||||
return annotationList;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -121,9 +132,8 @@ public class ZipkinMessageListener {
|
||||
*
|
||||
* @return list of Annotations that could be added to Zipkin Span.
|
||||
*/
|
||||
private List<BinaryAnnotation> createZipkinBinaryAnnotations(Span span,
|
||||
private static void addZipkinBinaryAnnotations(Builder zipkinSpan, Span span,
|
||||
Endpoint endpoint) {
|
||||
List<BinaryAnnotation> l = new ArrayList<>();
|
||||
for (Map.Entry<String, String> e : span.getAnnotations().entrySet()) {
|
||||
BinaryAnnotation.Builder binaryAnn = new BinaryAnnotation.Builder();
|
||||
binaryAnn.type(Type.STRING);
|
||||
@@ -135,33 +145,8 @@ public class ZipkinMessageListener {
|
||||
log.error("Error encoding string as UTF-8", ex);
|
||||
}
|
||||
binaryAnn.endpoint(endpoint);
|
||||
l.add(binaryAnn.build());
|
||||
zipkinSpan.addBinaryAnnotation(binaryAnn.build());
|
||||
}
|
||||
return l;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an annotation with the correct times and endpoint.
|
||||
*
|
||||
* @param value Annotation value
|
||||
* @param time timestamp will be extracted
|
||||
* @param endpoint the endpoint this annotation will be associated with.
|
||||
* @param sendRequest use the first or last timestamp.
|
||||
*/
|
||||
private static Annotation createZipkinAnnotation(String value, long time,
|
||||
Endpoint endpoint, boolean sendRequest) {
|
||||
Annotation.Builder annotation = new Annotation.Builder();
|
||||
annotation.endpoint(endpoint);
|
||||
|
||||
// Zipkin is in microseconds
|
||||
if (sendRequest) {
|
||||
annotation.timestamp(time * 1000);
|
||||
}
|
||||
else {
|
||||
annotation.timestamp(time * 1000);
|
||||
}
|
||||
annotation.value(value);
|
||||
return annotation.build();
|
||||
}
|
||||
|
||||
private static long hash(String string) {
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright 2013-2015 the original author or 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 org.springframework.cloud.sleuth.zipkin.stream;
|
||||
|
||||
import io.zipkin.BinaryAnnotation;
|
||||
import io.zipkin.Endpoint;
|
||||
import java.util.Collections;
|
||||
import org.junit.Test;
|
||||
import org.springframework.cloud.sleuth.MilliSpan;
|
||||
import org.springframework.cloud.sleuth.stream.Host;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class ZipkinMessageListenerTests {
|
||||
MilliSpan span = new MilliSpan(1, 3, "name", "traceId", Collections.<String>emptyList(), "spanId", true, true, "processId");
|
||||
Host host = new Host("myservice", "1.2.3.4", 8080);
|
||||
Endpoint endpoint = Endpoint.create("myservice", 1 << 24 | 2 << 16 | 3 << 8 | 4, 8080);
|
||||
|
||||
/** Sleuth timestamps are millisecond granularity while zipkin is microsecond. */
|
||||
@Test
|
||||
public void convertsTimestampAndDurationToMicroseconds() {
|
||||
long start = System.currentTimeMillis();
|
||||
span.addTimelineAnnotation("http/request/retry"); // System.currentTimeMillis
|
||||
|
||||
io.zipkin.Span result = ZipkinMessageListener.convert(span, host);
|
||||
|
||||
assertThat(result.timestamp)
|
||||
.isEqualTo(span.getBegin() * 1000);
|
||||
assertThat(result.duration)
|
||||
.isEqualTo((span.getEnd() - span.getBegin()) * 1000);
|
||||
assertThat(result.annotations.get(0).timestamp)
|
||||
.isGreaterThanOrEqualTo(start * 1000)
|
||||
.isLessThanOrEqualTo(System.currentTimeMillis() * 1000);
|
||||
}
|
||||
|
||||
/** Sleuth host corresponds to annotation/binaryAnnotation.host in zipkin. */
|
||||
@Test
|
||||
public void annotationsIncludeHost() {
|
||||
span.addTimelineAnnotation("http/request/retry");
|
||||
span.addAnnotation("spring-boot/version", "1.3.1.RELEASE");
|
||||
|
||||
io.zipkin.Span result = ZipkinMessageListener.convert(span, host);
|
||||
|
||||
assertThat(result.annotations.get(0).endpoint)
|
||||
.isEqualTo(endpoint);
|
||||
assertThat(result.binaryAnnotations.get(0).endpoint)
|
||||
.isEqualTo(result.annotations.get(0).endpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* In zipkin, the service context is attached to annotations. Sleuth spans
|
||||
* that have no annotations will get an "lc" one, which allows them to be
|
||||
* queryable in zipkin by service name.
|
||||
*/
|
||||
@Test
|
||||
public void spanWithoutAnnotationsLogsComponent() {
|
||||
io.zipkin.Span result = ZipkinMessageListener.convert(span, host);
|
||||
|
||||
assertThat(result.binaryAnnotations)
|
||||
.containsOnly(BinaryAnnotation.create("lc", span.getProcessId(), endpoint));
|
||||
}
|
||||
|
||||
// TODO: "unknown" bc process id, documented as not nullable, is null in some tests.
|
||||
@Test
|
||||
public void nullProcessIdCoercesToUnknownServiceName() {
|
||||
MilliSpan noProcessId = MilliSpan.builder().traceId("xxxx").name("parent").remote(true).build();
|
||||
|
||||
io.zipkin.Span result = ZipkinMessageListener.convert(noProcessId, host);
|
||||
|
||||
assertThat(result.binaryAnnotations)
|
||||
.containsOnly(BinaryAnnotation.create("lc", "unknown", endpoint));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user