package com.yammer.telemetry.tracing;
import com.google.common.base.Optional;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import java.math.BigInteger;
import java.util.EmptyStackException;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
public class SpanTest {
@Rule
public SpanContextRule spanContextRule = new SpanContextRule();
@After
public void clearSpanSinkRegistry() {
SpanSinkRegistry.clear();
}
@Test
public void testRootSpan() {
final SpanSink sink = mock(SpanSink.class);
SpanSinkRegistry.register(sink);
final Span span = SpanHelper.startTrace("testSpan");
span.end();
verify(sink).record(span);
assertNotNull(span.getSpanId());
assertEquals(Optional.<BigInteger>absent(), span.getParentSpanId());
assertEquals("testSpan", span.getName());
assertTrue(span.getDuration() >= 0);
}
@Test
public void testRootlessSpans() {
final Span outer = SpanHelper.startTrace("outerSpan");
final Span inner = SpanHelper.startSpan("innerSpan");
inner.end();
outer.end();
}
@Test
public void testNestedSpan() {
final SpanSink sink = mock(SpanSink.class);
SpanSinkRegistry.register(sink);
final Span outer = SpanHelper.startTrace("outerSpan");
final Span inner = SpanHelper.startSpan("innerSpan");
inner.end();
verify(sink).record(inner);
outer.end();
verify(sink).record(outer);
assertEquals(Optional.of(outer.getSpanId()), inner.getParentSpanId());
}
@Test
public void testMultipleSinks() {
final SpanSink first = mock(SpanSink.class);
final SpanSink second = mock(SpanSink.class);
SpanSinkRegistry.register(first);
SpanSinkRegistry.register(second);
final Span span = SpanHelper.startTrace("testSpan");
span.end();
verify(first).record(span);
verify(second).record(span);
}
@Test
public void testMultipleThreads() throws InterruptedException {
final SpanSink sink = mock(SpanSink.class);
SpanSinkRegistry.register(sink);
final CyclicBarrier inside = new CyclicBarrier(2);
final Thread one = new Thread(new BarrierSpanRunner("threadOne", inside));
final Thread two = new Thread(new BarrierSpanRunner("threadTwo", inside));
one.start();
two.start();
one.join();
two.join();
final ArgumentCaptor<Span> captor = ArgumentCaptor.forClass(Span.class);
verify(sink, times(2)).record(captor.capture());
final List<Span> spans = captor.getAllValues();
assertEquals(2, spans.size());
assertEquals(Optional.<BigInteger>absent(), spans.get(0).getParentSpanId());
assertEquals(Optional.<BigInteger>absent(), spans.get(1).getParentSpanId());
}
@Test
public void testSpansClosedInIncorrectOrderClearsContextButDoesNotLogUnclosedSpans() {
final SpanSink sink = mock(SpanSink.class);
SpanSinkRegistry.register(sink);
Span trace = SpanHelper.startTrace("The Trace");
// These are deliberately not closed
SpanHelper.startSpan("one");
SpanHelper.startSpan("two");
trace.end();
assertTrue(SpanHelper.captureSpans().isEmpty());
verify(sink).record(trace);
verifyZeroInteractions(sink);
}
@Test(expected = EmptyStackException.class)
public void testEndingASpanMoreThanOnce() {
final SpanSink sink = mock(SpanSink.class);
SpanSinkRegistry.register(sink);
Span trace = SpanHelper.startTrace("The Trace");
trace.end();
assertTrue(SpanHelper.captureSpans().isEmpty());
verify(sink).record(trace);
trace.end();
}
@Test(expected = IllegalStateException.class)
public void testEndingASpanFromInvalidThreadContext() throws Throwable {
final SpanSink sink = mock(SpanSink.class);
SpanSinkRegistry.register(sink);
try (final Span trace = SpanHelper.startTrace("The Trace")) {
final CountDownLatch successLatch = new CountDownLatch(1);
final ArrayBlockingQueue<Throwable> expectedException = new ArrayBlockingQueue<>(1);
// contrived to allow ending a span in a different thread where the spanContext is unavailable.
new Thread(new Runnable() {
@Override
public void run() {
try {
trace.end();
successLatch.countDown();
} catch (Throwable t) {
expectedException.offer(t.fillInStackTrace());
}
}
}).start();
//noinspection ThrowableResultOfMethodCallIgnored
throw expectedException.poll(100, TimeUnit.MILLISECONDS);
}
}
private class BarrierSpanRunner implements Runnable {
private final String spanName;
private final CyclicBarrier barrier;
public BarrierSpanRunner(String spanName, CyclicBarrier barrier) {
this.spanName = spanName;
this.barrier = barrier;
}
@Override
public void run() {
try (Span ignored = SpanHelper.startTrace(spanName)) {
barrier.await();
} catch (Exception e) {
throw new RuntimeException("Problem tracing a span", e);
}
}
}
}