In this second part of OpenTracing 5 part series, we’ll dive into the fundamental building blocks that underpin the inner workings of any OpenTracing-compatible tracer. As we already mentioned, OpenTracing is a kind of whitebox instrumentation and thus it requires code modifications to properly hook in trace points in strategic places throughout the code base.
Developers wrap code blocks using various constructs from OpenTracing API to start a new span when code execution is triggered, attach tags or establish relationships between parent and children spans.
OpenTracing basics and terminology explained
OpenTracing API is modeled around two fundamental types:
- Tracer – knows how to create a new span as well as inject/extract span contexts across process boundaries. All OpenTracing compatible tracers must provide a client with the implementation of the Tracer interface.
- Span – tracer’s build method yields a brand new created span. We can invoke a number of operations after the span has been started, like aggregating tags, changing span’s operation name, binding references to other spans, adding baggage items, etc.
SpanContext – the consumers of the API only interact with this type when injecting/extracting the span context from the transport protocol.
Traces, spans, and edges
In a distributed system, a trace encapsulates the transaction’s state as it propagates through the system.
During the journey of the transaction, it can create one or multiple spans. A span represents a single unit of work inside a transaction, for example, an RPC client/server call, sending query to the database server, or publishing a message to the message bus. Speaking in terms of OpenTracing data model, the trace can also be seen as a collection of spans structured around the directed acyclic graph (DAG).
The edges indicate the causal relationships (references) between spans. The span is identified by its unique ID, and optionally may include the parent identifier. If the parent identifier is omitted, we call that span as root span. The span also comprises human-readable operation name, start and end timestamps. All spans are grouped under the same trace identifier.
Spans may contain tags that represent contextual metadata relevant to a specific request. They consist of an unbounded sequence of key-value pairs, where keys are strings and values can be strings, numbers, booleans or date data types. Tags allow for context enrichment that may be useful for monitoring or debugging system behavior. While not mandatory, it’s highly recommended to follow the OpenTracing semantics guidelines when naming tags. Such as that, we should assign component tag to the framework, module or library which generates span/spans, use peer.hostname and peer.port to describe target hosts, etc. Another reason for tagging standardization is making the tracer aware of the existence of certain tags that would add intelligence or instruct the tracer to put special emphasis on them.
OpenTracing logging /log events
Besides tags, OpenTracing has a notion of log events.
They represent timestamped textual (although not limited to textual content) annotations that may be recorded along the duration of a span. Events could express any occurrence of interest to the active span, like timer expiration, cache miss events, build or deployment starting events, etc.
Baggage items allow for cross-span propagation, i.e., they let associate metadata that also propagates to future children of the root span. In other words, the local data is transported along the full path as request if traveling downstream through the system.
However, this powerful feature should be used carefully because it can easily saturate network links if the propagated items are about to be injected into many descendant spans. As at the time of writing, OpenTracing supports two types of relationships:
- ChildOf – to express casual references between two spans. Following with our RPC scenario, the server side span would be the ChildOf the initiator (request) span.
- FollowsFrom – when parent span isn’t linked to the outcome of the child span. This relationship is usually used to model asynchronous executions like emitting messages to the message bus.
OpenTracing has its own jargon and a set of concepts that we should become familiar with in order to fully understand and exploit its capabilities. Tags make it possible to attach any piece of metadata to outbound span, and we can even bind events and logs to the particular lifespan.
Baggage items are a powerful feature that allows for the propagation of common tags from the root span to any downstream children spans. Finally, we also scratched the surface of OpenTracing relationship model and saw how it helps to highlight the dependencies between different distributed services.
Since the job of the OpenTracing API is to hide the differences between distributed tracer implementations, so you can easily swap them out at any time without needing to change your instrumentation, one should look into OpenTracing-compatible distributed tracers options.
Zipkin and Jaeger are both such OpenTracing-compatible distributed tracers. If you want to learn more about them, we covered Zipkin as OpenTracing-compatible Distributed Tracer, followed by Jaeger as distributed tracer and also did a comparison of Jaeger vs Zipkin head to head!
If you are looking for a Full Stack Observability platform that brings together traces, logs, metrics, and user monitoring, have a look at Sematext Cloud or, if you need to keep your observability on site, at Sematext Enterprise. Sematext Tracing provides end to end visibility into your distributed applications so you can find bottlenecks quickly, resolve production issues faster and with less effort. Interested in trying it out? Sign up today for a free exclusive beta invite by clicking here.
We would love to hear your opinions, experiments or anything else you would like to share regarding OpenTracing ecosystem. Before you go, don’t forget to download your OpenTracing eBook: Distributed Tracing’s Emerging Industry Standard.