Objective
Have a standard test suite that can take 2 or more "node", and execute a series of transactions that traverse each system, with the purpose of verifying that they exchanged tracing headers in such a way that they were able to interoperate.
Benefits
By defining the tests and expectations described below we essentially define functional requirements of what we expect to happen in different interop scenarios, something that we're currently missing from the spec and which, imo, causes many round-about discussions of implementations without clear requirements.
Details
Node
Represents a microservice instrumented with a certain tracing library / implementation. Comes packaged as a Docker container that internally runs the tracing backend (or a proxy) and a small app that:
a. has a /transaction
endpoint that executes the test case transaction
b. has an /introspection
endpoint used by the test suite driver to verify that the respective tracing backend has captured the trace
Transactions
A transaction is described as a recursive set of instructions to call the next Node in the chain or to stop. E.g. it might look like
{
callNext: {
host: "zipkin", // name of the Node container running ZIpkin app, reachable via this host name
callNext: {
host: "jaeger",
callNext: null
}
}
}
Running this transaction would execute a chain zipkin -> jaeger
. When a Node receives such request, it looks for the nested callNext
fragment and calls the next Node with that nested (smaller) payload. The last node will receive an empty request so it simply returns.
There also can be a convention that each Node's response contains the trace/span ID it observed/generated, again as a recursive structure, e.g.
{
traceId: "...",
spanId: "...",
next: {
traceId: "...",
spanId: "...",
next: null
}
This would allow the test driver to interrogate the introspection endpoint using those IDs.
Verifications
Test suite driver calls /introspection
endpoint for each Node to retrieve captured trace(s) in some canonical form (just enough info for the test). If /transaction responses contain trace/span Id, it can do some validation.
Test Suite
The test suite is defined as a list of scenarions, e.g.
- vendor1, vendor1, vendor1 (i.e. a single vendor site)
- vendor1, vendor2, vendor1 (cross-site transaction)
- etc.
Each scenario is instantiated multiple times (test cases) by labelling different vendors with roles from the scenario, e.g.
- scenario 1, test case 1: vendor1 = zipkin
- scenario 1, test case 2: vendor1 = jaeger
- scenario 2, test case 1: vendor1 = zipkin, vendor2=jaeger
- etc.
Each test case runs and validates a single transaction, and checks different modes of participation in the trace.
Parameterization
The test suite framework can be also used to test multiple implementations of the tracing library from a given vendor, e.g. in different languages. This can be implemented as either different Node containers (e.g. zipkin_java, zipkin_go), or a single container controlled by env variables.
Participation Modes
The nodes can also support different trace participation modes, at minimum:
- respect and reuse incoming trace ID
- record incoming trace ID as correlation field, but don't trust it, start a new trace
If the test driver knows ahead of time which participation mode a given Node supports (these can again be parameters to the Node), it can validate the expected behavior.
Prerequisites
Each vendor must be able to provide a Docker image (or several) to act as a Node in the test suite. Ideally the containers should be fully self-contained, i.e. do not require external connectivity. It's possible to implement them as proxies to hosted tracing backends if necessary, but it will make the tests less reliable if those hosted backends are unavailable at times.
It's crazy / impossible
Jaeger internally uses an approach very similar to this one for many of its integration tests, in particular those that test compatibility of client libraries in different languages. Uber released a framework https://github.com/crossdock/crossdock that helps orchestrating these tests and permutations using docker-compose.