What Is the Difference Between Global and Local Providers in OpenTelemetry?
A plain-English guide to the two ways of handing out tracers, meters and loggers in OpenTelemetry: one shared provider for the whole program, or private providers passed around on purpose. You will learn how each works inside, when to choose which, and the mistakes that quietly leave you with no telemetry at all.
Imagine a big apartment building. Every flat gets its electricity from the building’s main power supply. Nobody has to bring their own power station. You plug in a lamp, and it simply works, because the supply is already there, shared by everyone, and managed in one place. That is wonderfully convenient.
Now imagine a camping trip. There is no main supply. You carry a small battery pack and plug your own lamp into it. Nobody else can use it by accident, and it does not matter what the building next door is doing. That is also convenient, in a different way: it is private, portable and under your control.
OpenTelemetry has exactly these two styles. A global provider is the shared supply that the whole program uses. A local provider is a private battery pack that you create and hand to the parts of your program that should use it. Both produce the same kind of telemetry. The difference is in who can find the provider, who is in charge of it, and what can go wrong. In this guide you will learn what providers are, how the global and local styles work inside, how to choose between them, and which beginner mistakes to avoid. No prior knowledge is assumed. Every new term is explained the first time it appears.
Read the first seven chapters in order, because they build the vocabulary and the two core ideas. After that, you can jump straight to the comparison table, the decision guide, the mistakes or the FAQ.
1The Big Picture: Where Does Telemetry Configuration Live?
Before we compare global and local providers, we need to understand the question they both answer: when your code wants to create telemetry, where does it find the machinery to do so?
When a program runs in production, you cannot stand next to it and watch. So you ask the program to send out information about itself. This information is called telemetry. The common kinds are traces, which follow one request through many services, metrics, which are numbers measured over time, and logs, which are timestamped messages.
To produce telemetry, a program needs a lot of behind-the-scenes machinery. Somebody has to decide where the data is sent, how often, which requests are recorded, and what name tag describes the program. This machinery has to be created and configured once, usually when the program starts. After that, hundreds of different places in the code, written by different people and sometimes by different companies, all need to use it.
The delivery problem
Here is the puzzle. Suppose a web framework library wants to record a span for every request. Suppose a database library wants to record a span for every query. Suppose your own code wants to record a span around a calculation. All three need to reach the same machinery. How does each of them get hold of it?
There are two broad answers. The first answer is: there is one well-known shared place, and everyone looks there. That is the global provider. The second answer is: whoever creates the machinery hands it directly to whoever needs it. That is the local provider. Each answer has strengths and weaknesses, and real systems often use both.
Think of a company with a single telephone directory pinned in the lobby. Anyone who needs to contact the finance team looks in the directory. That is the global approach. Alternatively, think of a manager who writes the finance team’s number on a sticky note and hands it personally to each person who needs it. That is the local approach. The directory is convenient but everyone depends on it being correct and up to date. The sticky note is precise but somebody has to remember to hand it out.
2The Basic Vocabulary: Signals, API, SDK, Tracer, Meter and Logger
A handful of small words appear in every conversation about providers. Learn them once and the rest becomes easy.
Signals
Traces, metrics and logs are called signals. Each signal has its own set of components in OpenTelemetry, and each signal has its own kind of provider.
API and SDK
OpenTelemetry splits its code into two layers. The API is a thin set of functions and interfaces that code calls to say “start a span” or “record a measurement.” The API does not know how to store or send anything. The SDK is the full implementation that actually stores, processes and exports the telemetry. Libraries should depend only on the API. Applications choose and install an SDK.
Think of a wall socket and a power plant. The socket on the wall is the API: a standard shape that every device can plug into without caring where the electricity comes from. The power plant is the SDK: the thing that really makes the electricity. A lamp manufacturer builds lamps for the socket and never needs to know about power plants.
Tracer, Meter and Logger
Each signal has a small object that your code uses directly. For traces, it is a Tracer, which creates spans. For metrics, it is a Meter, which creates instruments such as counters and gauges. For logs, it is a Logger, which creates log records. Each of these objects is given a name, usually the name of the library or module that uses it. That name is called the instrumentation scope. It tells you later which part of the code produced the telemetry.
Provider
A provider is the object that hands out tracers, meters or loggers. There is a TracerProvider for traces, a MeterProvider for metrics, and a LoggerProvider for logs. You never create a tracer out of thin air. You always ask a provider for it. The provider is also the object that holds the configuration that all its tracers, meters or loggers share. That is why the provider is the key to this whole guide.
API
The thin standard layer that code calls. It does not store or send anything by itself.
SDK
The full implementation that processes and exports telemetry.
Tracer, Meter, Logger
The small objects your code uses to create spans, measurements and log records.
Provider
The object that hands out tracers, meters or loggers and holds the shared configuration behind them.
Everything in this guide applies equally to all three kinds of provider. For simplicity we will often talk about “the provider” and use the tracer provider as the main example. Whenever you read “tracer provider,” you can mentally replace it with “meter provider” or “logger provider.”
3What Is a Provider?
Let us open up the provider and see what it contains, because the whole difference between global and local is about how this object is found and shared.
graph LR
A["Your code or a library"] --> B["Tracer Provider"]
B --> C["Tracer named for a library"]
C --> D["Span"]
D --> E["Span Processor"]
E --> F["Exporter"]
F --> G["Collector or Backend"]
B --> H["Resource: who is producing this data"]
B --> I["Sampler: which traces to keep"]
Read the figure from left to right. Your code asks the provider for a tracer. The tracer creates spans. Each finished span goes to a span processor, which batches it and passes it to an exporter, which sends it onward. The provider is the owner of the whole pipeline. It also holds two important settings that apply to every tracer it creates: the resource and the sampler.
What a tracer provider holds
- The resource. A description of who is producing the telemetry: the service name, the version, the host and the cloud region. It is attached to every span.
- The sampler. The rule that decides which traces are recorded and which are thrown away.
- The span processors and exporters. The pipeline that takes finished spans and sends them out.
- Limits. How many attributes, events and links a span may hold.
What a meter provider holds
- The resource, in the same way as for traces.
- The metric readers and exporters. The parts that collect measurements on a schedule and send them out.
- The views. Rules that reshape metrics, for example removing labels or changing how values are combined.
What a logger provider holds
- The resource.
- The log record processors and exporters. The pipeline for log records.
Think of a publishing house. The house has a name on every book (the resource), an editor who decides which manuscripts to print (the sampler), and a distribution system that ships finished books to shops (the processors and exporters). Each author who works for the house, a tracer, writes books under the house’s rules. The provider is the publishing house, and the tracers are its authors.
One provider, many tracers
A single provider usually creates many tracers, one for each library or module. Two tracers from the same provider share the same resource, the same sampler and the same exporters. That is what makes the output consistent: all spans from one program carry the same service name and go to the same place. When people say “configuring OpenTelemetry,” most of the work is configuring one provider for each signal.
4Why Providers Exist: The API and SDK Split
Providers are the bridge between the thin API that libraries use and the full SDK that applications install. Understanding this bridge explains why a global provider exists at all.
OpenTelemetry was designed so that libraries, such as web frameworks, database drivers and HTTP clients, can include instrumentation without forcing any decision on the people who use them. A library author wants to say “record a span here,” without choosing where the spans go, and without pulling a heavy SDK into every program that uses the library.
So the library depends only on the API. The application owner decides whether to install an SDK, which exporter to use and how to sample. If the application owner installs nothing, the library’s instrumentation calls still work, but they do nothing. They produce no-op spans: empty placeholders that cost almost nothing. This design is a gift to library authors and to users, because instrumentation can be shipped everywhere and switched on only where it is wanted.
How does the library find the SDK?
Here is the real question. A library compiled long before the application was written must somehow reach the SDK that the application installed later. The library cannot ask the application for it, because the library has no idea who the application is. The solution is a shared meeting point inside the API: a global registry. The application places its SDK’s provider into the registry at start-up. The library asks the registry for a provider whenever it needs one. They never meet directly, but they both know where the registry is.
The library asks the API
When the library wants a tracer, it calls the API’s global function, which says “give me a tracer for my library name.”
The API consults the registry
The API looks in its global registry for a registered provider. If one exists, it asks that provider for the tracer.
Nothing registered? A harmless placeholder
If no provider was registered, the API hands back a no-op tracer. Spans created by it do nothing, and the library keeps working without any telemetry.
The application registers an SDK provider
When the application starts, it builds a real provider and registers it as the global one. From that moment, requests for tracers are answered by the real provider.
Think of a public mailbox on a street corner. Letter writers drop letters in without knowing the postman. The postal company decides when and how to collect them. If the postal company never installs a collection service, the letters pile up unread. The mailbox is the global registry, the letter writers are libraries and the postal service is the SDK.
The main reason the global provider exists is so that independent libraries can find the application’s configuration without every application having to pass a provider to every library by hand. Keep this in mind. It explains almost all the guidance in the rest of this guide.
5What Is a Global Provider?
Now we can give a clear definition of the first of our two styles, and then see how it behaves in a real program.
A global provider is a provider that has been registered in the OpenTelemetry API’s shared, process-wide registry, so that any code in the same program can obtain tracers, meters or loggers from it without being handed the provider directly. It is called “global” because there is only one such registry slot per signal in the whole process, and everyone in the process sees the same one.
You set the global provider once, usually early in the program’s life. After that, any library, any module and any line of your own code can say “give me a tracer named such-and-such,” and receive a tracer that belongs to the global provider.
graph TD
A["Web framework instrumentation"] --> G["Global registry in the API"]
B["Database driver instrumentation"] --> G
C["HTTP client instrumentation"] --> G
D["Your own application code"] --> G
G --> P["One Global Tracer Provider"]
P --> E["One Span Processor and Exporter pipeline"]
E --> X["Collector or Backend"]
Look at the figure. Four different callers, written by different authors, all ask the same registry. The registry answers each of them with a tracer from the one global provider. All spans therefore flow through the same pipeline and end up in the same place, carrying the same resource. This is the great strength of the global approach: one configuration, applied everywhere, with no wiring.
The typical global setup
- At the start of the program, build a provider with your resource, sampler, processors and exporters.
- Register it as the global provider for its signal.
- Everywhere else, just ask the API for a tracer by name, never mentioning the provider again.
- At shutdown, flush and shut down the provider so that no data is lost.
Why most applications choose global
- It is simple. You configure once, and everything else just works.
- It works with automatic instrumentation. Agents and instrumentation libraries that you did not write cannot receive a provider from you, so they depend on the global one.
- It matches how most programs think. A program usually has one identity and one place to send its telemetry.
- It keeps code clean. You do not need to pass a provider through every function call and every constructor.
The global provider is the water tap in a house connected to the city water supply. Every room can use water by turning a tap, without anybody carrying buckets. The city’s pipes are configured once, far away from the kitchen. The occupants simply turn the tap.
The registry has only one slot per signal. That is deliberately limiting, and it prevents the chaos of many competing configurations. But it also means that the one global provider is a very important object. Whoever sets it controls the telemetry of the entire program.
6How Global Registration Works Inside
The global registry has a few special behaviors that surprise beginners. Knowing them in advance saves hours of confusion.
Behavior one: it can usually be set only once
Most SDKs allow the global provider to be set only a single time per process. A second attempt is either ignored with a warning, or rejected with an error. The reason is safety. If libraries have already obtained tracers from the first provider, replacing the provider later would leave some parts of the program using the old one and some using the new one, which would be very confusing. So the registry protects itself: first come, first served.
If two different pieces of code both try to set the global provider, the first one wins, and the second one silently or loudly loses. A common surprise is that a framework or a plugin sets the global provider before your own start-up code runs, and your carefully built provider is then ignored.
Behavior two: asking before it is set
What happens if some code asks for a tracer before any provider has been registered? The answer depends on the language and the SDK, and it is important enough to know.
- In some SDKs, the API returns a delegating placeholder. The tracer the library receives is a forwarding stand-in. When a real provider is registered later, the stand-in starts forwarding to it. Early callers are not stuck with a no-op for ever. Several SDKs behave this way, at least in part.
- In other SDKs, early callers are stuck. A tracer or a whole API instance obtained before registration may remain a no-op for the life of the program, or the act of asking may lock the registry in its no-op state so that a later attempt to register fails.
Because of this difference, the safest habit in every language is to register the global provider as early as possible, before any library asks for a tracer. We will return to this in the chapter about start-up order.
Behavior three: the registry is per signal
There is a separate global slot for each signal. Registering a global tracer provider does not register a global meter provider or logger provider. Beginners sometimes set up traces and then wonder why metrics are missing. Each signal needs its own registration.
Behavior four: the registry holds the provider, not the propagators
Context propagation, which passes trace identities between services, uses separate components called propagators, and they have their own global slot. Registering a global tracer provider does not automatically configure propagation in every SDK. We will look at this later, because it is one of the most common causes of broken traces.
| Behavior | What usually happens | Your safe habit |
|---|---|---|
| Setting the global twice | The second attempt is ignored or rejected | Set it exactly once, in one obvious place |
| Asking before setting | Depends on the SDK: forwarding stand-in or stuck no-op | Register before any library asks |
| Separate signals | One registration per signal | Register traces, metrics and logs each |
| Propagators | Registered separately from providers | Configure propagators explicitly |
| No SDK installed | The API gives no-op objects | Install an SDK in the application, not in libraries |
7What Is a Local Provider?
Now we meet the second style. A local provider is the opposite of a shared registry: it is a private object that you create and pass around yourself.
A local provider is a provider that you create and hold yourself, and that is not registered in the global registry. Code that wants telemetry from it must be given a reference to it directly, for example as a constructor argument or a function parameter. Only the code that holds the reference uses it. Nobody else in the program can find it by accident.
Other names for the same idea are an explicit provider or an injected provider. The word “injected” comes from a design style called dependency injection, in which the objects a piece of code needs are handed to it from outside rather than looked up from a shared place.
graph TD
A["Payments module"] --> PA["Local Provider A: resource payments, sampler 100 percent"]
B["Reporting module"] --> PB["Local Provider B: resource reporting, sampler 5 percent"]
PA --> EA["Exporter A"]
PB --> EB["Exporter B"]
EA --> X1["Backend for payments team"]
EB --> X2["Backend for analytics team"]
In the figure, the payments module was handed provider A, and the reporting module was handed provider B. They have different resources, different sampling rates and different destinations. They do not interfere with each other, and neither one can see the other’s provider. This is the strength of the local approach: isolation and control.
The typical local setup
- Build one or more providers, each with its own configuration.
- Do not register them globally, or register at most one of them.
- Pass each provider to the components that should use it, and let those components ask their provider for tracers, meters or loggers.
- Manage the lifecycle of each provider yourself, including flushing and shutting it down.
Why people choose local
- Testing. Each test can have its own provider with an in-memory exporter, so tests do not interfere with each other.
- Different configurations in one program. Two parts of one program may need different resources, samplers or destinations.
- No hidden shared state. The dependency is visible in the code, which some teams prefer on principle.
- Library and plugin safety. A library that builds its own provider cannot disturb the application’s global one.
A local provider is like a personal address book. You write in it the contacts you need, you keep it with you, and you give a copy to a colleague only if you choose to. Nobody can accidentally change it, and it works even if the company directory is wrong. But if you forget to share it, your colleague has no way to find it.
Anything that does not hold a reference to your local provider cannot use it. Automatic instrumentation, third-party libraries and any code that asks the global registry will never see your local provider. They will be using the global one, which may be a no-op. That is the number one source of “my telemetry is missing” in local setups.
8Global and Local Providers Side by Side
Now that you know each style on its own, let us put them next to each other. This table is the core answer to the question in the title of this guide.
| Question | Global provider | Local provider |
|---|---|---|
| How does code find it? | By asking the API’s shared registry | By being handed a reference directly |
| How many per process? | One per signal | As many as you create |
| Who can use it? | Every line of code in the process | Only code that holds the reference |
| Works with automatic instrumentation? | Yes, this is how agents and libraries find it | No, unless the library lets you pass a provider in |
| Setup effort | Low: register once | Higher: build and pass around |
| Code clarity | Clean call sites, hidden dependency | Visible dependency, more parameters |
| Testing | Harder: shared state across tests | Easier: a fresh provider per test |
| Different settings in one program | Not possible, there is one | Natural, one provider per setting |
| Risk of someone else changing it | Yes, first setter wins, others lose | Low, only the owner changes it |
| Lifecycle | One provider to flush and shut down | Every provider must be flushed and shut down |
| Typical owner | The application | A module, a test, a plugin or a tenant |
| Biggest risk | Set too late or set by the wrong code | Forgotten, unused or never flushed |
Same telemetry, different plumbing
It is worth stressing what does not differ. A span produced through a global provider and a span produced through a local provider look identical. They have the same fields, the same IDs and the same attributes. The difference lies entirely in the plumbing: how the code reached the provider, and who controls the provider. The two styles are about access and ownership, not about the kind of data.
Think of drinking water from a city tap versus from a bottle you carried. The water may be the same quality. What differs is how you get it, who controls the supply and what happens if the pipes go down. The city tap serves everyone and needs no effort. The bottle serves only you, and you must remember to bring and refill it.
Can the two styles live together?
Yes, and they often do. A very common design is to register one provider globally for the main application and use local providers for special cases, such as tests or a particular module that needs a different destination. Libraries and agents use the global provider. The special module uses its local provider. They coexist as long as everyone knows which is which.
For a normal application, register one global provider per signal at start-up, and let everything use it. Reach for local providers only when you have a specific reason, such as tests, isolation or different settings.
9Start-Up Order: The “Too Early” Problem
Timing is where most global-provider mistakes happen. Whether telemetry appears at all can depend on the order of a few lines at the start of your program.
Programs start up in a sequence: configuration is read, libraries are loaded, components are created, and the application begins serving requests. Instrumentation libraries often ask for their tracers very early, sometimes while they are being loaded. If your global provider is registered after those requests, some SDKs will already have handed out no-op tracers that never become real.
sequenceDiagram
participant App as Application start-up
participant Reg as Global registry
participant Lib as Instrumentation library
Note over App,Lib: Bad order
Lib->>Reg: Give me a tracer
Reg-->>Lib: No provider yet, here is a no-op tracer
App->>Reg: Register my real provider
Note over Lib: Library may keep the no-op tracer
Note over App,Lib: Good order
App->>Reg: Register my real provider
Lib->>Reg: Give me a tracer
Reg-->>Lib: Here is a real tracer
In the bad order shown at the top of the figure, the library asks first and receives a placeholder. When the application later registers its real provider, some SDKs forward the placeholder to the real provider, and some do not. In SDKs that do not, the library keeps its no-op tracer and produces nothing. There is no error. The program works. The traces from that library are simply absent.
Making the order right
- Register first. Make provider registration the very first thing your program does, before importing or initializing libraries that may ask for tracers.
- Use the agent or auto-configuration when possible. Agents and auto-configuration helpers are designed to register providers before application code starts.
- Avoid asking for tracers at load time. In your own code, request a tracer when you need it, or cache it after start-up, rather than at the moment a file is loaded.
- Watch for plugins that register providers. If a framework registers its own global provider before yours, yours will be ignored. Look for warnings in logs.
The silent failure
The danger of this problem is how quiet it is. Telemetry that is missing does not raise an alarm. Dashboards look empty, and people assume that nothing is happening, or that the backend is broken. The cause is often a start-up order issue that takes a minute to fix once someone knows where to look.
Many SDKs print a warning when a second attempt to set the global provider is made, or when a tracer is requested before registration. Read the first few lines of your program’s output after any change to telemetry set-up.
It is like plugging in a lamp before the electricity company has connected the house. The lamp does not complain. It simply stays dark. If, later, the power is connected, some lamps light up on their own and some need to be switched off and on again. Connect the power before you set up the lamps.
10Applications and Libraries: Who Should Do What?
The right choice between global and local depends partly on who you are. An application owner and a library author have very different responsibilities.
If you own an application
You are in charge of the whole program. You decide where telemetry goes. The usual rule is simple: build one provider per signal at start-up, and register it globally. Everything in your program, including libraries you did not write, will then find it automatically. Use local providers only for special situations.
If you write a library
You are a guest in somebody else’s program. Your library may be used by thousands of different applications, each with its own configuration. The rules for guests are strict.
- Depend only on the API. Do not include an SDK in your library. You do not know which SDK, if any, the application uses.
- Use the global provider by default. Obtain your tracer from the API’s global registry, so that the application’s configuration applies to you automatically.
- Allow the user to pass a provider in. OpenTelemetry’s guidance for instrumentation libraries is to let users supply their own provider, for example through an option or constructor argument. This helps tests and special cases.
- Never register a global provider. A library that sets the global provider hijacks the whole application. If the application also sets one, the two fight, and one loses.
- Name your tracer clearly. Use the library’s name and version as the instrumentation scope, so users can tell which spans are yours.
If you write a plugin or an extension
Plugins live inside another program and often have their own lifecycle. If your plugin needs its own destination or settings, create a local provider that you own, and never touch the global slot. If your plugin should simply join the host’s telemetry, use the global provider like any library.
Own the global slot
Build and register the provider once at start-up. Make sure it happens before libraries ask for tracers.
Use the API, accept injection
Use the global provider by default, accept a user-supplied provider, and never register a global one.
Stay out of the way
Join the host’s global provider, or use a private local provider if you truly need your own destination.
Prefer local
Give each test a fresh local provider with an in-memory exporter, and avoid changing the global slot.
Think of a hotel guest and the hotel owner. The owner decides the house rules and the thermostat. The guest can adjust the lamp in the room, but should never rewire the building. Applications are owners. Libraries are guests. A well-behaved guest uses the hotel’s power, politely, and brings their own battery pack only for something special.
11Automatic Instrumentation and the Global Provider
If you use agents or auto-instrumentation, the global provider is not just a convenience. It is how everything works.
Automatic instrumentation means that ready-made tools add measuring code to popular libraries without you writing any. It might be a Java agent attached at start-up, a Python wrapper command, or a set of Node.js packages. These tools patch or wrap libraries from the outside. They have no way to receive a provider from you, because you never call them. They therefore use the only meeting point available: the global registry.
What this means in practice
- The agent usually sets the global provider for you. A zero-code agent builds a provider from environment variables and registers it before your code runs.
- Your own code can use the same global provider. If you add manual spans, asking the global API for a tracer returns one from the agent’s provider, so your spans appear in the same traces.
- If you register your own global provider on top, you may conflict. Because the global slot can usually be set only once, either the agent’s provider or yours wins, and the other is ignored. Decide who owns configuration. Do not try to do both.
- A local provider is invisible to the agent. Spans created through your local provider do not receive the agent’s automatic spans as neighbors, unless the context links them. And the agent’s spans go to the global provider’s exporters, not your local one’s.
graph LR
A["Agent patches the web framework and database driver"] --> G["Global provider built by the agent"]
B["Your manual spans via the API"] --> G
C["Your module with a private local provider"] --> L["Local provider"]
G --> E1["Exporter one: main backend"]
L --> E2["Exporter two: separate destination"]
Configuring the agent’s provider
Because the agent builds the global provider for you, you configure it through the standard OpenTelemetry environment variables: the service name, the exporter, the sampler and so on. This is a major benefit. You get a correctly configured global provider without writing a line of set-up code. If you need something that environment variables cannot express, most agents offer an extension mechanism, which keeps you inside the agent’s single global provider instead of creating a competing one.
When an agent is present, let the agent own the global provider. Use the API to add your own spans. Introduce a local provider only for something that must be separate from the agent’s pipeline.
12How the Idea Looks in Different Languages
The concepts are the same in every OpenTelemetry language, but each language has its own habits. Here is a general feel for them, without code.
The exact function names and defaults differ between languages and between versions of the same SDK. Treat the cards below as a map of the territory, and check the official documentation for the details of the version you use.
A global OpenTelemetry object
Java bundles the three providers and the propagators into one object. A global holder lets you register it once, and a build option can create and register an SDK instance in one step. Calling the global getter before registering can lock the registry to a no-op state, and a second registration is rejected. Local use means holding your own instance and passing it in.
Setter functions on the API module
Python has setter functions for the global tracer, meter and logger providers. The global provider can be set only once, and later attempts log a warning and are ignored. Before registration, the API returns proxy objects that forward to the real provider after it is set. Local use means creating a provider and calling its own method to get tracers.
Package-level setters
Go has package-level functions to set and get the global providers. The global getter returns a delegating provider so that tracers obtained early start working after registration. The default global propagator is a no-op until you set one explicitly. Local use means keeping your provider in a struct and passing it down.
Register on the provider
In Node.js, the SDK’s provider typically has a method that registers it globally along with propagators and a context manager. Without that registration, the API remains a no-op. Local use means creating a provider and getting tracers from it directly.
Static sources and builders
.NET builds on the platform’s own diagnostic types, where sources of activity are static and identified by name. A provider built with the SDK’s builder listens for those named sources. So the idea of “global” is built into the platform, and you choose which sources a given provider subscribes to.
Ruby, PHP, Rust, C++ and more
Each follows the same specification with its own naming. Always read the documentation for the global-registration rules of your language, especially what happens when you register twice or ask early.
Defaults, function names and error handling change between SDK versions. Use this chapter to know what questions to ask: Can I register twice? What happens if I ask early? Do I need to register propagators separately? Then find the answers in your SDK’s own documentation.
13What a Provider Does Not Control: Propagators and More
A common beginner mistake is to think that registering a provider configures everything. It does not. Several important pieces live outside the provider.
Propagators
A propagator is the component that writes trace identity into outgoing messages and reads it from incoming ones, so that a trace can continue from one service to the next. Propagators are configured separately from providers. In many SDKs they have their own global slot. If you register a provider but forget the propagator, your spans may be created perfectly, but traces will break at every service boundary, because no trace headers are sent. In some SDKs the default propagator is a no-op, so you must set it explicitly.
A tracer provider is the printing press that makes tracking labels. The propagator is the worker who actually sticks the labels on parcels and reads them at the next depot. Having a printing press does not put labels on parcels.
Context management
Inside a program, the current span is carried by a context. In some languages the context is carried automatically by the runtime. In others, you must install a context manager, which keeps track of “the current span” across function calls and asynchronous tasks. The provider does not install it for you in every SDK. If the context is lost, spans become disconnected roots.
Environment-based configuration
The standard environment variables, such as those for the service name, exporter and sampler, are normally read by auto-configuration helpers that build providers for you. If you build a provider by hand, in the local style, some SDKs read the environment variables and some do not. When you create a provider manually, check whether the environment settings apply, and set the resource, sampler and exporter explicitly if you are unsure.
Spans across providers still share a trace
Providers do not own traces. A trace is identified by its trace ID, which travels in the context. If a span from provider A calls code that creates a span from provider B, the second span can still become a child of the first, because both use the same context. But each span is exported through its own provider’s exporter, so the two halves of the trace may land in different backends. If that is not what you want, use one provider, or send both providers’ data to the same place.
| Item | Part of the provider? | Where it is configured |
|---|---|---|
| Resource | Yes | On each provider |
| Sampler | Yes, for tracer providers | On each tracer provider |
| Exporters and processors | Yes | On each provider |
| Metric views | Yes, for meter providers | On each meter provider |
| Propagators | No | Their own global slot, or the bundled API object in some languages |
| Context manager | No | Separate set-up, depending on the language |
| Environment variable reading | Depends | Auto-configuration helpers, or manual reading |
14One Provider with Many Exporters, or Many Providers?
A very common reason people consider local providers is to send telemetry to more than one destination. But there is often a simpler tool.
Suppose you want your spans to go both to your main tracing backend and to an internal audit store. You might think you need two providers. In most cases you do not. A single provider can have several span processors, each with its own exporter. Every span goes through every processor, so every destination receives it. The provider’s resource and sampler apply to all of them equally.
When one provider with many exporters is enough
- You want the same telemetry in several places.
- You are happy with one resource, one sampler and one set of limits.
- You want simple lifecycle management with a single flush and a single shutdown.
When you really need several providers
- Different parts of the program need different resources, for example telemetry that should appear as coming from different services.
- Different parts need different samplers, for example 100 percent for one module and 1 percent for another.
- Different parts must send to completely separate destinations with different credentials, for example one per customer in a multi-tenant host.
- You want strict isolation, so a fault or flood in one pipeline cannot affect another.
- You are writing tests that need independent, inspectable pipelines.
| Goal | One provider, many processors | Several providers |
|---|---|---|
| Same data in two places | Best fit | Works, but more effort |
| Different sampling per module | Needs a custom sampler with rules | Natural |
| Different resource per module | Not possible | Natural |
| Per-customer destinations | Difficult | Natural, but plan for the cost |
| Simple shutdown | One call | One call per provider |
| Memory and threads | Lowest | Each provider has its own pipeline and background workers |
If you want to send the same letter to two addresses, you do not hire a second post office. You make a photocopy. A second post office, with its own stamps and its own rules, only makes sense when the two letters really must follow different rules.
Before creating a second provider, ask: “Do I need a different resource, a different sampler or strict isolation?” If the answer is no to all three, add a processor to the existing provider instead.
15Lifecycle: Flushing and Shutting Down Providers
Creating a provider is the easy half. Every provider also needs a proper ending, or the last and sometimes most important telemetry is lost.
Providers do not send each finished span instantly. For efficiency, the span processor collects spans into batches and sends a batch every few seconds or when the batch is full. Metric readers collect measurements on a timer. Log processors batch too. This means that at any moment, some telemetry is sitting in memory waiting to be sent.
When a program exits normally or crashes, the telemetry in memory is lost unless somebody tells the provider to flush, which means “send everything you are holding right now,” and to shut down, which means “flush, then release your resources and stop your background workers.”
Global provider lifecycle
- There is one provider to shut down, and it is easy to do in one place, such as the program’s exit handler.
- Some agents and auto-configuration helpers register shutdown hooks for you.
- Short-lived programs, such as scripts, jobs and serverless functions, must flush explicitly before they end.
Local provider lifecycle
- You are responsible for each provider. Nobody else knows it exists, so nobody else will flush or shut it down.
- Every provider has its own background workers. If you create many and never shut them down, you leak threads and memory.
- Never create a provider for each request. This is one of the worst mistakes: it creates new batching threads and exporters over and over, quickly exhausting resources. Providers are meant to be created once and reused.
- Shut them down in the reverse order of creation, or according to your program’s ownership, so that nothing is used after it is closed.
Create once
Build each provider at start-up or when its owner is created, never per request.
Share and reuse
Providers and the tracers they create are designed to be shared by many threads. Cache tracers rather than creating them again and again.
Flush at natural points
For short-lived programs, flush before the work ends. For long-running ones, rely on regular batching and flush at shutdown.
Shut down cleanly
On exit, shut down every provider you created, so that pending data is sent and background workers stop.
When a program crashes or is stopped, the last few seconds are often exactly what you want to see. If shutdown is not wired up, those seconds are the ones that never arrive. Make sure that a clean shutdown path exists, and test it.
16Testing: Why Local Providers Shine
If there is one place where local providers are almost always the better choice, it is automated testing. Here is why.
A good test for instrumented code asks a simple question: “When this function runs, does it produce the spans, metrics or logs I expect?” To answer it, the test needs to look at the telemetry. The usual trick is to attach an in-memory exporter, an exporter that stores finished spans in a list inside the test instead of sending them across a network. The test then reads that list and checks it.
The problem with the global provider in tests
- Shared state. The global provider lives for the whole process. Spans from one test can leak into the next, and tests may pass or fail depending on their order.
- Set once. If the global slot can be set only once, the first test wins. Later tests that try to register their own in-memory exporter are silently ignored, and their checks see nothing.
- Parallel tests. When tests run side by side, they all write into the same provider, and the results get mixed.
- Reset tricks. Some SDKs offer a special reset function for tests only. It is useful, but it is a sign that the global style fights testing.
The local provider answer
With a local provider, each test builds its own small provider with its own in-memory exporter, passes it into the code under test, runs the code, reads the result and throws everything away. Nothing is shared, so nothing leaks. Tests can run in any order and in parallel.
graph TD
T1["Test one"] --> P1["Local provider one"]
P1 --> M1["In-memory exporter one"]
T2["Test two"] --> P2["Local provider two"]
P2 --> M2["In-memory exporter two"]
T1 --> C["Code under test"]
T2 --> C
C --> P1
C --> P2
For this to work, the code under test must be able to accept a provider. This is the main design cost. Code that always reaches for the global registry cannot be pointed at a test provider. That is why library authors are advised to accept an optional provider, and why application code often takes a tracer or provider as a constructor argument.
What to test
- That a span exists with the expected name and kind.
- That important attributes are present, and that sensitive ones are absent.
- That child spans point to the right parent.
- That errors are recorded when something fails.
- That metrics move by the expected amount after an action.
Imagine testing a recipe. If every cook shares one pot, the soup from the first test ends up in the second. Giving each cook a clean pot makes each result clear. A local provider is a clean pot for every test.
If you must test code that only uses the global provider, register one test provider once for the whole test run, clear its exporter between tests, and run those tests one at a time. It works, but it is more fragile than injecting a local provider.
17Real-World Scenarios
Here are five typical situations and which style fits each. The company names and numbers are invented, to make the ideas concrete.
Scenario 1: A single web service
Situation: A small team runs one web service, deployed as several identical copies. They use an agent for the web framework and database. They add a few manual spans for business steps.
Choice: Global provider, registered by the agent or at the very start of the program. There is no reason for anything else.
Why: One service name, one destination, one sampling policy. Simple and complete.
Scenario 2: A shared library used by many teams
Situation: A platform team ships a payment client library. Dozens of services use it, each with their own set-up.
Choice: The library depends on the API only. It gets its tracer from the global registry by default and offers an option to pass a provider in.
Why: Every service’s telemetry settings apply automatically, while tests and special cases can inject a provider.
Scenario 3: A multi-tenant host
Situation: One program hosts workloads for several customers. Each customer wants telemetry in their own backend with their own credentials, and must never see another customer’s data.
Choice: One local provider per customer, each with its own resource attributes, exporter and credentials. A routing layer passes the right provider to each customer’s code.
Why: Strict isolation. A single global provider cannot separate customers. The team must plan the lifecycle and memory cost for every provider.
Scenario 4: A unit-test suite
Situation: A team wants to check that every checkout operation produces a correct span and increments a counter.
Choice: A fresh local provider with in-memory exporters in each test.
Why: Fast, isolated, order-independent tests with no network.
Scenario 5: An audit module with its own rules
Situation: A module must record every sensitive action at 100 percent, to a secure audit store, while the rest of the program samples one percent to the main backend.
Choice: The main application uses the global provider with a low sampling rate. The audit module owns a local provider with an always-on sampler and its own exporter.
Why: Different sampler, different destination, and a clear boundary. The audit module is the only code that holds that provider.
| Scenario | Style | Main reason |
|---|---|---|
| Single web service | Global | Simplicity and agent support |
| Shared library | Global by default, injection allowed | Respect the host’s settings |
| Multi-tenant host | Local per tenant | Isolation and separate credentials |
| Unit tests | Local per test | No shared state |
| Audit module | Global plus one local | Different sampling and destination |
18Design Patterns for Handling Providers
Over time, teams have settled on a few tidy ways to organize providers. Here are the most common ones, described in plain words.
Single global, set at start-up
One place in the program, executed first, builds the providers and registers them. Everything else uses the API. This is the default for most applications.
Global with optional injection
Components use the global provider unless a provider is passed in. This keeps production code simple and tests easy. Library authors usually adopt it.
Dependency injection of providers or tracers
A container or composition root creates the provider once and hands tracers to each component that needs them. Nothing reaches for a global. Dependencies are explicit and tests are simple.
Provider registry per tenant
A small lookup service holds one provider per tenant or per environment, creates them on demand, caches them and shuts them all down at exit.
Test fixture provider
A helper in the test library builds a provider with an in-memory exporter and gives back both. Every test calls the helper, so no test repeats the set-up.
Thin wrapper around the API
Some companies write a tiny internal module that returns tracers for the company’s naming rules. It hides whether the provider is global or local, so the choice can change later in one place.
Which pattern should you pick?
Start with Pattern 1. Add Pattern 2 if you write libraries. Move to Pattern 3 if your code base already uses dependency injection or if you want maximum clarity. Use Patterns 4 and 5 only when the situation, such as multi-tenancy or heavy testing, demands it.
These patterns are like different ways to supply a kitchen with water. One main tap for the whole house is Pattern 1. A main tap with a bucket for special jobs is Pattern 2. Piping water to each appliance directly is Pattern 3. A separate well for each tenant is Pattern 4.
19A Decision Guide: Global or Local?
Here is a simple set of questions that leads you to an answer for almost any situation.
graph TD
S["Start: do I need a provider?"] --> Q1{"Is this the main application?"}
Q1 -->|"Yes"| Q2{"Do I need different resource, sampler or isolation for part of it?"}
Q1 -->|"No, a library"| LIB["Use the global provider via the API and accept an optional provider"]
Q2 -->|"No"| GL["Register one global provider per signal at start-up"]
Q2 -->|"Yes"| BOTH["Global for the main part, local providers for the special parts"]
S --> Q3{"Am I writing a test?"}
Q3 -->|"Yes"| LOC["Use a fresh local provider with an in-memory exporter"]
Walking through the chart
- Is it a test? Use a local provider with an in-memory exporter. Done.
- Is it a library? Use the global provider through the API by default, and let users pass their own.
- Is it the main application with ordinary needs? Register one global provider per signal, before libraries ask for tracers.
- Does a part of the program need a different resource, sampler or isolation? Keep the global provider for the main part, and give that part its own local provider.
Extra questions to check yourself
- Will automatic instrumentation or agents be used? If yes, you need a global provider for them.
- Do I really need a second provider, or would one more processor on the existing provider do the job?
- Who will flush and shut down each provider?
- Have I registered propagators and context management, which are not part of the provider?
Put your decision and its reason in a short architecture note: “We use one global provider; module X has a local provider because it needs 100 percent sampling.” Future teammates will thank you.
20Advantages, Disadvantages and Trade-Offs
Neither style is perfect. Knowing what you give up helps you choose with open eyes.
Global providers: advantages
- Almost no set-up effort after the first registration.
- Works with agents and auto-instrumentation.
- Libraries pick up your settings automatically.
- One lifecycle to manage.
- Clean call sites with no extra parameters.
Global providers: disadvantages
- Hidden dependency that is easy to forget.
- Start-up order can silently break telemetry.
- Hard to test because of shared state.
- Only one configuration for the whole process.
- Another piece of code can take the slot first.
Local providers: advantages
- Strong isolation between parts of a program.
- Easy, parallel-safe testing.
- Different resource, sampler or destination per provider.
- Explicit dependencies that are easy to read.
- No fight over a single global slot.
Local providers: disadvantages
- Invisible to agents and to libraries that use the global registry.
- More code: creating, passing and closing providers.
- Every provider needs its own flush and shutdown.
- More memory and background workers.
- Easy to forget to use the right one, so telemetry goes missing.
Main trade-offs
| You gain | You give up |
|---|---|
| Convenience of global access | Visibility of the dependency and testability |
| Isolation of local providers | Automatic pick-up by libraries and agents |
| Flexibility of many providers | Simple lifecycle and low resource use |
| Simplicity of one provider | Different settings for different parts |
Decision record: example
Context: A service needs full traces for payment actions and low sampling for everything else.
Decision: Use one global provider with a parent-based, ratio sampler, and one local provider for the payment module with an always-on sampler.
Consequence: Payment spans are not seen by agent instrumentation unless explicitly passed. The team must flush both providers on shutdown and document the exception.
21Common Mistakes and How to Avoid Them
Almost every team makes at least a few of these. Learn them now, and save yourself a long debugging evening.
Mistake 1: Registering the provider too late
What happens: Libraries ask for tracers before the real provider exists and keep no-op objects. Traces are missing, with no error.
Fix: Register providers first thing in the program, or use an agent or auto-configuration that does it before your code.
Mistake 2: Creating a provider for each request
What happens: Each provider starts new background workers and exporters. Memory and threads grow until the program slows or fails.
Fix: Create providers once. Reuse them and the tracers they hand out.
Mistake 3: Using a local provider and expecting agents to see it
What happens: Automatic spans go to the global provider’s destination, and manual spans to the local one. Traces appear split between two places or incomplete.
Fix: Use the global provider unless you have a clear reason to separate. If separate, accept that the agent’s data will not use the local provider.
Mistake 4: Registering two global providers
What happens: The second is ignored or rejected. Your configuration may not be the one that is active, for example the agent’s wins over yours.
Fix: Decide who owns the global slot, and read start-up warnings.
Mistake 5: Forgetting to flush and shut down
What happens: The final batch of telemetry, often the most useful, is lost, especially in short jobs and serverless functions.
Fix: Shut down every provider at exit. Test the shutdown path.
Mistake 6: A library that registers a global provider
What happens: The library takes over the application’s configuration, or fights with it.
Fix: Libraries must depend on the API only and never set a global provider.
Mistake 7: Forgetting propagators
What happens: Spans are created, but traces break at every service boundary because no trace headers are sent.
Fix: Register propagators separately and check them with a two-service test.
Mistake 8: Mixing resources by accident
What happens: A local provider is built without a service name and appears in the backend as an unknown service.
Fix: Give every provider an explicit resource with a service name, and check how it appears in the backend.
Mistake 9: Using the global provider in parallel tests
What happens: Tests pass alone and fail together, or the other way around.
Fix: Use a local provider per test, or run global-dependent tests one at a time.
Mistake 10: Creating a second provider when one would do
What happens: Extra memory, extra threads and a more complicated shutdown, with no benefit.
Fix: Add a second processor or exporter to the existing provider unless you need a different resource, sampler or isolation.
22Changing Your Mind: Moving Between Global and Local
Real programs grow. A design that was right last year may need to change. Here is how to move between the two styles without breaking your telemetry.
From global to local for one module
Imagine that a module, such as billing, now needs its own sampling rate and its own destination. You do not have to rebuild the whole program. Follow a gentle path.
- Find every place the module gets a tracer. List each call that asks the global registry. If there are many, wrap them behind one small function first, so that you change only one place.
- Create the new provider. Give it its own resource, sampler and exporter. Write down why it exists.
- Pass it in. Give the module the new provider through its constructor or its set-up call, rather than letting it reach for the global one.
- Check parents and children. Make sure that context still flows between the module and the rest of the program, so that the traces stay connected.
- Add shutdown. Include the new provider in your exit handling.
- Compare before and after. Confirm that the module’s spans appear in the new destination, and are gone from the old one.
From local back to global
Sometimes a local provider turns out to be unnecessary. Perhaps the different sampling rule can be written into one sampler, or the extra destination can be a second processor. Merging is usually a simplification, and it is worth doing when the reasons for the split have gone.
- Add the extra processor or sampler rule to the global provider.
- Switch the module to the global tracer, with the same instrumentation scope name as before.
- Check that its telemetry still arrives where it should.
- Shut down and remove the old provider.
From no provider to a global provider
Many programs start with no telemetry set-up at all. Their libraries are instrumented through the API, so everything is a no-op. Adding a global provider is the smallest possible step. Register it first thing at start-up, and traces from every instrumented library will begin to appear. This is the most rewarding change in the whole subject, and it needs very little code or risk.
Keep the change reversible
- Hide the decision behind one small helper in your own code, so that switching styles later touches one file.
- Use a configuration switch, so that you can turn the new provider off and fall back to the old one if something goes wrong.
- Roll out to a small share of traffic first and watch for missing or duplicated telemetry.
- Keep the instrumentation scope names stable, so that dashboards and alerts built on them do not break.
Moving between the two styles is like deciding whether a family shares one kitchen or a flat gets its own. Giving one flat its own kitchen is a reasonable improvement when its needs differ. Taking the kitchen out again is fine once the need goes away. In both cases, you plan the plumbing, move the appliances carefully and check that nobody is left without dinner.
During a move, the same spans can be exported twice, once by the old provider and once by the new one. Dashboards then show double counts. Check volumes during the transition and remove the old path as soon as the new one is proven.
23Best Practices Checklist
If you remember nothing else from this guide, follow this list. It is built from everything covered so far.
Default to one global provider per signal
For an application, build it once at start-up and register it first. Move away from this default only for a clear reason.
Register before anything asks
Make telemetry set-up the first action in the program, so that libraries never receive no-op tracers.
Libraries use the API only
Never include an SDK in a library, never register a global provider from it, and allow an optional provider to be passed in.
Use local providers for tests, isolation and special needs
Keep each local provider small, named and owned by one module.
Prefer more processors over more providers
Add a second destination to the existing provider unless you need a different resource, sampler or isolation.
Never create providers per request
Create once, cache tracers and reuse them.
Set a resource on every provider
At least the service name, so that telemetry never shows up as an unknown service.
Register propagators and context separately
Remember that the provider does not carry them, and test a call across two services.
Flush and shut down every provider
Do it at exit, and test that it works, especially for short-lived programs.
Document the decision
Write down which provider is global, which are local, who owns them and why.
Add “Does this change create or register a provider?” to your code review checklist. Provider changes are rare but have large effects.
24Security, Privacy and Cost
Providers decide where your telemetry goes and what rides along with it. That makes them a place where security, privacy and cost are quietly decided.
Security
- Credentials live in exporters. Each provider’s exporter may hold a token or certificate. A local provider per tenant means many credentials, each of which must be stored safely and rotated.
- Isolation is a security feature. In a multi-tenant host, separate providers keep one customer’s telemetry out of another’s backend. A single global provider cannot offer that boundary.
- Protect the registration point. Whoever can change the start-up code or environment variables can redirect all telemetry. Limit and review that access.
- Secure the transport. Use encrypted connections for every exporter, global or local.
Privacy
Resource attributes set on a provider travel with every signal that provider emits. If a resource contains a customer name, a host name you consider sensitive or an email address, it is copied into every span, metric and log. Review resources as carefully as you review attributes on spans. With local providers, make sure that tenant-specific data stays with that tenant’s provider and never in the shared global resource.
Cost
The cost of a provider is small, but it is not zero, and it multiplies with the number of providers. Ten providers in a service cost roughly ten sets of queues, workers and connections. For a few providers, this is negligible. For hundreds, such as one per customer in a large host, it can become a real concern, and you may need shared exporters, limits on queue sizes or lazy creation of providers.
The backend cost is separate. It depends on how much telemetry you send, not how many providers produce it. Splitting telemetry across providers does not make it cheaper. Sampling and suppression do.
A registry that creates a provider for each new tenant, and never removes the old ones, leaks resources slowly. Close and remove providers that are no longer needed.
25Monitoring and Troubleshooting
When telemetry goes missing, the cause is very often a provider issue. Here is a short guide to finding it.
Symptom: no telemetry at all
- Was a provider ever registered? Without an SDK, the API quietly produces nothing.
- Was the SDK disabled by an environment setting?
- Is the exporter pointing to the right address, with working credentials and network access?
- Did the program exit before flushing?
Symptom: telemetry from some libraries is missing
- Did those libraries ask for tracers before registration? Move registration earlier.
- Are they using a different provider, for example a local one, from the rest?
- Is their instrumentation installed and enabled?
Symptom: traces are split between services
- Are propagators registered on both sides, and do they use the same format?
- Is the context lost in asynchronous code?
- Do the two halves go to different backends because they use different providers?
Symptom: my configuration seems ignored
- Was another global provider registered first, perhaps by an agent or a framework? Look for warnings about setting the provider twice.
- Are you looking at the right service name? A missing resource may make telemetry appear under an unexpected name.
Symptom: memory or threads keep growing
- Is a provider being created per request or per task?
- Are old providers shut down when no longer needed?
graph TD
A["Telemetry missing"] --> B{"Was a provider registered or injected?"}
B -->|"No"| C["Register one before use"]
B -->|"Yes"| D{"Registered before libraries asked?"}
D -->|"No"| E["Move registration to the start"]
D -->|"Yes"| F{"Does the code use the same provider as the exporter you check?"}
F -->|"No"| G["Align global and local usage, or check the other backend"]
F -->|"Yes"| H["Check exporter address, credentials and flush on exit"]
Monitoring the pipeline itself
Some SDKs expose counters for dropped spans, full queues and failed exports. If yours does, watch them for each provider. A queue that is often full means the exporter cannot keep up. Failed exports mean network or credential problems. Many SDKs also log internal errors, and turning that logging on for a short time during set-up is one of the fastest ways to find provider problems.
Troubleshooting missing telemetry is like finding why a lamp does not light. First check that it is plugged in (a provider exists), then that the power is on (registered early enough), then that it is the right lamp on the right circuit (global or local), and last that the bulb works (exporter and network).
26Frequently Asked Questions
Short, direct answers to the questions beginners ask most often.
27Summary and Key Takeaways
You began with a building’s power supply and a camping battery pack. You now have a full map of how OpenTelemetry providers are shared and separated.
A provider is the factory that hands out tracers, meters and loggers, and holds the settings that decide what happens to telemetry. The API and SDK split lets libraries ask for these objects without knowing the details. A global provider is registered in one shared slot, so that every piece of code, including agents and libraries, finds it automatically. A local provider is created and held by you, giving isolation, flexibility and easy testing, at the cost of invisibility to anything that does not receive it. The best choice follows from who you are and what you need: applications default to one global provider, libraries use the API and accept injection, and tests and special modules use local providers.
Key Takeaways
- A provider is the factory and the settings holder: it hands out tracers, meters and loggers and decides resource, sampling, processing and export.
- Global means shared and found automatically: one slot per signal, usually set once, used by libraries and agents through the API.
- Local means owned and passed explicitly: excellent for tests, isolation and different settings, but invisible to code that only asks the global registry.
- Order matters: register the global provider before libraries ask for tracers, or telemetry may silently vanish.
- Providers are not everything: propagators, context management and some configuration are separate and must be set up too.
- Prefer simplicity: one global provider per signal for applications, an extra processor rather than an extra provider, and never a provider per request.
- Own the lifecycle: flush and shut down every provider, set a resource on each, and document which provider is global, which are local and why.