Ask Miss O11y: Making Sense of OpenTelemetry—Tracer and TracerProvider

5 Min. Read

“There’s a lot to understand in OpenTelemetry; for instance, what is the difference between a Tracer and a TracerProvider? How should I use them?”

OpenTelemetry is a strong standard for instrumentation because it is built of careful, well-thought-out abstractions created by experts in the space.

OpenTelemetry feels painful to start using because it’s full of abstractions that make sense to experts in the space. For a developer who wants to think about their own software and not spend a month becoming an expert in telemetry, this is hard.

For high-level conceptual description, there’s the OpenTelemetry specification. For basic what-to-type, there’s the getting-started guides for various languages (Go, JavaScript, and more).

For the in-between of “How does this all connect?” and “What does it mean?” Miss O11y is here for you!

TL;DR: a TracerProvider (usually there’s exactly one) knows how to send telemetry out. It can give you a Tracer, which lets you construct spans.

In our quest to understand what’s going on in our applications, we want observability, which requires our app to send telemetry data, which happens in instrumentation code.

Instrumentation code does two things: construct telemetry and send telemetry
Instrumentation code does two things: construct telemetry and send telemetry

Construct traces

We construct spans with a Tracer. The spec doesn’t say exactly how because each language uses idiomatic method names.

In JavaScript, you might call `tracer.startSpan(“span name”)` or `tracer.startActiveSpan(“span name”, cb)`. Which you choose affects the way spans get connected.

In Go, you use `tracer.startSpan(ctx, “span name”)`. Which context `ctx` you pass determines how spans get connected.

Where does that Tracer come from?

From the air! The air inside the OpenTelemetry API, that is.

The syntax is different in every language, but the steps are usually: import the OpenTelemetry API and ask it for a Tracer.

In JavaScript:

```

const opentelemetry = require(“@opentelemetry/api”);

const tracer  = opentelemetry.trace.getTracer(“my package name”);

```

In Go:

```

import (

"go.opentelemetry.io/otel"

"go.opentelemetry.io/otel/attribute"

"go.opentelemetry.io/otel/trace"

)

…

tracer := otel.Tracer(“my package name”)

```

The name that you pass in becomes the `library.name` field in all spans created by the Tracer. The meaning of this field is: Whose instrumentation created the span? So use the name of your application or of the current package. If you’re writing a library, definitely use that name.

How many Tracers should you have? Any number. Ask for one in every file, or every function, or every time you want to create a span. This is fine.

No really, where does that Tracer come from?

If the only OpenTelemetry packages you bring in are from the API, then you get a default. It’s a no-op Tracer—when you create a span, it does nothing.

That way, if you publish a library with its own instrumentation (for instance: Apollo GraphQL), and people use it in applications that don’t set up OpenTelemetry, nothing breaks. The OpenTelemetry API packages contain only no-op versions of the abstractions it exposes. You can write code against them, but it won’t do anything until someone includes code to send the telemetry.

Send traces

To activate all that telemetry-construction code, bring in more than the OpenTelemetry API. Bring in an implementation, usually in the form of the OpenTelemetry SDK for that language.

Then configure a TracerProvider that knows how to send telemetry out.

See, when your span-constructing instrumentation asks for a Tracer, the API is getting one from the global TracerProvider. (You knew it! You could guess it from the name!)

To construct real spans and transmit them to your observability platform, configure a real TracerProvider in initialization of your application.

This usually means creating at least an Exporter, a SpanProcessor, a Resource. Those are out of scope for this post. The syntax is different in each language. The (many) packages you need are different in each language. The examples in the documentation are a starting point. Then the OpenTelemetry registry can help you find instrumentation libraries helpful to your specific app.

Your goal is to configure a TracerProvider and then install it as the global one. You get one TracerProvider for the whole application. It may be possible to have more, but it would be strange.

For instance, in Node.js, you can instantiate a `NodeTracerProvider`, configure its exporters, and then call `provider.register()` to install it globally. (In Node, you can instead initialize the SDK with `new SDK(…)` which does this internally.) My favorite example for this, which we keep up to date, is here.

In Go, you’ll create a TracerProvider and then call `otel.SetTracerProvider(tp)`. An example for connecting to Honeycomb is here.

OpenTelemetry separates constructing telemetry from sending telemetry. You get a Tracer (or many) from a TracerProvider (a single global instance). The Tracer knows how to make spans, and it has a link back to its TracerProvider, which knows how to send them.
OpenTelemetry separates constructing telemetry from sending telemetry. You get a Tracer (or many) from a TracerProvider (a single global instance). The Tracer knows how to make spans, and it has a link back to its TracerProvider, which knows how to send them.

That’s two of the OpenTelemetry abstractions. Want to hear about others? Send us a question!

Got telemetry, want to see it clearly? Sign up for a free Honeycomb account!

Don’t forget to share!
Jessica Kerr

Jessica Kerr

Manager, Developer Relations

Jess is a symmathecist, in the medium of code. She sees development teams as learning systems made of people and running software. If we make that software teach us what’s happening, it’s a better teammate. And if this process makes us into systems thinkers, we can be better persons in the world.

Related posts