opentelemetry-java-instrume.../dd-trace/src/test/groovy/datadog/trace/tracer/SpanImplTest.groovy

375 lines
11 KiB
Groovy

package datadog.trace.tracer
import com.fasterxml.jackson.databind.ObjectMapper
import datadog.trace.api.DDTags
import spock.lang.Specification
class SpanImplTest extends Specification {
private static final String SERVICE_NAME = "service.name"
private static final String PARENT_TRACE_ID = "trace id"
private static final String PARENT_SPAN_ID = "span id"
private static final long START_TIME = 100
private static final long DURATION = 321
def interceptors = [Mock(name: "interceptor-1", Interceptor), Mock(name: "interceptor-2", Interceptor)]
def tracer = Mock(Tracer) {
getDefaultServiceName() >> SERVICE_NAME
getInterceptors() >> interceptors
}
def parentContext = Mock(SpanContextImpl) {
getTraceId() >> PARENT_TRACE_ID
getSpanId() >> PARENT_SPAN_ID
}
def startTimestamp = Mock(Timestamp) {
getTime() >> START_TIME
getDuration() >> DURATION
getDuration(_) >> { args -> args[0] + DURATION }
}
def trace = Mock(TraceImpl) {
getTracer() >> tracer
}
ObjectMapper objectMapper = new ObjectMapper()
def "test setters and default values"() {
when: "create span"
def span = new SpanImpl(trace, parentContext, startTimestamp)
then: "span got created"
span.getTrace() == trace
span.getStartTimestamp() == startTimestamp
span.getDuration() == null
span.getContext().getTraceId() == PARENT_TRACE_ID
span.getContext().getParentId() == PARENT_SPAN_ID
span.getContext().getSpanId() ==~ /\d+/
span.getTraceId() == PARENT_TRACE_ID
span.getParentId() == PARENT_SPAN_ID
span.getSpanId() == span.getContext().getSpanId()
span.getService() == SERVICE_NAME
span.getResource() == null
span.getType() == null
span.getName() == null
!span.isErrored()
when: "span settings changes"
span.setService("new.service.name")
span.setResource("resource")
span.setType("type")
span.setName("name")
span.setErrored(true)
then: "span fields get updated"
span.getService() == "new.service.name"
span.getResource() == "resource"
span.getType() == "type"
span.getName() == "name"
span.isErrored()
}
def "test setter #setter on finished span"() {
setup: "create span"
def span = new SpanImpl(trace, parentContext, startTimestamp)
span.finish()
when: "call setter on finished span"
span."$setter"(newValue)
then: "error reported"
1 * tracer.reportError(_, {
it[0] == fieldName
it[1] == span
})
and: "value unchanged"
span."$getter"() == oldValue
where:
fieldName | setter | getter | newValue | oldValue
"service" | "setService" | "getService" | "new service" | SERVICE_NAME
"resource" | "setResource" | "getResource" | "new resource" | null
"type" | "setType" | "getType" | "new type" | null
"name" | "setName" | "getName" | "new name" | null
"errored" | "setErrored" | "isErrored" | true | false
}
def "test meta set and remove for #key"() {
when:
def span = new SpanImpl(trace, parentContext, startTimestamp)
then:
span.getMeta(key) == null
when:
span.setMeta(key, value)
then:
span.getMeta(key) == value
when:
span.setMeta(key, value.class.cast(null))
then:
span.getMeta(key) == null
where:
key | value
"string.key" | "string"
"boolean.key" | true
"number.key" | 123
}
def "test getMeta"() {
setup:
def span = new SpanImpl(trace, parentContext, startTimestamp)
when:
span.setMeta("number.key", 123)
span.setMeta("string.key", "meta string")
span.setMeta("boolean.key", true)
then:
span.getMeta() == ["number.key": "123", "string.key": "meta string", "boolean.key": "true"]
}
def "test meta setter on finished span for #key"() {
setup: "create span"
def span = new SpanImpl(trace, parentContext, startTimestamp)
span.finish()
when: "call setter on finished span"
span.setMeta(key, value)
then: "error reported"
1 * tracer.reportError(_, {
it[0] == "meta value " + key
it[1] == span
})
and: "value unchanged"
span.getMeta(key) == null
where:
key | value
"string.key" | "string"
"boolean.key" | true
"number.key" | 123
}
def "test attachThrowable"() {
setup:
def exception = new RuntimeException("test message")
when:
def span = new SpanImpl(trace, parentContext, startTimestamp)
then:
!span.isErrored()
span.getMeta(DDTags.ERROR_MSG) == null
span.getMeta(DDTags.ERROR_TYPE) == null
span.getMeta(DDTags.ERROR_STACK) == null
when:
span.attachThrowable(exception)
then:
span.isErrored()
span.getMeta(DDTags.ERROR_MSG) == "test message"
span.getMeta(DDTags.ERROR_TYPE) == RuntimeException.getName()
span.getMeta(DDTags.ERROR_STACK) != null
}
def "test attachThrowable on finished span"() {
setup: "create span"
def exception = new RuntimeException("test message")
def span = new SpanImpl(trace, parentContext, startTimestamp)
span.finish()
when: "attach throwable"
span.attachThrowable(exception)
then: "error reported"
1 * tracer.reportError(_, {
it[0] == "throwable"
it[1] == span
})
and: "span unchanged"
!span.isErrored()
span.getMeta(DDTags.ERROR_MSG) == null
span.getMeta(DDTags.ERROR_TYPE) == null
span.getMeta(DDTags.ERROR_STACK) == null
}
def "test no parent"() {
when:
def span = new SpanImpl(trace, null, startTimestamp)
then:
span.getContext().getTraceId() ==~ /\d+/
span.getContext().getParentId() == SpanContextImpl.ZERO
span.getContext().getSpanId() ==~ /\d+/
}
def "test lifecycle '#name'"() {
when: "create span"
def span = new SpanImpl(trace, parentContext, startTimestamp)
then: "interceptors called"
interceptors.each({ interceptor ->
then:
1 * interceptor.afterSpanStarted({
// Apparently invocation verification has to know expected value before 'when' section
// To work around this we just check parent span id
it.getContext().getParentId() == parentContext.getSpanId()
})
0 * interceptor._
})
then:
!span.isFinished()
when: "finish/finalize span"
span."$method"(*methodArgs)
then: "warning is reported"
_ * trace.getTracer() >> tracer
if (finalizeErrorReported) {
1 * tracer.reportWarning(_, span)
}
then: "interceptors called"
interceptors.reverseEach({ interceptor ->
then:
1 * interceptor.beforeSpanFinished({
it == span
it.getDuration() == null // Make sure duration is not set yet
})
0 * interceptor._
})
then: "trace is informed that span is closed"
1 * trace.finishSpan({
it == span
it.getDuration() == expectedDuration
}, finalizeErrorReported)
0 * trace._
span.isFinished()
when: "try to finish span again"
span.finish(*secondFinishCallArgs)
then: "interceptors are not called"
interceptors.each({ interceptor ->
0 * interceptor._
})
and: "trace is not informed"
0 * trace.finishSpan(_, _)
and: "error is reported"
1 * tracer.reportError(_, span)
where:
name | method | methodArgs | expectedDuration | finalizeErrorReported | secondFinishCallArgs
"happy" | "finish" | [] | DURATION | false | []
"happy" | "finish" | [] | DURATION | false | [222]
"happy with timestamp" | "finish" | [111] | DURATION + 111 | false | [222]
"happy with timestamp" | "finish" | [111] | DURATION + 111 | false | []
// Note: doing GC tests with mocks is nearly impossible because mocks hold all sort of references
// We will do 'real' GC test as part of integration testing, this is more of a unit test
"finalized" | "finalize" | [] | DURATION | true | []
"finalized" | "finalize" | [] | DURATION | true | [222]
}
def "finished span GCed without errors"() {
setup: "create span"
def span = new SpanImpl(trace, parentContext, startTimestamp)
when: "finish span"
span.finish()
then:
span.isFinished()
0 * tracer.reportError(_, *_)
when: "finalize span"
span.finalize()
then:
0 * tracer.reportError(_, *_)
}
def "finalize catches all exceptions"() {
setup:
def span = new SpanImpl(trace, parentContext, startTimestamp)
when:
span.finalize()
then:
1 * startTimestamp.getDuration() >> { throw new Throwable() }
noExceptionThrown()
}
def "span without timestamp"() {
when:
new SpanImpl(trace, parentContext, null)
then:
thrown TraceException
}
def "test JSON rendering"() {
setup: "create span"
def parentContext = new SpanContextImpl("123", "456", "789")
def span = new SpanImpl(trace, parentContext, startTimestamp)
span.setResource("test resource")
span.setType("test type")
span.setName("test name")
span.setMeta("number.key", 123)
span.setMeta("string.key", "meta string")
span.setMeta("boolean.key", true)
span.finish()
when: "convert to JSON"
def string = objectMapper.writeValueAsString(span)
def parsedSpan = objectMapper.readerFor(JsonSpan).readValue(string)
then:
parsedSpan == new JsonSpan(span)
}
def "test JSON rendering with throwable"() {
setup: "create span"
def parentContext = new SpanContextImpl("123", "456", "789")
def span = new SpanImpl(trace, parentContext, startTimestamp)
span.attachThrowable(new RuntimeException("test"))
span.finish()
when: "convert to JSON"
def string = objectMapper.writeValueAsString(span)
def parsedSpan = objectMapper.readerFor(JsonSpan).readValue(string)
then:
parsedSpan == new JsonSpan(span)
}
def "test JSON rendering with big ID values"() {
setup: "create span"
def parentContext = new SpanContextImpl(
new BigInteger(2).pow(64).subtract(1).toString(),
"123",
new BigInteger(2).pow(64).subtract(2).toString())
def span = new SpanImpl(trace, parentContext, startTimestamp)
span.finish()
when: "convert to JSON"
def string = objectMapper.writeValueAsString(span)
def parsedSpan = objectMapper.readValue(string, JsonSpan)
then:
parsedSpan == new JsonSpan(span)
when:
def json = objectMapper.readTree(string)
then: "make sure ids rendered as number"
json.get("trace_id").isNumber()
json.get("parent_id").isNumber()
json.get("span_id").isNumber()
}
}