TracerProvider, MeterProvider and LoggerProvider in OpenTelemetry
The three central desks of every OpenTelemetry setup. They hand out the tools that record traces, metrics and logs, and they hold all the rules about where that data goes. Learn what each one does, how they fit together, and how to set them up without losing data.
Imagine a large company with three note-taking departments. One keeps a diary of journeys, noting every step a customer’s order takes. One keeps a counter board with running totals, such as orders per minute. One keeps a logbook of events, such as “payment failed at 10:42”. Employees do not make their own notebooks. When they need one, they walk to a central stationery desk and ask for it. The desk knows which department’s rules apply, which stamp to put on the page, and where every filled notebook must be sent at the end of the day.
OpenTelemetry works in much the same way. Your code asks for a Tracer to record traces, a Meter to record metrics, and a Logger to record logs. But it does not build them itself. It asks a central desk, called a provider. There are three of them: the TracerProvider, the MeterProvider and the LoggerProvider. Each holds the settings for one signal, and each decides how the data it collects will be processed and sent away.
In this guide we start from zero. We explain why providers exist, what is inside each one, how they share an identity, and how they are created, registered, used and shut down. We look at how they work internally, how they are named in different languages, and how to avoid the mistakes that cause lost or confusing data. Every new term is explained when it first appears, and each big idea comes with an everyday comparison.
1What Are the Three Providers?
A provider is the starting point for one kind of telemetry. It hands out the tools your code uses and carries the settings that shape everything those tools produce.
OpenTelemetry is an open-source project that gives software a standard way to produce telemetry, the information a program sends out about itself. Telemetry comes in several kinds, called signals. The three main signals are traces, which follow one request through many steps, metrics, which are numbers measured over time, and logs, which are time-stamped records of events.
For each signal there is one provider. The TracerProvider belongs to traces. The MeterProvider belongs to metrics. The LoggerProvider belongs to logs. The names follow a pattern that is easy to remember: the tool is named after the job, and the provider is the thing that provides it. A Tracer traces, a Meter measures and a Logger logs.
| Signal | Provider | Tool it hands out | What the tool records |
|---|---|---|---|
| Traces | TracerProvider | Tracer | Spans, the timed steps of a request |
| Metrics | MeterProvider | Meter | Measurements through instruments such as counters and histograms |
| Logs | LoggerProvider | Logger | Log records and events |
What a provider does in one sentence
A provider is a factory and a holder of settings. As a factory, it creates and gives out the Tracer, Meter or Logger that your code asks for. As a holder of settings, it knows the identity of your service, the rules for processing the data, and the destination where the data will be sent. It also controls the life of the whole signal: how it starts, how it sends remaining data and how it stops.
Where they live
Providers belong to the SDK, the engine part of OpenTelemetry, as we explain in chapter 3. Your application code and the libraries it uses talk only to the simple interfaces of the API. The providers sit behind those interfaces, quietly doing the real work. Most people write their provider set-up once, in a small startup section of the program, and never touch it again. Others never write it at all, because an automatic helper creates the providers for them.
Think of three department reception desks in one office building. People who need a diary, a counter board or a logbook go to the matching desk. Each desk gives out the right item with the company’s name already printed on the cover, and each desk knows which mail room collects the finished items. Providers are those desks, and the Tracer, Meter and Logger are the items they give out.
You will almost never create a Tracer, Meter or Logger directly. You always get one from a provider. If you remember only one rule from this guide, remember that: tools come from providers.
2The Problem Providers Solve
A middleman may look like extra work, but here it removes four real difficulties.
Difficulty 1: libraries must not decide where data goes
Your program uses many libraries, such as a web framework or a database driver. Their authors want to record telemetry, but they cannot know where you want it sent. One company uses a hosted monitoring tool, another runs its own store. If every library chose its own destination, you would have a mess. Instead, library authors ask for a Tracer from whatever provider has been set up, record their data, and leave the rest to you. The provider is the single place where you decide.
Difficulty 2: settings must be in one place
Telemetry needs many settings: the name of your service, how many traces to keep, how often to send metrics, and which formats to use. Scattering these across the code would make them hard to find and easy to get wrong. A provider collects the settings for a signal in one place, set once at the start of the program.
Difficulty 3: the code must work even when nothing is set up
A library may be used in a program that has no OpenTelemetry set up at all. It must not crash. When no provider has been configured, the API quietly gives out no-op tools, which do nothing at all, at almost no cost. This lets library authors add instrumentation freely, and lets application owners switch it on later by setting up providers, with no change to the libraries.
Difficulty 4: shutdown must not lose data
Telemetry is usually collected in memory and sent in batches. When a program stops, the last batch may still be waiting. Someone must be in charge of sending that last batch and cleaning up. The provider owns the whole pipeline behind it, so it is the natural place to say “send everything now” and “stop”.
Destination choice
Libraries record data, and the application owner decides where it goes, through the provider.
Scattered settings
Identity, sampling, batching and export rules are held together in one place per signal.
Missing set-up
With no provider, the API gives out harmless no-op tools, so code never breaks.
Lost data
The provider can flush waiting data and shut down the whole pipeline safely.
A water tap in your kitchen is a good picture. You turn it and water comes out. You do not know, and do not need to know, whether it comes from a tank, a river or a city main. The plumber decided that once, behind the wall. If the house has no water supply connected yet, the tap simply gives nothing, and nothing breaks. Libraries use the tap, and the provider is the plumbing behind the wall.
The harmless no-op behaviour has a catch. If you forget to set up a provider, or set it up in the wrong place, your code still runs without errors, but no telemetry is produced. Silence is the symptom. We return to this in the mistakes chapter.
3A Quick Refresher: API, SDK, Resource and Scope
Four words explain where providers stand: API, SDK, resource and instrumentation scope.
API and SDK
OpenTelemetry splits its software into two layers. The API is a small set of interfaces, which are lists of calls that code may make, such as “start a span” or “add one to a counter”. The API does very little by itself. The SDK is the full implementation. It contains the engine that stores spans, adds up numbers, batches records and exports them. Providers are part of the SDK.
The reason for this split is that library authors should depend only on the light API, so that they do not force a heavy engine on every user. Application owners choose an SDK and set it up. If no SDK is present, the API does nothing. If an SDK is present and registered, the same API calls suddenly do real work.
flowchart TD
Lib["Your code and libraries"] --> API["OpenTelemetry API: Tracer, Meter, Logger"]
API --> Reg{"Is an SDK registered?"}
Reg -- "No" --> Noop["No-op: calls do nothing"]
Reg -- "Yes" --> SDK["SDK providers do the real work"]
SDK --> Out["Processing and export"]
Resource: who is sending
A resource is a set of labels that describes the thing producing telemetry: the service name, its version, the environment, the machine, the cloud region. It is attached to every span, metric and log the program sends, so that the backend knows who sent them. All three providers need a resource, and it should be the same for all of them.
Instrumentation scope: who is recording
Telemetry also needs to say which piece of code recorded it. This is called the instrumentation scope. It has a name, such as the name of a library or of a module in your application, and optionally a version. When your code asks a provider for a Tracer, Meter or Logger, it passes this name. The name then appears with every item the tool records, so you can tell whether a span came from the web framework or from your own checkout module.
| Term | Answers the question | Example |
|---|---|---|
| Resource | Which service or machine sent this? | Service “checkout”, version 2.4, production |
| Instrumentation scope | Which code recorded this? | The HTTP client library, or your payment module |
| Attributes | What are the details of this item? | Method GET, status 200 |
Think of a letter. The company letterhead printed at the top says who the sender is, and that is the resource. The initials of the clerk who typed it say which person wrote this particular letter, and that is the instrumentation scope. The contents are the attributes. The reader needs all three to understand where a letter came from and who wrote it.
The labels in a resource follow the OpenTelemetry semantic conventions, the agreed dictionary of names. For example, the service name has a standard name that every backend knows how to read. Using standard names is what lets tools group and filter telemetry correctly.
Why the two layers protect you
Suppose a popular library hard-wired a heavy engine. Every program that used the library would carry that engine, even programs that never wanted telemetry, and two libraries with two engines would fight each other. By depending only on the thin API, libraries stay light and neutral. The application owner alone chooses the engine, and that choice applies to every library at once. This split is the quiet reason why OpenTelemetry can be adopted slowly, one service at a time, without a big rewrite.
Semantic versions and stability
The API is kept very stable on purpose, because many libraries depend on it. The SDK may change more often, since it holds options, processors and exporters. When you upgrade, you will usually be safe moving the SDK forward while the API stays steady, but still read the release notes, because the maturity of each signal is not the same in every language.
4The Anatomy of a Provider
All three providers follow the same blueprint. Learn it once, and you understand them all.
Four jobs, repeated three times
Every provider does four jobs. First, it hands out tools: you ask it for a Tracer, Meter or Logger by scope name, and it returns one. Second, it holds the resource that identifies your service. Third, it owns a processing pipeline: the steps that take what the tool records and prepare it for export. Fourth, it manages life: it can be asked to send everything it holds, called a flush, and to stop completely, called a shutdown.
flowchart LR Req["Your code asks for a tool by scope name"] --> P["Provider"] P --> Tool["Tracer, Meter or Logger"] P --> Res["Holds the Resource"] P --> Pipe["Owns the processing pipeline"] P --> Life["Controls flush and shutdown"]
What differs between the three
The blueprint is the same, but the pipeline inside each provider is different, because the signals behave differently. Traces are made of individual spans, so the TracerProvider has a sampler that decides which traces to keep, and span processors that batch and send them. Metrics are numbers that need adding up over time, so the MeterProvider has views that decide how to summarise them, and readers that collect the summaries. Logs are individual records, so the LoggerProvider has log record processors that batch and send them.
| Part | TracerProvider | MeterProvider | LoggerProvider |
|---|---|---|---|
| Hands out | Tracer | Meter | Logger |
| Holds | Resource | Resource | Resource |
| Decision maker | Sampler | Views | None by default; processors may filter |
| Pipeline entry | Span processors | Metric readers | Log record processors |
| Sends out through | Span exporters | Metric exporters | Log record exporters |
| Limits | Span limits, such as attribute counts | Cardinality limits on series | Log record limits |
A note on timing
Providers are normally configured when they are created. You decide the resource, the sampler, the processors and the exporters at that moment. Some languages let you add a processor later, but the cleanest habit is to build the whole provider in one place and then use it for the life of the program. Changing the pipeline on a live provider is unusual and easy to get wrong.
Three kitchens in a large restaurant follow the same house rules: each has a head chef who hands out ingredients, a uniform with the restaurant’s name, a set of stations, and a closing routine. But the pastry kitchen has ovens and the sushi kitchen has none, because their jobs differ. Providers share a blueprint but have station layouts suited to their signal.
Each provider is independent. Setting up a TracerProvider does not set up a MeterProvider or a LoggerProvider. Teams sometimes configure traces carefully and then wonder why their metrics and logs never appear. Remember that you need to set up each signal you want.
Creating a provider step by step
Describe the service
Prepare the resource with name, version and environment.
Choose the exporter
Pick where the signal goes, usually the Collector over OTLP.
Choose the processor or reader
Use batching for traces and logs, and a periodic reader for metrics.
Add the decision rules
Set a sampler for traces, views for metrics, and filters for logs where needed.
Assemble and register
Build the provider with all of the above and register it.
Why a provider is built once
A provider is not a light object. It starts background workers, opens network connections and keeps queues and summaries in memory. Building one is like opening a branch office, and you would not open a new branch for every customer. Build it at startup and keep it until the program ends.
5The TracerProvider in Depth
The TracerProvider is the oldest and best known of the three. Its pipeline decides which traces live and how they leave.
What it contains
Resource
The labels that say which service these spans came from. Attached to every span.
Sampler
Decides, at the start of a span, whether the trace is recorded. Common choices keep everything, keep nothing, keep a fixed share, or follow the parent’s decision.
Span processors
Receive each finished span. The batch processor groups spans and sends them in the background. The simple processor sends each span at once, which suits tests.
Span exporters
Convert spans to a wire format, usually OTLP, and deliver them to a Collector or a backend.
Span limits
Caps on attributes, events and links per span, so one badly behaved span cannot use endless memory.
ID generator
Creates the random trace and span identifiers. The default is random and rarely needs changing.
The sampler
A busy system creates far more spans than it is worth storing. The sampler is the gatekeeper. It looks at the start of each span and says “record this” or “do not record this”. The most common production choice is the parent-based sampler wrapped around a ratio sampler. At the start of a trace, with no parent, the ratio sampler keeps a chosen share, such as one trace in ten. For later spans, the parent-based part simply follows the decision of the parent, so that a trace is either kept whole or dropped whole. The default, if nothing is chosen, is to follow the parent and to keep root spans.
The batch processor and its defaults
Sending each span over the network immediately would be wasteful and slow. The batch span processor holds finished spans in a queue and sends them in groups. Its standard settings are a queue of about two thousand spans, a delay of about five seconds between sends, and at most about five hundred spans per group. When the queue is full, new spans are dropped rather than slowing your application. You can tune these through settings, but the defaults are a good start.
flowchart LR
T["Tracer starts a span"] --> S{"Sampler"}
S -- "record" --> Sp["Span is created and recorded"]
S -- "drop" --> D["Span is not recorded"]
Sp --> E["Span ends"]
E --> BP["Batch span processor queue"]
BP --> X["Span exporter"]
X --> O["Collector or backend"]
Where propagation fits
One thing the TracerProvider does not control is propagation, the passing of trace identity between programs through headers. Propagators are set separately through the global part of the API. They work with the context rather than with the provider, but they are essential for traces to join across services, so remember to set them when you set up tracing.
A newspaper’s picture desk decides which photographs to print (the sampler), collects the chosen photographs in a tray, and sends a batch to the press at fixed times (the batch processor and exporter). The picture desk does not decide the delivery route of the paper to the shops, which is a different department, just as propagation is separate from the TracerProvider.
For tests, use the simple processor with an in-memory exporter. Spans are then available at once for the test to inspect, with no waiting for a batch delay.
Span processors in more detail
A span processor gets two chances to act on a span: once when the span starts, and once when it ends. Most processors only care about the end, because a span is complete then and can be exported. A processor that acts at the start can, for example, copy a value from the context into the span, such as the tenant name of the current customer. You may chain several processors on one provider. A common chain is one that adds extra attributes, followed by one that batches and exports.
The two built-in export processors differ in a simple way. The simple one exports each span the moment it ends, on the thread that ended it. This is easy to understand and never loses data in a crash, but it slows the application and so is only for tests and learning. The batch one is the production choice. It keeps its own background worker and a queue, so the application never waits for the network.
Span limits explained
A span can collect attributes, events and links as it runs. Without a ceiling, a loop that adds an attribute on every pass could grow one span to enormous size. Span limits set a maximum number of each, and a maximum length for text values. A common default for attributes is about one hundred and twenty-eight per span. Extra items are quietly discarded, and the span carries a count of how many were dropped. This protects memory, and it also protects your backend from oversized messages.
Choosing a sampling strategy
| Strategy | How it works | Best for |
|---|---|---|
| Always on | Records every trace | Development and low-traffic tools |
| Always off | Records nothing | Switching tracing off without removing code |
| Ratio | Keeps a fixed share of traces, decided from the trace identifier | Steady cost control |
| Parent-based | Follows the decision already made upstream | Keeping traces whole across services |
| Tail sampling in the Collector | Decides after the whole trace is seen | Keeping all errors and slow traces |
Head sampling, done in the provider, is fast but cannot know whether a trace will turn out slow or failed. Tail sampling, done in the Collector, can know, but needs more memory and a place where all spans of a trace arrive together. Many teams combine a modest head ratio with tail rules for errors.
Head sampling is like a bouncer who lets in one visitor in ten at the door, without knowing who will be interesting. Tail sampling is like a museum curator who watches the whole visit and later keeps only the interesting recordings. The bouncer is cheap and quick, and the curator is clever and costly.
6Tracers, Spans and Instrumentation Scope
The Tracer is what your code actually holds. Understanding how to get one, and how to name it, avoids a surprising number of mistakes.
Getting a tracer
To record a span, your code asks the TracerProvider for a Tracer, giving a name. The name should identify the code that is doing the instrumenting, not the whole application. A library uses its own package name. In an application, a team may use the name of a module, such as the checkout module or the payment module. Optionally, the request includes a version, and in newer versions of the specification, a schema address and a few attributes.
Asking for the same Tracer twice with the same details returns the same one, or an equivalent one, so you may fetch a tracer wherever it is convenient. It is also cheap. Tracers are safe to use from many threads at once, and you normally keep one in a variable for each module.
What the Tracer creates
The Tracer starts spans. A span has a name, a start time, an end time, a status, a kind such as server or client, and attributes. A new span usually becomes a child of the span that is current in the context, which is how the family tree of a trace is built. When the span ends, it is handed to the provider’s processor for export.
Why the name matters
The tracer’s name travels with every span as its instrumentation scope. In your tracing tool, you can then filter by the scope to see only spans from a certain library, or to find which module is producing too many spans. A good name makes later investigation easy, and a vague name, such as “app”, wastes this benefit.
| Good tracer name | Weak tracer name | Why |
|---|---|---|
| The package name of your payment module | app | Scope should say which code recorded the span |
| The library’s published package name | my-service | Service identity belongs in the resource, not the scope |
| A stable name with a version | A name that includes a random value | Changing names make filtering impossible |
The timeline of one span
Ask the provider for a Tracer
Done once per module, usually when the module starts.
Start a span
The Tracer asks the sampler whether to record, then creates the span as a child of the current span.
Do the work
Attributes, events and errors are added to the span while the work runs.
End the span
The end time is set and the finished span is passed to the span processor.
Export
The batch processor groups the span with others and the exporter sends them out.
A Tracer is like a pen with the department’s name engraved on it. Every note written with that pen can be traced back to the department that wrote it. Using one generic pen for the whole company would make every note look the same, and it would be hard to say which department caused a problem.
If you ask the global provider for a Tracer before the real provider has been registered, some languages give you a no-op tracer that stays no-op even after set-up. Others hand out a tracer that switches over later. Check how your language behaves, and register the provider as early in startup as possible.
Span kinds and why they matter
Every span has a kind that says its role in a conversation between programs. A server span covers handling a request that came in. A client span covers sending a request out. An internal span is work inside one program. Producer and consumer spans describe sending and receiving messages through a queue. Backends use these kinds to draw service maps and to compute request rates, so choose them honestly.
Span status and errors
A span can end as unset, ok or error. Mark a span as an error when the operation failed, and record the failure details as an event on the span. Avoid setting error for normal outcomes such as a “not found” answer to a search, since this fills dashboards with false alarms. Keep the rule simple: an error means the work did not do what it was meant to do.
7The MeterProvider in Depth
Metrics are numbers that must be added up over time. The MeterProvider therefore has a different shape: readers and views instead of samplers and span processors.
Why metrics need a different pipeline
A trace records every single request. A metric does not. If your shop handles ten thousand orders a minute, it would be silly to send ten thousand separate numbers. Instead, the SDK aggregates them, which means it adds them up in memory and keeps only a summary, such as “ten thousand orders in this minute”. The MeterProvider owns this summarising machinery.
What it contains
Resource
The same service labels used by the other providers, attached to every metric.
Metric readers
The part that asks for the current summaries. A periodic reader collects on a timer, usually every sixty seconds by default. A pull reader, such as one for Prometheus, collects when an outside tool asks.
Views
Rules that change how a metric is summarised: rename it, change its aggregation, choose its bucket boundaries, or drop attributes you do not need.
Metric exporters
Send the collected summaries to a Collector or a backend, usually in OTLP format.
Cardinality limit
A cap on how many different attribute combinations one metric may hold. By default it is around two thousand, and extra combinations are folded into an overflow series.
Temporality
Whether totals are reported as running totals (cumulative) or as the change since the last report (delta).
Readers: push and pull
A metric reader is what triggers collection. In the push style, a periodic reader wakes up on a timer, collects all summaries, and hands them to an exporter that sends them out. In the pull style, nothing is sent until an outside system, such as a Prometheus server, comes and asks. The provider can have several readers at once, for example one pushing to a Collector and one serving a pull endpoint. Each reader gets its own copy of the data.
Views: reshaping the data
A view is a rule that says “for metrics that match this description, do it this way”. It can match by instrument name, by type, or by the scope that created it. The rule can rename the metric, change how it is aggregated, set the histogram bucket boundaries, or keep only certain attributes. Views are the main tool for controlling cost and the number of series, because you can remove a high-detail attribute such as a user identifier before it multiplies your data.
flowchart LR M["Instruments record measurements"] --> V["Views reshape and filter"] V --> A["Aggregation in memory"] A --> R1["Periodic reader, every N seconds"] A --> R2["Pull reader, on request"] R1 --> E1["Metric exporter"] R2 --> E2["Prometheus endpoint"] E1 --> B["Collector or backend"]
Cardinality in plain words
Cardinality is the number of different combinations of attribute values a metric has. A counter of orders with an attribute for the country has as many series as there are countries, perhaps a hundred. Add an attribute for the customer identifier, and it explodes into millions. Each series uses memory in your program and money at your backend. The cardinality limit and views exist to protect you from this.
Picture a shop’s accountant. She does not keep a note for each coin that enters the till. She keeps a daily total and perhaps a total per product. A manager (the view) may say “only keep totals per product, not per customer”. A visitor (the reader) arrives each evening to copy the totals, or the accountant posts them herself on a schedule. The MeterProvider is that accounting office.
Never put unbounded values, such as user identifiers, session identifiers or full web addresses, into metric attributes. This is the single most common way teams end up with huge monitoring bills and slow dashboards.
Temporality: cumulative and delta
Imagine a counter that has counted one hundred orders so far. A cumulative report says “the total since the program began is one hundred”. A delta report says “since my last report, there were twelve more”. Different backends prefer different styles. Prometheus-style systems expect cumulative totals. Some commercial systems prefer deltas because they use less memory in the application. The exporter or reader can be told which style to use, and the SDK handles the arithmetic.
Histograms and bucket boundaries
A histogram counts how many measurements fell into each range, called a bucket. For request durations, the ranges might be under five milliseconds, five to ten, ten to twenty-five, and so on. From the buckets, a backend can estimate the median or the ninety-ninth percentile. The default bucket boundaries suit many cases but not all. If your requests usually take two seconds, default boundaries aimed at milliseconds will put everything in the top bucket and the graph will be useless. A view lets you set boundaries that match your reality. Newer exponential histograms adjust their ranges automatically and are a good option when your backend supports them.
What happens when a reader collects
On each collection, the reader asks the provider for the current summaries from every instrument. Synchronous instruments simply report what they have added up. For observable instruments, the callbacks run at that moment, so they should be quick and never block. The results are bundled with the resource and scope information and passed to the exporter. Because collection is on a timer, a callback that takes too long delays the whole report.
Several readers, several destinations
Because every reader receives its own copy of the data, a single MeterProvider can feed several destinations. You might push to the Collector every thirty seconds while also exposing a pull endpoint for a local Prometheus scraper. The cost is a little extra memory, since each reader keeps its own state, especially in cumulative style.
8Meters and Instruments
A Meter is a small workshop that makes instruments. Each instrument is a specialised measuring tool.
Getting a meter
Just like a Tracer, a Meter is requested from its provider with a scope name and optional version. The Meter itself records nothing. It creates instruments, and you use the instruments to record. You usually create each instrument once, near the start of the module, and keep it, because creating an instrument again and again is wasteful.
The instrument family
| Instrument | What it measures | Goes up or down? | Example |
|---|---|---|---|
| Counter | A running total of events | Only up | Orders placed |
| UpDownCounter | A total that can rise and fall | Both | Items waiting in a queue |
| Histogram | The spread of many values | Not applicable | Request duration |
| Gauge | The current value of something at a moment | Both | Room temperature |
| Observable (async) versions | Values read by a callback when collection happens | Depends | Memory in use, read when asked |
Synchronous and asynchronous
A synchronous instrument is used by your code at the moment something happens: an order arrives, so you add one to the counter. An asynchronous instrument, also called observable, is different. You give it a small function, a callback, and the SDK calls it whenever a reader collects. This suits things that you read rather than cause, such as the current memory use or the size of a cache.
Choosing the right one
Ask three questions. Do I count events as they happen? Use a counter. Do I care about how long or how large each event was? Use a histogram. Do I read the state of something at a moment? Use a gauge or an observable. A histogram is the right choice for durations, because averages hide slow requests, while a histogram lets you see the slowest few percent.
flowchart TD
Q["What do you want to record?"] --> A{"Counting events?"}
A -- "Only increases" --> C["Counter"]
A -- "Can go up and down" --> U["UpDownCounter"]
Q --> B{"How big or how long each time?"}
B --> H["Histogram"]
Q --> S{"Current state at a moment?"}
S --> G["Gauge or Observable"]
A Meter is like a toolbox of measuring tools. The counter is a tally clicker used by a doorman. The histogram is a stopwatch with a record sheet. The gauge is a thermometer you glance at. The provider is the store that sold the toolbox and tells you where to send the readings.
Name instruments clearly and give them a unit and a short description. The semantic conventions list standard names for common measurements, such as request duration, which lets dashboards work out of the box.
Units, names and descriptions
Each instrument has a name, a unit and a description. The unit, such as seconds or bytes, lets backends show graphs with the right labels and convert values sensibly. Use base units where possible, for example seconds rather than milliseconds, because the conventions prefer them and tools expect them. A short description helps the next engineer understand what a mystery number means.
Attributes on measurements
When you record a measurement, you may attach attributes, such as the payment method or the HTTP status class. Each unique combination of values becomes its own series. Choose attributes whose values come from a small, known list. A status class such as success or failure is excellent. A customer name is not. The next chapters show how views and the cardinality limit protect you if someone forgets this rule.
9The LoggerProvider and the Logs Bridge
Logs are the youngest signal in OpenTelemetry, and the LoggerProvider is built with one special idea in mind: you probably already have a logging library.
Why logs are different
For traces and metrics, OpenTelemetry offered a brand-new way of working. But logging already existed for decades, with libraries in every language, and millions of lines of code already call them. Asking everyone to rewrite all of that would never work. So OpenTelemetry took a gentler path. It lets you keep your current logging library and adds a bridge that carries those log lines into the OpenTelemetry pipeline.
What the LoggerProvider contains
Resource
The same service labels used for traces and metrics.
Log record processors
Receive each log record. The batch processor groups them and sends them in the background, with a shorter default delay, about one second.
Log record exporters
Send the records onward, usually in OTLP format.
Limits
Caps on attribute counts and value lengths, so one noisy log cannot be enormous.
The bridge, in plain words
The part that connects your logging library to OpenTelemetry is called an appender or handler, depending on the language. It listens to the logging library. Each time your code writes a log line, the appender converts it into an OpenTelemetry log record and hands it to a Logger from the LoggerProvider. The log record then goes through the processors and exporters like any other data.
The API that appender authors use is called the Logs Bridge API. It is deliberately meant for the authors of these bridges, not for everyday application developers. As an application developer, you keep writing logs the way you always did, and you only install and enable the appender for your library.
flowchart LR App["Your code writes a log line"] --> Lib["Existing logging library"] Lib --> Ap["OpenTelemetry appender or handler"] Ap --> L["Logger from LoggerProvider"] L --> LP["Log record processor"] LP --> LE["Log record exporter"] LE --> B["Collector or backend"]
The big benefit: automatic trace links
When the appender builds a log record, it can copy the current trace identifier and span identifier from the context into it. This means each log line is tied to the exact request that produced it. In your backend, you can open a slow trace and see the log lines from that very request, or start from an error log and jump to its trace. This link is the strongest reason to send logs through OpenTelemetry.
Imagine an office where everyone already writes notes in their own favourite notebook. Instead of forcing them to change, you add a mail clerk who photocopies each new note, stamps it with the number of the case it belongs to, and sends it to the central archive. The notebook users carry on as before, and the archive gets neat, case-numbered copies. The clerk is the appender, and the LoggerProvider is the archive’s front desk.
The status of the logs signal differs by language and version. In some languages the logs part of the SDK is stable, in others it is still marked experimental, and the class names may even carry an underscore. Always check the documentation of the exact version you use.
Log record processors
The LoggerProvider is the home of log record processors, which work like span processors. A simple one exports at once, and a batch one groups records in a background queue. Log processors also have a handy extra use: because they see every record before export, they can modify or filter them. A processor can add attributes, mask a sensitive field, or drop records that are not worth sending. This is a useful safety net, though cleaning at the source remains better.
Volume is the main challenge
Traces are sampled and metrics are summarised, but logs are usually sent one by one. A busy service may write thousands of lines each second, so logs are normally the biggest cost of the three signals. Good habits help a lot: log at the right severity, avoid logging inside tight loops, write structured data rather than long sentences, and do not log the same fact at many layers. Sending only warnings and errors from busy modules, while keeping debug lines local, is a common compromise.
Structured logs and attributes
A structured log keeps details as separate named attributes, such as the order number and the payment method, instead of burying them in a sentence. Structured logs are much easier to search and count in the backend. When the appender converts a log line, it maps the fields your library already provides into attributes, so adopting structured logging in your library pays off directly in OpenTelemetry.
10Loggers, Log Records and Events
The Logger is the tool the appender uses. Knowing what it builds helps you read the logs that arrive at your backend.
The log record
A log record is the unit of the logs signal. It carries a time stamp, a severity (such as debug, info, warning, error), a body with the message, a set of attributes, the trace and span identifiers if a span was active, and the resource and scope supplied by the provider and logger. Because the structure is standard, any backend can show logs from any language in the same way.
| Field | Meaning | Example |
|---|---|---|
| Timestamp | When the event happened | 10:42:07 |
| Severity | How serious it is | Error |
| Body | The message itself | Payment declined |
| Attributes | Extra details | Payment method card |
| Trace and span IDs | Which request produced it | Links to the checkout trace |
| Resource | Which service sent it | checkout, version 2.4 |
| Scope | Which code recorded it | The payment module |
Events
OpenTelemetry also describes events, which are log records with a defined name and a standard set of attributes, used for things such as a user action or a browser event. They travel through the same LoggerProvider. Their exact API and naming have been evolving, so check the current documentation for your language before building on them.
Severity and filtering
Most logging libraries already let you choose a minimum severity. That filtering happens before the bridge, so debug lines that your library drops never reach OpenTelemetry. This is the first and cheapest place to control volume. Later, a processor or the Collector can filter further.
A log record is like a hospital chart entry: time, urgency level, a note, extra details, and the patient number that links it to the full file. The patient number is the trace identifier. Without it you have a pile of notes. With it, you have a story.
Logs often hold sensitive data by accident, such as email addresses or tokens. Once logs flow through the pipeline, they are copied to more places. Clean them at the source, and consider redaction in the Collector as a second guard.
Correlation in practice
For the trace link to work, the log must be written while a span is active in the context. A log line written in a background thread that has lost the context will carry no identifiers. In busy programs, make sure the context is passed along when work moves between threads, and check a few sample logs to confirm that identifiers appear. If they do not, the problem is usually context propagation, not the LoggerProvider.
Severity numbers
OpenTelemetry stores severity as a number in ranges, so that different libraries’ levels can be compared. Trace and debug sit at the low end, then info, warning, error and fatal. The appender maps your library’s levels onto this scale. This is why an error from one language and an error from another look the same in your backend, even though the libraries named them differently.
11One Resource Shared by All Three
The three providers are separate, but they must tell the same story about who you are.
Why sharing matters
Each provider attaches a resource to its data. If the TracerProvider says the service is “checkout” and the MeterProvider says “checkout-service”, your backend will treat them as two different services. You would lose the ability to move from a slow trace to the matching metrics or logs. Correlation depends on identical identity.
How to share it safely
The simplest approach is to build the resource once at startup and pass the same object to all three providers. Most SDKs also read the resource from environment settings, namely the service name and a list of extra attributes. When the environment supplies identity, every provider reads the same source and agrees without extra effort. Autoconfiguration and agents do this for you.
What belongs in a resource
| Attribute | Why it helps |
|---|---|
| Service name | The primary label for grouping all telemetry |
| Service version | Lets you compare behaviour before and after a release |
| Deployment environment | Separates production from staging |
| Host or pod name | Finds a single bad machine |
| Cloud region or zone | Shows regional problems |
| Service instance identifier | Distinguishes copies of the same service |
flowchart TD R["One Resource: service name, version, environment, host"] --> TP["TracerProvider"] R --> MP["MeterProvider"] R --> LP["LoggerProvider"] TP --> T["Traces carry the resource"] MP --> M["Metrics carry the resource"] LP --> L["Logs carry the resource"] T --> B["Backend can join all three"] M --> B L --> B
Three clerks in the same company must print the same company name on their letters. If one writes “Acme Ltd”, one “ACME Limited” and one “Acme Co”, the post office will treat them as three companies. A shared letterhead keeps everything together.
If no service name is supplied, SDKs use a placeholder such as “unknown service”. Dashboards full of “unknown” are a clear sign that the resource was never set.
Service name and instance identity
The service name groups all copies of a service together. The service instance identifier separates one copy from another, which helps when only one pod misbehaves. Many SDKs generate a random instance identifier at startup if none is provided. If each provider generated its own random value, signals from the same process would look as if they came from different instances, which is one more reason to build the resource once and share it.
Detectors
Writing every attribute by hand is tedious. Resource detectors are small helpers that discover facts from the environment, such as the host name, the operating system, the container identifier or the cloud region, and add them to the resource. Using detectors keeps your identity accurate and saves effort. Review what they add, since some details can be too long or sensitive for your taste.
12Global Registration and the No-op Behaviour
Creating a provider is not enough. Libraries must be able to find it, and that is the job of registration.
The global registry
Libraries are written long before your application exists, so they cannot receive your provider as an argument. Instead, the API keeps a global slot for each signal. When you register a provider, it goes into that slot. When any code asks the global API for a Tracer, Meter or Logger, the answer comes from the registered provider. This is how a library you did not write ends up using your configuration.
Set once only
The global slot can usually be set only once. A second attempt is ignored or logs a warning, depending on the language. This prevents two parts of a program from fighting over the global provider, but it also means the order of start-up matters. Register early, before libraries start asking for tools.
Passing providers directly
Registration is not the only way. You may also keep the provider in your own variable and ask it directly for tools, without touching the global slot. This is common in tests, in programs that run several isolated parts, and in code that avoids global state on purpose. Instrumentation libraries usually allow you to pass a provider to them, so they can use yours.
Global registration: strengths
- Libraries find the provider with no extra wiring
- One set-up covers the whole program
- Matches how most auto-instrumentation works
Global registration: weaknesses
- Hidden shared state, harder to test
- Set-up order becomes important
- Only one provider per signal can be global
What happens without registration
If nothing is registered, the API supplies no-op tools. Spans have valid identifiers when a parent exists, so context can still pass through, but nothing is recorded or exported. This is safe, but silent. A quick check during development is to confirm that a first span, metric or log really reaches your backend or a console exporter.
Think of a building’s main switchboard. Every phone in the building connects to it. Once you plug in the real telephone company line (register the provider), every phone can make calls. Until then the handsets work but only produce silence. You can plug in only one main line at a time.
When a platform such as a Java agent or a distribution does the set-up for you, it registers the providers on your behalf. Registering a second set manually can then clash, so check whether something has already done it.
Dealing with start-up order
Because registration happens once and early, the order of start-up work matters. Frameworks often create their instrumented clients while loading. If those clients ask for a tracer before your provider is registered, they may keep a no-op tracer for life. A reliable approach is to put telemetry set-up at the very top of the program entry point, or to load it before the application code, as agents and launchers do. In some environments, a proxy tracer is handed out that later switches to the real one, but you should not depend on it unless the documentation of your language promises it.
A short checklist for registration
Create the resource
Decide the service name, version and environment before anything else.
Build the providers
Add processors, readers and exporters to each of the three.
Register globally
Place each provider in its global slot, and set the propagators.
Load the application
Only now start frameworks and clients that ask for tools.
Prove it works
Emit one span, one metric and one log to a console exporter and see all three appear.
13Lifecycle: Start, Flush and Shutdown
Data waiting in memory is lost when a program ends suddenly. The lifecycle controls decide whether your last seconds of telemetry arrive.
The three stages
Create and register
Build each provider with its resource, processors and exporters, and register them early in startup.
Run
Tools record data, processors batch it, and exporters send it in the background.
Flush and shut down
On exit, ask each provider to flush waiting data and then shut down, which also stops background workers and closes connections.
Flush versus shutdown
ForceFlush means “send everything you are holding, now, but keep running”. It is useful before a short pause, such as the end of a serverless function call. Shutdown means “flush, then stop for good”. After shutdown, the provider no longer accepts new data. Both normally accept a time limit, so a stuck network cannot freeze your exit forever.
Who needs this most
Long-running servers lose very little, because the batch delay is a few seconds and they usually stop gracefully. The programs at greatest risk are short-lived ones: command-line jobs, scheduled tasks, serverless functions and containers that are killed quickly. If such a program ends before the next batch send, its last data is gone. The fix is to flush and shut down all three providers before exit, and to give the platform enough time at termination.
sequenceDiagram participant App as Application participant P as Provider participant Pr as Processor participant Ex as Exporter participant Bk as Backend App->>P: Create and register at startup App->>P: Record data while running P->>Pr: Pass data to the processor Pr->>Ex: Send batches on timer Ex->>Bk: Deliver App->>P: Shutdown on exit P->>Pr: Flush remaining data Pr->>Ex: Final batch Ex->>Bk: Deliver last data P-->>App: Done
A shop closes at night. The owner does not just lock the door while a bag of cash and receipts sits on the counter. First the closing routine takes everything to the bank (flush), then the lights go off and the door is locked (shutdown). Forgetting the bank run means the day’s takings are missing.
Shut down the providers in a safe order. Ideally flush traces, metrics and logs, then stop. A common cause of a missing final error log is that the logging provider was shut down before the last message was written.
Serverless and short-lived programs
A serverless function may be frozen the moment it returns its answer. A batch timer set for five seconds will then never fire, and spans wait in the queue until the function is thawed or destroyed. The usual fixes are to flush all providers before the function returns, or to use a platform extension or Collector that receives data immediately. Flushing adds a small delay to each call, so measure the cost. Command-line tools and scheduled jobs need the same care, with a flush at the end of the main routine.
Graceful termination in containers
When a container platform wants to stop your service, it first sends a polite stop signal and waits a grace period, often thirty seconds, before killing it. Your program should catch the signal, stop accepting work, finish current requests, then flush and shut down the providers. If the grace period is shorter than your flush timeout, the platform will cut you off in the middle. Make the timeouts agree: the export timeout must be shorter than the grace period.
What shutdown does not do
Shutdown flushes what the provider holds. It cannot recover data that was already dropped from a full queue, and it cannot send data if the network is down. It is a best effort. For stronger guarantees, rely on a Collector with persistent storage close to the application.
14Internal Working and End-to-End Data Flow
Let us follow one customer request through all three providers, from the first call to the backend screen.
The story of one request
A customer presses “Pay”. The web framework, instrumented by a library, asks the global API for a Tracer. The API hands over the tracer made by the registered TracerProvider. A span starts, the sampler says yes, and the span becomes the current span in the context. While the request runs, your code increases an order counter from a Meter, and a database call adds a child span. A payment failure causes a log line, and the appender turns it into a log record that already carries the trace and span identifiers.
Three parallel pipelines
Each signal now travels its own pipeline inside its own provider. Spans finish and enter the span processor queue. The counter value sits in the aggregation memory until the periodic reader wakes up. The log record enters the log record processor queue. Timers then push each batch to its exporter. All three exporters usually point to the same OpenTelemetry Collector, a separate program that receives, processes and forwards telemetry, which then sends everything on to the backend.
flowchart LR Req["Customer request"] --> Fw["Instrumented code"] Fw --> TP["TracerProvider: sampler, span processor, exporter"] Fw --> MP["MeterProvider: views, reader, exporter"] Fw --> LP["LoggerProvider: log processor, exporter"] TP --> Col["OpenTelemetry Collector"] MP --> Col LP --> Col Col --> Bk["Backend: traces, metrics, logs"]
Where the three signals touch
The providers do not talk to each other. They are connected by two shared things: the resource, which identifies the service, and the context, which carries the current span. The appender reads the context to stamp logs with trace identifiers. Some metrics features, called exemplars, copy a trace identifier into a metric sample, so a spike on a graph can lead you to a sample trace. Everything else is independent.
Why independence is a strength
Since the pipelines are separate, a problem in one rarely hurts the others. If the log exporter cannot reach the network, spans and metrics still flow. You can also tune each one differently: sample traces at ten percent, collect metrics every thirty seconds and keep all error logs.
A hospital has separate lanes for the ambulance, the walk-in and the planned-surgery patients. They run alone, so a queue in one does not stop the others. But all patients carry the same hospital wristband and the same case number, which allows doctors to match their records later.
15Names in Different Languages, Autoconfiguration and Zero-Code Set-up
The idea is the same everywhere, but the class names and the set-up style change from language to language.
Common names
The names below are accurate to the best of our knowledge, but SDKs evolve quickly, so confirm them in the documentation of your version.
| Language | Tracer side | Meter side | Logger side | Notes |
|---|---|---|---|---|
| Java | SdkTracerProvider | SdkMeterProvider | SdkLoggerProvider | All three are combined by an OpenTelemetrySdk object |
| Python | TracerProvider | MeterProvider | LoggerProvider | The logs part may be marked experimental or use underscored module names |
| JavaScript and Node | Tracer provider classes in the SDK trace packages | MeterProvider in the metrics package | LoggerProvider in the logs package | A NodeSDK helper wires the common set-up |
| Go | sdktrace TracerProvider | sdkmetric MeterProvider | sdklog LoggerProvider | Providers are passed or set globally with helper calls |
| .NET | TracerProvider built by a builder | MeterProvider built by a builder | LoggerProvider or logging builder integration | Metrics and tracing build on built-in .NET types |
Three ways to set up
Build in code
You create each provider, choose processors and exporters, and register them. Most control, most effort.
Autoconfiguration
The SDK reads standard environment variables, such as the service name, exporter choice and sampler, and builds all three providers for you.
Agent or distribution
A Java agent, a Python launcher or similar tool attaches to your program, creates the providers, and instruments common libraries without code changes.
Environment variables
OpenTelemetry defines a standard set of environment variables, so the same names work across languages. They set the service name, resource attributes, which exporter to use for each signal, the sampler, the endpoint of the Collector, and the export timing. This is a big help in containers, because operations teams can change behaviour in the deployment files without touching the program.
Which to choose
Begin with autoconfiguration or an agent, because it gives you correct defaults and a shared resource for free. Move to manual set-up only when you need something special, such as custom views, a custom sampler, several exporters, or several providers in one process.
flowchart TD
Start["Need telemetry"] --> Q1{"Can you attach an agent or launcher?"}
Q1 -- "Yes" --> Z["Use zero-code: providers built for you"]
Q1 -- "No" --> Q2{"Are the standard options enough?"}
Q2 -- "Yes" --> A["Use autoconfiguration with environment variables"]
Q2 -- "No" --> M["Build the three providers by hand"]
Z --> Fin["Add custom spans and metrics where needed"]
A --> Fin
M --> Fin
Moving into a new flat, you can buy and place every piece of furniture yourself (manual), rent a ready package with a few choices (autoconfiguration), or rent a fully furnished flat (zero-code). Many people start with the furnished flat and replace one chair later.
Even with an agent, you may still add your own spans and metrics in your code. You then ask the global API for a Tracer or Meter, and the agent’s providers answer.
What can go wrong with autoconfiguration
Autoconfiguration is convenient, but mistakes in environment variables are easy to make and hard to spot. A misspelt variable name is simply ignored. A wrong endpoint leads to silent export failures. A sampler value in the wrong format may fall back to a default. When behaviour surprises you, print the effective configuration, which many SDKs can do through their diagnostic logging, and compare it with what you intended.
Mixing agents and manual code
Teams often attach an agent and then add a manual provider by habit. This can lead to double registration, duplicated spans or two competing exporters. Decide who owns the providers. If an agent owns them, your code should use only the global API. If your code owns them, do not also attach an agent that builds its own set.
16Performance, Scaling and Reliability
Telemetry must never harm the application it watches. Providers are designed with this promise in mind.
Cost on the hot path
The hot path is the code that runs for every request. Starting a span, adding to a counter and writing a log should cost very little. Providers keep this small by doing the heavy work, such as batching, serialising and network sending, on background threads. Your request thread only places a small item in a queue and moves on.
Bounded memory
Every queue is limited. When the span queue or the log queue is full, new items are dropped instead of making your service wait. This is a deliberate trade: losing a little telemetry is better than slowing customers. Metrics are naturally bounded because they are summaries, and the cardinality limit stops them from growing without end.
Reducing volume
| Signal | Main tool | Effect |
|---|---|---|
| Traces | Sampler, such as parent-based with a ratio | Keeps a share of traces whole |
| Traces | Span limits | Caps the size of each span |
| Metrics | Views that drop attributes | Fewer series |
| Metrics | Longer collection interval | Fewer data points |
| Logs | Minimum severity in the logging library | Drops noisy lines early |
| All | Collector filtering and tail sampling | Central control after export |
Scaling out
When you run hundreds of copies of a service, each has its own providers, so each sends its own data. This is fine, but the number of connections to your backend can grow large. A common design is to send everything to a nearby Collector, which batches and forwards on. That keeps the application’s set-up simple and moves heavy decisions out of the application.
What if the destination is down
Exporters retry for a limited time and then drop data. The queue absorbs short outages. For longer outages, a Collector with a persistent queue gives extra safety. In every case the application keeps running normally, and that is the essential promise.
flowchart LR H["Hot path: request thread"] --> Q["Bounded queue"] Q -- "full" --> Dr["Drop new items, count them"] Q --> W["Background worker thread"] W --> Ex["Exporter with timeout and retry"] Ex --> Col["Collector"] Col --> Bk["Backend"]
A restaurant waiter drops each order slip into a rack and walks on. The kitchen picks up slips in its own time. If the rack is full, the waiter must tell the guest to wait or the restaurant refuses new orders. For telemetry, the rack is the queue, and a full rack means dropping slips so customers are never kept waiting.
Dropped data is silent unless you look for it. Watch the SDK’s own internal counters or logs for dropped spans and failed exports, so you know when your window into production has gone blind.
Thinking about the Collector as a shock absorber
A local Collector sits near your application, often on the same machine or in the same cluster. Applications send to it over a fast local link and return to work. The Collector can retry against a distant backend for much longer than an application would be willing to. It can also add common attributes, remove sensitive ones, batch efficiently and balance load across several backends. For these reasons, many production guides recommend always sending through a Collector, and keeping the provider settings in the application as simple as possible.
Measuring the overhead
The cost of instrumentation is usually small but not zero. Before turning on broad tracing in a very hot service, measure latency and CPU with and without it, under realistic load. Pay attention to the number of spans created per request: a hundred tiny spans cost much more than ten meaningful ones. A useful rule is to create spans for operations that matter to an investigation, such as calls across a network boundary, and not for every small function.
Memory and the queue sizes
Each queued span or log record holds attributes in memory. A queue of two thousand small spans is trivial, but a queue of two thousand huge spans is not. Combine queue sizes with span limits and attribute discipline. If you raise the queue size to avoid drops, you are also raising worst-case memory use, so test it in a staging environment under pressure.
17Security, Monitoring and Cost
Telemetry is a copy of your system’s inner life, so it deserves the same care as any sensitive data.
Security of the data
Spans, metrics and logs can contain customer names, addresses, tokens and query text. Treat attributes as potentially sensitive. Never put passwords, keys, full card numbers or session tokens into them. Avoid recording full request bodies. Where you must keep an identifier, prefer a hashed or shortened form. Redaction in the Collector gives a second layer of defence.
Security of the connection
Exporters send data across the network, so use encrypted connections to the Collector or backend. Where the Collector or backend needs authentication, supply headers or certificates through the exporter’s settings, and keep those secrets in a secret store, not in the code or in plain deployment files. Restrict who can reach the Collector endpoint.
Security of the set-up
Remember that the environment variables controlling telemetry can also change behaviour. An attacker with the power to change them could redirect your data to another endpoint. Limit who can edit deployment settings, and review them like any other production configuration.
Monitoring the monitors
You should watch the health of the providers themselves. Useful signs include the number of spans dropped from full queues, the number of failed exports, export latency, queue fill level, and the memory used by metric series. Many SDKs expose internal diagnostics through their own logging. Turn these on in a staging environment, and alert on repeated export failures in production.
Cost control
Most monitoring bills grow from three causes: too many traces kept, too many metric series, and too many verbose logs. Each of the three providers has a matching control, as the previous chapter showed. Review volume regularly, and treat a sudden jump as a sign that a new attribute or a new debug line has slipped in.
Security cameras in a shop also record customers’ faces and the numbers on the cards they hold. Good shops store this footage in a locked room, limit who may watch it, and blur what is not needed. Telemetry needs the same locked room and the same discipline.
The standard semantic conventions describe attributes that are safe and useful. Following them naturally keeps you away from dumping whole objects into telemetry.
Retention and access to telemetry data
Think about how long you keep telemetry and who may read it. Debug-level logs from last year rarely help anyone, but they may hold private details. Set retention periods in your backend, limit access by role, and treat exports of telemetry for outside parties with the same care as any customer data export. Where laws about personal data apply to you, check with your legal and security teams what telemetry may contain and how long it may stay.
18Advantages, Disadvantages and Trade-offs
Every design choice has a price. Here is an honest view of the provider model.
Advantages
- One place per signal to control identity, processing and export
- Libraries stay free of backend choices
- Safe no-op behaviour when nothing is configured
- Independent pipelines isolate failures
- Lifecycle control allows clean flush and shutdown
- Works the same way across many languages
Disadvantages
- Three providers mean three sets to configure and keep consistent
- Silent failure when a provider is not registered
- Global registration can be set only once
- Class names and maturity differ by language, especially for logs
- Resource sharing is a manual discipline unless automated
Key trade-offs
| Choice | Benefit | Cost |
|---|---|---|
| Batch processing instead of immediate send | Low overhead, fewer network calls | Short delay, and risk of loss on a sudden crash |
| Small queues | Bounded memory | More dropping during spikes |
| High sampling ratio | More detailed traces | Higher cost and load |
| Short metric interval | Fresher graphs | More data points to store |
| Global provider | Easy discovery by libraries | Harder to isolate in tests |
| Several providers in one process | Separate rules per part | More set-up and more memory |
Reading the trade-offs correctly
There is no single best setting. A small internal tool may sample everything, because traffic is low. A large retail site may keep one trace in a hundred and rely on tail sampling in the Collector for errors. The right answer depends on traffic, budget and how quickly you need to answer questions during an incident.
Choosing provider settings resembles choosing how often to take photographs on a hike. Photograph every step and your memory card fills at once, while photographing only the summit means you miss the interesting turns. You pick a rhythm to match the length of the trail and the size of the card.
When the provider model feels heavy
For a tiny script, setting up three providers can feel like too much. In such a case, use the all-in-one helper of your language or a launcher, or turn on only the one signal you need. The model scales down by simply leaving things out, and it scales up by adding readers, processors and several providers. That flexibility is worth the small learning cost.
19Design Patterns, Anti-patterns, Best Practices and Common Mistakes
These lessons come from the way teams actually run OpenTelemetry in production.
Helpful patterns
One telemetry module
Put all provider set-up in a single startup module, built once, with one shared resource.
Collector in front
Send all signals to a Collector and make routing and filtering decisions there.
Environment-driven
Take endpoints and sampling from environment variables so the same build runs everywhere.
Test providers
Use in-memory exporters with simple processors in tests, and assert on what was recorded.
Scoped names
Name tracers, meters and loggers after the module or library doing the work.
Graceful exit hook
Flush and shut down all three providers when the program is told to stop.
Anti-patterns
Creating a new provider for each request creates new threads, queues and exporters again and again. Memory and connections leak. Providers are built once and live as long as the program.
Using different service names in different providers breaks the link between traces, metrics and logs.
User identifiers, session identifiers and raw web addresses as metric attributes cause a series explosion.
The simple processor sends each item at once and blocks the caller. It is meant for tests and debugging.
Best practices at a glance
| Do | Why |
|---|---|
| Register providers first thing at startup | Libraries then find real tools, not no-op ones |
| Share one resource | Keeps all signals under one service identity |
| Use batch processors in production | Protects request latency |
| Flush and shut down on exit | Prevents losing the last data |
| Print to console during first set-up | Proves each signal works before sending to a backend |
| Add views to cut metric detail | Controls cost and memory |
| Review volume and drops | Catches silent loss early |
Common mistakes and quick fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| No telemetry at all | Provider never registered, or registered too late | Register early and test with a console exporter |
| Traces but no metrics or logs | Only the TracerProvider was configured | Configure all three providers |
| Service shows as “unknown” | No service name in the resource | Set the service name once and share it |
| Missing final spans from a job | Exited before flush | Flush and shut down before exit |
| Second registration ignored | Global slot already set by an agent | Use the existing set-up or pass providers directly |
| Huge metrics bill | High-cardinality attributes | Add views to drop them |
| Logs lack trace links | Appender not enabled, or no active span | Enable the appender and log inside spans |
Good provider hygiene is like good kitchen hygiene: set everything up before the rush, label things clearly, use the right tools for the job, and clean up properly at closing. The mistakes above are the equivalent of leaving the oven on or serving dishes without a table number.
Testing code that uses providers
Good tests prove that the right spans, metrics and logs are produced. Build a test-only provider with an in-memory exporter and a simple processor, pass it to the code under test, run the action, and then read what the exporter captured. Because the simple processor exports at once, no waiting is needed. Check names, attributes, status and parent-child links. For metrics, use a manual reader that you can trigger on demand, instead of waiting for a timer. This approach avoids global state, which is another reason to let code accept a provider as an input where practical.
Naming and consistency across teams
In an organisation with many teams, agree on a short guide: how services are named, which environment names are used, which attributes are required on every resource, and which attributes are forbidden. Put this guide in shared startup code or deployment templates, and use a checker in review to catch violations. A small amount of agreement early prevents the painful clean-up of thousands of inconsistent dashboards later.
Upgrading safely
Telemetry libraries change over time, and parts of the logs and event areas have been moving fast. Pin versions, read release notes before upgrading, and upgrade API, SDK and instrumentation packages together. After an upgrade, run your console-exporter check again to prove all three signals still flow, and watch dashboards for changes in names or units.
20A Real-World Walkthrough and Industry Usage
Let us watch the three providers at work in an imagined online shop called ShopFast. The company and numbers are made up for teaching.
ShopFast runs about forty small services. A Java agent is attached to most of them. The agent builds all three providers, reads the service name and environment from deployment variables, and sends everything to a Collector running next to each group of services. The Collector forwards traces, metrics and logs to the company’s monitoring backend.
Day one: setting up
The platform team sets standard environment variables in a shared template: the Collector address, the sampler as parent-based with a ratio of one in ten, and a metric interval of thirty seconds. Each service adds only its own name. The result is a consistent resource across all three signals, with no code changes.
Day thirty: a surprise bill
A developer adds a customer identifier as an attribute on a checkout counter. Within a day the number of metric series rises sharply, and the backend bill follows. The team adds a view in the provider settings that drops that attribute from the metric, and adds a code review rule against unbounded attributes. The cardinality limit also protects the service memory while the fix is prepared.
Day sixty: a lost final error
A nightly invoice job sometimes fails, but its last error never reaches the backend. The cause is that the job exits right after logging. The fix is a shutdown step that flushes the LoggerProvider and the TracerProvider before exit, and a few seconds of grace time at container termination. Errors from the job then appear reliably, each linked to its trace.
Day ninety: a single investigation
On a busy evening, the checkout latency graph rises. An on-call engineer clicks a point on the histogram and opens an exemplar trace. The trace shows a slow call to the payment service. From the span, she opens the correlated logs, which show “payment gateway timeout” with the same trace identifier. The problem is found in minutes, because three providers produced three signals that share one identity.
How the industry uses these ideas
The provider model is the base of OpenTelemetry SDKs used by many large organisations and by the commercial monitoring vendors that accept OpenTelemetry data. Cloud platforms and managed observability services publish their own distributions that preconfigure the providers with the right exporters. Teams commonly run a fleet of Collectors, and tune sampling and views centrally. Details differ by vendor, so always check your vendor’s documentation for supported versions and exporters.
Decision: Every ShopFast service builds its three providers from one shared resource, uses batch processors, and sends all signals to a local Collector.
Reason: It gives uniform identity, protects request latency, and moves routing and filtering out of application code.
Consequence: Services depend on a healthy Collector, so the platform team monitors Collector queues and failures.
sequenceDiagram participant Eng as On-call engineer participant Dash as Dashboard participant Tr as Trace view participant Lg as Log view Eng->>Dash: Notice latency spike on histogram Dash->>Tr: Open example trace from the spike Tr-->>Eng: Slow payment call found Eng->>Lg: Open logs for the same trace id Lg-->>Eng: Gateway timeout message Eng->>Eng: Root cause identified in minutes
Lessons from the ShopFast story
Standardise early
A shared template for environment variables gave all services the same identity from the first day.
Guard cost at the provider
Views and limits stopped one careless attribute from becoming a long-term expense.
Never skip shutdown
A flush at exit rescued the most valuable log lines, the final errors of a failing job.
Join the signals
Shared resources and trace identifiers turned three separate screens into one investigation.
A second picture: a small team
Consider a three-person start-up with one service and no platform team. They choose autoconfiguration with a hosted backend. They set the service name, the backend address and a token through environment variables, and they leave sampling at its default. For their traffic, that is enough. When traffic grows, they change only the sampler variable. They never touched the provider code, yet they benefited from every concept in this guide. Understanding providers still matters, because it tells them which variable to change and where to look when a graph is blank.
21Frequently Asked Questions
22Summary and Key Takeaways
Three desks, one identity, and a careful closing routine: that is the whole story in short.
The TracerProvider, MeterProvider and LoggerProvider are the SDK’s central desks for traces, metrics and logs. Each hands out the tool your code uses and holds the settings that decide how data is processed and sent. Code and libraries talk only to the API, and the registered providers make the calls real. If you set them up early, give them one shared resource, choose sensible processors and limits, and flush on exit, you get dependable and connected telemetry at a cost you can control.
- Each provider is a factory and a settings holder for one signal: the TracerProvider for traces, the MeterProvider for metrics and the LoggerProvider for logs.
- The API is a thin front, and without a registered SDK provider every call is a silent no-op, so register early and check output with a console exporter.
- Give all three providers the same resource so traces, metrics and logs join under one service identity.
- Each pipeline has its own controls: samplers and span processors for traces, views and readers for metrics, and log record processors with an appender bridge for logs.
- Create providers once, use batch processors in production, and keep attributes bounded to protect performance and cost.
- Flush and shut down every provider on exit, especially in short-lived jobs and serverless functions.