/**
* 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.storage;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import zipkin.Annotation;
import zipkin.BinaryAnnotation;
import zipkin.Constants;
import zipkin.DependencyLink;
import zipkin.Endpoint;
import zipkin.Span;
import zipkin.internal.ApplyTimestampAndDuration;
import zipkin.internal.CallbackCaptor;
import static java.util.Arrays.asList;
import static java.util.stream.Collectors.toList;
import static org.assertj.core.api.Assertions.assertThat;
import static zipkin.Constants.CLIENT_ADDR;
import static zipkin.Constants.CLIENT_RECV;
import static zipkin.Constants.CLIENT_SEND;
import static zipkin.Constants.LOCAL_COMPONENT;
import static zipkin.Constants.SERVER_ADDR;
import static zipkin.Constants.SERVER_RECV;
import static zipkin.Constants.SERVER_SEND;
import static zipkin.TestObjects.APP_ENDPOINT;
import static zipkin.TestObjects.DAY;
import static zipkin.TestObjects.DB_ENDPOINT;
import static zipkin.TestObjects.DEPENDENCIES;
import static zipkin.TestObjects.LINKS;
import static zipkin.TestObjects.TODAY;
import static zipkin.TestObjects.TRACE;
import static zipkin.TestObjects.WEB_ENDPOINT;
/**
* Base test for {@link SpanStore} implementations that support dependency aggregation. Subtypes
* should create a connection to a real backend, even if that backend is in-process.
*
* <p>This is a replacement for {@code com.twitter.zipkin.storage.DependencyStoreSpec}.
*/
public abstract class DependenciesTest {
/** Should maintain state between multiple calls within a test. */
protected abstract StorageComponent storage();
SpanStore store() {
return storage().spanStore();
}
/** Clears store between tests. */
@Before
public abstract void clear() throws IOException;
/**
* Override if dependency processing is a separate job: it should complete before returning from
* this method.
*/
protected void processDependencies(List<Span> spans) {
// Blocks until the callback completes to allow read-your-writes consistency during tests.
CallbackCaptor<Void> captor = new CallbackCaptor<>();
storage().asyncSpanConsumer().accept(spans, captor);
captor.get(); // block on result
}
/**
* Normally, the root-span is where trace id == span id and parent id == null. The default is to
* look back one day from today.
*/
@Test
public void getDependencies() {
processDependencies(TRACE);
assertThat(store().getDependencies(TODAY + 1000L, null))
.containsOnlyElementsOf(LINKS);
}
/**
* This tests that dependency linking ignores the high-bits of the trace ID when grouping spans
* for dependency links. This allows environments with 64-bit instrumentation to participate in
* the same trace as 128-bit instrumentation.
*/
@Test
public void getDependencies_strictTraceId() {
List<Span> mixedTrace = new ArrayList<>(TRACE);
mixedTrace.set(1, TRACE.get(1).toBuilder().traceIdHigh(2).build());
processDependencies(mixedTrace);
assertThat(store().getDependencies(TODAY + 1000L, null))
.containsOnlyElementsOf(LINKS);
}
/** It should be safe to run dependency link jobs twice */
@Test
public void replayOverwrites() {
processDependencies(TRACE);
processDependencies(TRACE);
assertThat(store().getDependencies(TODAY + 1000L, null))
.containsOnlyElementsOf(LINKS);
}
/** Edge-case when there are no spans, or instrumentation isn't logging annotations properly. */
@Test
public void empty() {
assertThat(store().getDependencies(TODAY + 1000L, null))
.isEmpty();
}
/**
* Trace id is not required to be a span id. For example, some instrumentation may create separate
* trace ids to help with collisions, or to encode information about the origin. This test makes
* sure we don't rely on the trace id = root span id convention.
*/
@Test
public void traceIdIsOpaque() {
List<Span> differentTraceId = TRACE.stream()
.map(s -> s.toBuilder().traceId(Long.MAX_VALUE).build())
.collect(toList());
processDependencies(differentTraceId);
assertThat(store().getDependencies(TODAY + 1000L, null))
.containsOnlyElementsOf(LINKS);
}
/**
* When all servers are instrumented, they all log a {@link Constants#SERVER_RECV ("sr")}
* annotation, indicating the service.
*/
@Test
public void getDependenciesAllInstrumented() {
Endpoint one = Endpoint.create("trace-producer-one", 127 << 24 | 1);
Endpoint onePort3001 = one.toBuilder().port((short) 3001).build();
Endpoint two = Endpoint.create("trace-producer-two", 127 << 24 | 2);
Endpoint twoPort3002 = two.toBuilder().port((short) 3002).build();
Endpoint three = Endpoint.create("trace-producer-three", 127 << 24 | 3);
List<Span> trace = asList(
Span.builder().traceId(10L).id(10L).name("get")
.timestamp(1445136539256150L).duration(1152579L)
.addAnnotation(Annotation.create(1445136539256150L, SERVER_RECV, one))
.addAnnotation(Annotation.create(1445136540408729L, SERVER_SEND, one))
.build(),
Span.builder().traceId(10L).parentId(10L).id(20L).name("get")
.timestamp(1445136539764798L).duration(639337L)
.addAnnotation(Annotation.create(1445136539764798L, CLIENT_SEND, onePort3001))
.addAnnotation(Annotation.create(1445136539816432L, SERVER_RECV, two))
.addAnnotation(Annotation.create(1445136540401414L, SERVER_SEND, two))
.addAnnotation(Annotation.create(1445136540404135L, CLIENT_RECV, onePort3001))
.build(),
Span.builder().traceId(10L).parentId(20L).id(30L).name("get")
.timestamp(1445136540025751L).duration(371298L)
.addAnnotation(Annotation.create(1445136540025751L, CLIENT_SEND, twoPort3002))
.addAnnotation(Annotation.create(1445136540072846L, SERVER_RECV, three))
.addAnnotation(Annotation.create(1445136540394644L, SERVER_SEND, three))
.addAnnotation(Annotation.create(1445136540397049L, CLIENT_RECV, twoPort3002))
.build()
);
processDependencies(trace);
long traceDuration = trace.get(0).duration;
assertThat(
store().getDependencies((trace.get(0).timestamp + traceDuration) / 1000, traceDuration / 1000)
).containsOnly(
DependencyLink.create("trace-producer-one", "trace-producer-two", 1),
DependencyLink.create("trace-producer-two", "trace-producer-three", 1)
);
}
/**
* Legacy instrumentation don't set Span.timestamp or duration. Make sure dependencies still work.
*/
@Test
public void getDependencies_noTimestamps() {
Endpoint one = Endpoint.create("trace-producer-one", 127 << 24 | 1);
Endpoint onePort3001 = one.toBuilder().port((short) 3001).build();
Endpoint two = Endpoint.create("trace-producer-two", 127 << 24 | 2);
Endpoint twoPort3002 = two.toBuilder().port((short) 3002).build();
Endpoint three = Endpoint.create("trace-producer-three", 127 << 24 | 3);
List<Span> trace = asList(
Span.builder().traceId(10L).id(10L).name("get")
.addAnnotation(Annotation.create(1445136539256150L, SERVER_RECV, one))
.addAnnotation(Annotation.create(1445136540408729L, SERVER_SEND, one))
.build(),
Span.builder().traceId(10L).parentId(10L).id(20L).name("get")
.addAnnotation(Annotation.create(1445136539764798L, CLIENT_SEND, onePort3001))
.addAnnotation(Annotation.create(1445136539816432L, SERVER_RECV, two))
.addAnnotation(Annotation.create(1445136540401414L, SERVER_SEND, two))
.addAnnotation(Annotation.create(1445136540404135L, CLIENT_RECV, onePort3001))
.build(),
Span.builder().traceId(10L).parentId(20L).id(30L).name("get")
.addAnnotation(Annotation.create(1445136540025751L, CLIENT_SEND, twoPort3002))
.addAnnotation(Annotation.create(1445136540072846L, SERVER_RECV, three))
.addAnnotation(Annotation.create(1445136540394644L, SERVER_SEND, three))
.addAnnotation(Annotation.create(1445136540397049L, CLIENT_RECV, twoPort3002))
.build()
);
processDependencies(trace);
long traceDuration = ApplyTimestampAndDuration.apply(trace.get(0)).duration;
assertThat(
store().getDependencies((trace.get(0).annotations.get(0).timestamp + traceDuration) / 1000, traceDuration / 1000)
).containsOnly(
DependencyLink.create("trace-producer-one", "trace-producer-two", 1),
DependencyLink.create("trace-producer-two", "trace-producer-three", 1)
);
}
/**
* The primary annotation used in the dependency graph is [[Constants.SERVER_RECV]]
*/
@Test
public void getDependenciesMultiLevel() {
processDependencies(TRACE);
assertThat(store().getDependencies(TODAY + 1000L, null))
.containsOnlyElementsOf(LINKS);
}
@Test
public void dependencies_loopback() {
List<Span> traceWithLoopback = asList(
TRACE.get(0),
TRACE.get(1).toBuilder()
.annotations(TRACE.get(1).annotations.stream()
.map(a -> Annotation.create(a.timestamp, a.value, WEB_ENDPOINT)).collect(toList()))
.binaryAnnotations(asList())
.build());
processDependencies(traceWithLoopback);
assertThat(store().getDependencies(TODAY + 1000L, null))
.containsOnly(DependencyLink.create("web", "web", 1));
}
/**
* Some systems log a different trace id than the root span. This seems "headless", as we won't
* see a span whose id is the same as the trace id.
*/
@Test
public void dependencies_headlessTrace() {
processDependencies(asList(TRACE.get(1), TRACE.get(2)));
assertThat(store().getDependencies(TODAY + 1000L, null))
.containsOnlyElementsOf(LINKS);
}
@Test
public void looksBackIndefinitely() {
processDependencies(TRACE);
assertThat(store().getDependencies(TODAY + 1000L, null))
.containsOnlyElementsOf(LINKS);
}
@Test
public void insideTheInterval() {
processDependencies(TRACE);
assertThat(store().getDependencies(DEPENDENCIES.endTs, DEPENDENCIES.endTs - DEPENDENCIES.startTs))
.containsOnlyElementsOf(LINKS);
}
@Test
public void endTimeBeforeData() {
processDependencies(TRACE);
assertThat(store().getDependencies(TODAY - DAY, null))
.isEmpty();
}
@Test
public void lookbackAfterData() {
processDependencies(TRACE);
assertThat(store().getDependencies(TODAY + 2 * DAY, DAY))
.isEmpty();
}
/**
* This test confirms that the span store can detect dependency indicated by SERVER_ADDR and
* CLIENT_ADDR. In some cases an RPC call is made where one of the two services is not
* instrumented. However, if the other service is able to emit "sa" or "ca" annotation with a
* service name, the link can still be constructed.
*
* span1: CA SR SS: Dependency 1 by a not-instrumented client span2: intermediate call span3: CS
* CR SA: Dependency 2 to a not-instrumented server
*/
@Test
public void notInstrumentedClientAndServer() {
Endpoint someClient = Endpoint.create("some-client", 172 << 24 | 17 << 16 | 4);
List<Span> trace = asList(
Span.builder().traceId(20L).id(20L).name("get")
.timestamp(TODAY * 1000).duration(350L * 1000)
.addAnnotation(Annotation.create(TODAY * 1000, SERVER_RECV, WEB_ENDPOINT))
.addAnnotation(Annotation.create((TODAY + 350) * 1000, SERVER_SEND, WEB_ENDPOINT))
.addBinaryAnnotation(BinaryAnnotation.address(CLIENT_ADDR, someClient))
.build(),
Span.builder().traceId(20L).parentId(20L).id(21L).name("get")
.timestamp((TODAY + 50L) * 1000).duration(250L * 1000)
.addAnnotation(Annotation.create((TODAY + 50) * 1000, CLIENT_SEND, WEB_ENDPOINT))
.addAnnotation(Annotation.create((TODAY + 100) * 1000, SERVER_RECV, APP_ENDPOINT))
.addAnnotation(Annotation.create((TODAY + 250) * 1000, SERVER_SEND, APP_ENDPOINT))
.addAnnotation(Annotation.create((TODAY + 300) * 1000, CLIENT_RECV, WEB_ENDPOINT))
.build(),
Span.builder().traceId(20L).parentId(21L).id(22L).name("get")
.timestamp((TODAY + 150L) * 1000).duration(50L * 1000)
.addAnnotation(Annotation.create((TODAY + 150) * 1000, CLIENT_SEND, APP_ENDPOINT))
.addAnnotation(Annotation.create((TODAY + 200) * 1000, CLIENT_RECV, APP_ENDPOINT))
.addBinaryAnnotation(BinaryAnnotation.address(CLIENT_ADDR, APP_ENDPOINT))
.addBinaryAnnotation(BinaryAnnotation.address(SERVER_ADDR, DB_ENDPOINT))
.build()
);
processDependencies(trace);
assertThat(store().getDependencies(TODAY + 1000L, null)).containsOnly(
DependencyLink.create("some-client", "web", 1),
DependencyLink.create("web", "app", 1),
DependencyLink.create("app", "db", 1)
);
}
@Test
public void instrumentedClientAndServer() {
List<Span> trace = asList(
Span.builder().traceId(10L).id(10L).name("get")
.timestamp((TODAY + 50L) * 1000).duration(250L * 1000)
.addAnnotation(Annotation.create((TODAY + 50) * 1000, CLIENT_SEND, WEB_ENDPOINT))
.addAnnotation(Annotation.create((TODAY + 100) * 1000, SERVER_RECV, APP_ENDPOINT))
.addAnnotation(Annotation.create((TODAY + 250) * 1000, SERVER_SEND, APP_ENDPOINT))
.addAnnotation(Annotation.create((TODAY + 300) * 1000, CLIENT_RECV, WEB_ENDPOINT))
.build(),
Span.builder().traceId(10L).parentId(10L).id(11L).name("get")
.timestamp((TODAY + 150L) * 1000).duration(50L * 1000)
.addAnnotation(Annotation.create((TODAY + 150) * 1000, CLIENT_SEND, APP_ENDPOINT))
.addAnnotation(Annotation.create((TODAY + 200) * 1000, CLIENT_RECV, APP_ENDPOINT))
.addBinaryAnnotation(BinaryAnnotation.address(CLIENT_ADDR, APP_ENDPOINT))
.addBinaryAnnotation(BinaryAnnotation.address(SERVER_ADDR, DB_ENDPOINT))
.build()
);
processDependencies(trace);
assertThat(store().getDependencies(TODAY + 1000L, null)).containsOnly(
DependencyLink.create("web", "app", 1),
DependencyLink.create("app", "db", 1)
);
}
/** Ensure there's no query limit problem around links */
@Test
public void manyLinks() {
int count = 1000; // Larger than 10, which is the default ES search limit that tripped this
List<Span> spans = new ArrayList<>(count);
for (int i = 1; i <= count; i++) {
Endpoint web = WEB_ENDPOINT.toBuilder().serviceName("web-" + i).build();
Endpoint app = APP_ENDPOINT.toBuilder().serviceName("app-" + i).build();
Endpoint db = DB_ENDPOINT.toBuilder().serviceName("db-" + i).build();
spans.add(Span.builder().traceId(i).id(10L).name("get")
.timestamp((TODAY + 50L) * 1000).duration(250L * 1000)
.addAnnotation(Annotation.create((TODAY + 50) * 1000, CLIENT_SEND, web))
.addAnnotation(Annotation.create((TODAY + 100) * 1000, SERVER_RECV, app))
.addAnnotation(Annotation.create((TODAY + 250) * 1000, SERVER_SEND, app))
.addAnnotation(Annotation.create((TODAY + 300) * 1000, CLIENT_RECV, web))
.build());
spans.add(Span.builder().traceId(i).parentId(10L).id(11L).name("get")
.timestamp((TODAY + 150L) * 1000).duration(50L * 1000)
.addAnnotation(Annotation.create((TODAY + 150) * 1000, CLIENT_SEND, app))
.addAnnotation(Annotation.create((TODAY + 200) * 1000, CLIENT_RECV, app))
.addBinaryAnnotation(BinaryAnnotation.address(SERVER_ADDR, db))
.build());
}
processDependencies(spans);
List<DependencyLink> links = store().getDependencies(TODAY + 1000L, null);
assertThat(links).hasSize(count * 2); // web-? -> app-?, app-? -> db-?
assertThat(links).extracting(l -> l.callCount)
.allSatisfy(callCount -> assertThat(callCount).isEqualTo(1));
}
/**
* This test confirms that the span store can detect dependency indicated by SERVER_RECV or
* SERVER_ADDR only. Some of implementations such as finagle don't send CLIENT_SEND and
* CLIENT_ADDR annotations as desired. However, if there is a SERVER_RECV or SERVER_ADDR
* annotation in the trace tree, the link can still be constructed.
*
* span1: SR SS: parent service span2: SA: Dependency 1
*
* Currently, the standard implentation can't detect a link with intermediate spans that should be
* detected.
*
* span1: SR SS: parent service span2: intermediate call span3: SR SS: Dependency 1 not detectable
* in the implementation
*/
@Test
public void noClientSendAddrAnnotations() {
List<Span> trace = asList(
Span.builder().traceId(20L).id(20L).name("get")
.timestamp(TODAY * 1000).duration(350L * 1000)
.addAnnotation(Annotation.create(TODAY * 1000, SERVER_RECV, WEB_ENDPOINT))
.addAnnotation(Annotation.create((TODAY + 350) * 1000, SERVER_SEND, WEB_ENDPOINT))
.binaryAnnotations(asList( // finagle also sends SA/CA itself
BinaryAnnotation.address(SERVER_ADDR, WEB_ENDPOINT),
BinaryAnnotation.address(CLIENT_ADDR, WEB_ENDPOINT)))
.build(),
Span.builder().traceId(20L).parentId(20L).id(21L).name("get")
.timestamp((TODAY + 150L) * 1000).duration(50L * 1000)
.addAnnotation(Annotation.create((TODAY + 150) * 1000, CLIENT_SEND, APP_ENDPOINT))
.addAnnotation(Annotation.create((TODAY + 200) * 1000, CLIENT_RECV, APP_ENDPOINT))
.binaryAnnotations(asList( // finagle also no SR on some condition and CA with itself
BinaryAnnotation.address(SERVER_ADDR, APP_ENDPOINT),
BinaryAnnotation.address(CLIENT_ADDR, APP_ENDPOINT)))
.build()
);
processDependencies(trace);
assertThat(store().getDependencies(TODAY + 1000L, null))
.containsOnly(DependencyLink.create("web", "app", 1));
}
/**
* This test shows that dependency links can be filtered at daily granularity. This allows the UI
* to look for dependency intervals besides TODAY.
*/
@Test
public void canSearchForIntervalsBesidesToday() {
// Let's pretend we have two days of data processed
// - Note: calling this twice allows test implementations to consider timestamps
processDependencies(subtractDay(TRACE));
processDependencies(TRACE);
// A user looks at today's links.
// - Note: Using the smallest lookback avoids bumping into implementation around windowing.
assertThat(store().getDependencies(DEPENDENCIES.endTs, DEPENDENCIES.endTs - DEPENDENCIES.startTs))
.containsOnlyElementsOf(LINKS);
// A user compares the links from those a day ago.
assertThat(store().getDependencies(DEPENDENCIES.endTs - DAY, DEPENDENCIES.endTs - DEPENDENCIES.startTs))
.containsOnlyElementsOf(LINKS);
// A user looks at all links since data started
assertThat(store().getDependencies(DEPENDENCIES.endTs, null)).containsOnly(
DependencyLink.create("web", "app", 2),
DependencyLink.create("app", "db", 2)
);
}
/** This test confirms that core ("sr", "cs", "cr", "ss") annotations are not required. */
@Test
public void noCoreAnnotations() {
Endpoint someClient = Endpoint.create("some-client", 172 << 24 | 17 << 16 | 4);
List<Span> trace = asList(
Span.builder().traceId(20L).id(20L).name("get")
.timestamp(TODAY * 1000).duration(350L * 1000)
.addBinaryAnnotation(BinaryAnnotation.address(CLIENT_ADDR, someClient))
.addBinaryAnnotation(BinaryAnnotation.address(SERVER_ADDR, WEB_ENDPOINT)).build(),
Span.builder().traceId(20L).parentId(20L).id(21L).name("get")
.timestamp((TODAY + 50) * 1000).duration(250L * 1000)
.addBinaryAnnotation(BinaryAnnotation.address(CLIENT_ADDR, WEB_ENDPOINT))
.addBinaryAnnotation(BinaryAnnotation.address(SERVER_ADDR, APP_ENDPOINT)).build(),
Span.builder().traceId(20L).parentId(21L).id(22L).name("get")
.timestamp((TODAY + 150) * 1000).duration(50L * 1000)
.addBinaryAnnotation(BinaryAnnotation.address(CLIENT_ADDR, APP_ENDPOINT))
.addBinaryAnnotation(BinaryAnnotation.address(SERVER_ADDR, DB_ENDPOINT)).build()
);
processDependencies(trace);
assertThat(store().getDependencies(TODAY + 1000, null)).containsOnly(
DependencyLink.create("some-client", "web", 1),
DependencyLink.create("web", "app", 1),
DependencyLink.create("app", "db", 1)
);
}
/** Some use empty string for the {@link Constants#CLIENT_ADDR} to defer naming to the server. */
@Test
public void noEmptyLinks() {
Endpoint someClient = Endpoint.create("", 172 << 24 | 17 << 16 | 4);
List<Span> trace = asList(
Span.builder().traceId(20L).id(20L).name("get")
.timestamp(TODAY * 1000).duration(350L * 1000)
.addBinaryAnnotation(BinaryAnnotation.address(CLIENT_ADDR, someClient))
.addBinaryAnnotation(BinaryAnnotation.address(SERVER_ADDR, WEB_ENDPOINT)).build(),
Span.builder().traceId(20L).parentId(20L).id(21L).name("get")
.timestamp((TODAY + 50) * 1000).duration(250L * 1000)
.addBinaryAnnotation(BinaryAnnotation.address(CLIENT_ADDR, WEB_ENDPOINT))
.addBinaryAnnotation(BinaryAnnotation.address(SERVER_ADDR, APP_ENDPOINT)).build(),
Span.builder().traceId(20L).parentId(21L).id(22L).name("get")
.timestamp((TODAY + 150) * 1000).duration(50L * 1000)
.addBinaryAnnotation(BinaryAnnotation.address(CLIENT_ADDR, APP_ENDPOINT))
.addBinaryAnnotation(BinaryAnnotation.address(SERVER_ADDR, DB_ENDPOINT)).build()
);
processDependencies(trace);
assertThat(store().getDependencies(TODAY + 1000, null)).containsOnly(
DependencyLink.create("web", "app", 1),
DependencyLink.create("app", "db", 1)
);
}
/**
* This test confirms that the span store can process trace with intermediate spans like the below
* properly.
*
* span1: SR SS span2: intermediate call span3: CS SR SS CR: Dependency 1
*/
@Test
public void intermediateSpans() {
List<Span> trace = asList(
Span.builder().traceId(20L).id(20L).name("get")
.timestamp(TODAY * 1000).duration(350L * 1000)
.addAnnotation(Annotation.create(TODAY * 1000, SERVER_RECV, WEB_ENDPOINT))
.addAnnotation(Annotation.create((TODAY + 350) * 1000, SERVER_SEND, WEB_ENDPOINT)).build(),
Span.builder().traceId(20L).parentId(20L).id(21L).name("call")
.timestamp((TODAY + 25) * 1000).duration(325L * 1000)
.addBinaryAnnotation(
BinaryAnnotation.create(LOCAL_COMPONENT, "depth2", WEB_ENDPOINT)).build(),
Span.builder().traceId(20L).parentId(21L).id(22L).name("get")
.timestamp((TODAY + 50) * 1000).duration(250L * 1000)
.addAnnotation(Annotation.create((TODAY + 50) * 1000, CLIENT_SEND, WEB_ENDPOINT))
.addAnnotation(Annotation.create((TODAY + 100) * 1000, SERVER_RECV, APP_ENDPOINT))
.addAnnotation(Annotation.create((TODAY + 250) * 1000, SERVER_SEND, APP_ENDPOINT))
.addAnnotation(Annotation.create((TODAY + 300) * 1000, CLIENT_RECV, WEB_ENDPOINT)).build(),
Span.builder().traceId(20L).parentId(22L).id(23L).name("call")
.timestamp((TODAY + 110) * 1000).duration(130L * 1000)
.addBinaryAnnotation(
BinaryAnnotation.create(LOCAL_COMPONENT, "depth4", APP_ENDPOINT)).build(),
Span.builder().traceId(20L).parentId(23L).id(24L).name("call")
.timestamp((TODAY + 125) * 1000).duration(105L * 1000)
.addBinaryAnnotation(
BinaryAnnotation.create(LOCAL_COMPONENT, "depth5", APP_ENDPOINT)).build(),
Span.builder().traceId(20L).parentId(24L).id(25L).name("get")
.timestamp((TODAY + 150) * 1000).duration(50L * 1000)
.addAnnotation(Annotation.create((TODAY + 150) * 1000, CLIENT_SEND, APP_ENDPOINT))
.addAnnotation(Annotation.create((TODAY + 200) * 1000, CLIENT_RECV, APP_ENDPOINT))
.addBinaryAnnotation(BinaryAnnotation.address(SERVER_ADDR, DB_ENDPOINT)).build()
);
processDependencies(trace);
assertThat(store().getDependencies(TODAY + 1000, null)).containsOnly(
DependencyLink.create("web", "app", 1),
DependencyLink.create("app", "db", 1)
);
}
/**
* This test confirms that the span store can process trace with intermediate spans like the below
* properly.
*
* span1: SR SS span2: intermediate call span3: CS SR SS CR: Dependency 1
*/
@Test
public void duplicateAddress() {
List<Span> trace = asList(
Span.builder().traceId(20L).id(20L).name("get")
.timestamp(TODAY * 1000).duration(350L * 1000)
.addAnnotation(Annotation.create(TODAY * 1000, SERVER_RECV, WEB_ENDPOINT))
.addAnnotation(Annotation.create((TODAY + 350) * 1000, SERVER_SEND, WEB_ENDPOINT))
.addBinaryAnnotation(BinaryAnnotation.address(CLIENT_ADDR, WEB_ENDPOINT))
.addBinaryAnnotation(BinaryAnnotation.address(SERVER_ADDR, WEB_ENDPOINT)).build(),
Span.builder().traceId(20L).parentId(21L).id(22L).name("get")
.timestamp((TODAY + 50) * 1000).duration(250L * 1000)
.addAnnotation(Annotation.create((TODAY + 50) * 1000, CLIENT_SEND, WEB_ENDPOINT))
.addAnnotation(Annotation.create((TODAY + 300) * 1000, CLIENT_RECV, WEB_ENDPOINT))
.addBinaryAnnotation(BinaryAnnotation.address(CLIENT_ADDR, APP_ENDPOINT))
.addBinaryAnnotation(BinaryAnnotation.address(SERVER_ADDR, APP_ENDPOINT)).build()
);
processDependencies(trace);
assertThat(store().getDependencies(TODAY + 1000, null)).containsOnly(
DependencyLink.create("web", "app", 1)
);
}
@Test
public void unmergedSpans() {
List<Span> trace = asList(
Span.builder().traceId(1L).parentId(1L).id(2L).name("get").timestamp((TODAY + 100) * 1000)
.addAnnotation(Annotation.create((TODAY + 100) * 1000, SERVER_RECV, APP_ENDPOINT))
.addAnnotation(Annotation.create((TODAY + 250) * 1000, SERVER_SEND, APP_ENDPOINT))
.addBinaryAnnotation(BinaryAnnotation.address(CLIENT_ADDR, WEB_ENDPOINT))
.build(),
Span.builder().traceId(1L).parentId(1L).id(2L).name("get").timestamp((TODAY + 50) * 1000)
.addAnnotation(Annotation.create((TODAY + 50) * 1000, CLIENT_SEND, WEB_ENDPOINT))
.addAnnotation(Annotation.create((TODAY + 300) * 1000, CLIENT_RECV, WEB_ENDPOINT))
.addBinaryAnnotation(BinaryAnnotation.address(SERVER_ADDR, APP_ENDPOINT))
.build()
);
processDependencies(trace);
assertThat(store().getDependencies(TODAY + 1000, null)).containsOnly(
DependencyLink.create("web", "app", 1)
);
}
/**
* Span starts on one host and ends on the other. In both cases, a response is neither sent nor
* received.
*/
@Test
public void oneway() {
List<Span> trace = asList(
Span.builder().traceId(10L).id(10L).name("")
.addAnnotation(Annotation.create((TODAY + 50) * 1000, CLIENT_SEND, WEB_ENDPOINT))
.addAnnotation(Annotation.create((TODAY + 100) * 1000, SERVER_RECV, APP_ENDPOINT))
.build()
);
processDependencies(trace);
assertThat(store().getDependencies(TODAY + 1000L, null)).containsOnly(
DependencyLink.create("web", "app", 1)
);
}
/** Async span starts from an uninstrumented source. */
@Test
public void oneway_noClient() {
Endpoint kafka = Endpoint.create("kafka", 172 << 24 | 17 << 16 | 4);
List<Span> trace = asList(
Span.builder().traceId(10L).id(10L).name("receive")
.addAnnotation(Annotation.create((TODAY) * 1000, SERVER_RECV, APP_ENDPOINT))
.addBinaryAnnotation(BinaryAnnotation.address(CLIENT_ADDR, kafka))
.build(),
Span.builder().traceId(10L).parentId(10L).id(11L).name("process")
.timestamp((TODAY + 25) * 1000).duration(325L * 1000)
.addBinaryAnnotation(BinaryAnnotation.create(LOCAL_COMPONENT, "", APP_ENDPOINT)).build()
);
processDependencies(trace);
assertThat(store().getDependencies(TODAY + 1000L, null)).containsOnly(
DependencyLink.create("kafka", "app", 1)
);
}
/** rebases a trace backwards a day with different trace and span id. */
List<Span> subtractDay(List<Span> trace) {
return trace.stream()
.map(s -> s.toBuilder()
.traceId(s.traceId + 100)
.parentId(s.parentId != null ? s.parentId + 100 : null)
.id(s.id + 100)
.timestamp(s.timestamp != null ? s.timestamp - (DAY * 1000) : null)
.annotations(s.annotations.stream()
.map(a -> Annotation.create(a.timestamp - (DAY * 1000), a.value, a.endpoint))
.collect(toList()))
.build()
).collect(toList());
}
}