How Do You Build a Custom Processor in OpenTelemetry?
A step-by-step guide to designing, writing, testing, building, and safely running your own Collector processor, and to knowing when you should not build one at all.
Picture a busy post office. Letters arrive from every direction, and before they go out again, clerks stamp some of them, fix wrong addresses, throw away junk mail, and sort the rest into bags. Nobody sees these clerks from the street, yet the whole system depends on them. In OpenTelemetry, the clerks in the middle are called processors. They sit between the place where telemetry arrives and the place where it is sent, and they change it on the way through.
OpenTelemetry already ships dozens of ready-made processors. Most of the time, one of them does the job. But sometimes your company has a rule that no ready-made processor knows, such as “stamp every span with the team that owns the service, using our own ownership list that changes every day”. That is when you build a custom processor. This guide teaches the whole journey in plain steps: what a processor is, how to decide whether to build one, how it is put together, how to test it, how to package it, and how to keep it healthy for years.
1What a Custom Processor Is, and Why People Build One
Before writing anything, let us be clear about what we are making and what problem it solves.
The simple definition
A processor in the OpenTelemetry Collector is a small piece of software that receives a batch of telemetry, does something to it, and passes it on. Telemetry means the three kinds of data that applications produce: traces (the journey of a request), metrics (numbers measured over time), and logs (text records of events). A processor can add labels, remove labels, rename things, drop whole items, combine items, or hold items for a while before sending them on.
A custom processor is one that you write yourself, because no existing one does what you need. It becomes part of your own build of the Collector, and it behaves exactly like the built-in ones: you list it in a configuration file, and the Collector runs it.
Think of a kitchen assembly line. Plates move along a belt. One cook adds sauce, another garnishes, another removes plates that came out wrong. A custom processor is a new cook that you hire for one special job, such as “add the allergy card for tables that asked for it”. The belt, the plates, and the other cooks stay the same. Only the new cook is yours.
Why not just change the applications?
You could add the same labels inside every application. But a company may have hundreds of services written in many languages, and changing all of them for each new rule is slow and easy to get wrong. A processor works in one central place. Change it once, and every service benefits at once, with no new release of any application.
Common reasons for a custom processor
Enrichment from private data
Adding labels that come from your own systems, such as team, cost center, or customer tier.
Company-specific cleaning
Removing or masking values that only your company knows are sensitive, in your own formats.
Special routing or limiting
Applying your own budget or fairness rules that no general tool offers.
Format repair
Fixing telemetry from old systems whose names or units do not match your standards.
Aggregation
Combining many small items into fewer summary items to save cost.
Integration with internal services
Looking something up in an internal catalog, with caching, so that data carries more meaning.
Notice that every reason above needs something the standard processors cannot see: private data, private rules, or private systems. If your need can be described with the standard tools, you probably do not need custom code. Chapter 4 teaches how to check.
2Two Meanings of “Processor” in OpenTelemetry
The same word is used in two different places, and mixing them up causes a lot of confusion for newcomers.
Meaning one: the Collector processor
The Collector is a separate program that receives telemetry from many applications, processes it, and sends it to storage. A processor in this sense is a pipeline stage inside that program. This is what most of this guide is about. Collector components are written in the Go programming language.
Meaning two: the SDK span processor
Inside each application, the OpenTelemetry SDK (the library that creates telemetry) also has things called span processors and log record processors. These live inside the application. They are called when a span starts and when it ends. The built-in ones batch spans before export or send each span immediately. You can write your own in whatever language the application uses, such as Java, Python, JavaScript, or Go. Metrics do not have processors in the SDK in the same way; they use views and readers instead.
| Question | Collector processor | SDK span processor |
|---|---|---|
| Where does it run? | In the separate Collector program | Inside each application |
| What language? | Go | The application’s own language |
| What does it see? | Batches from many services at once | Spans of one application, one at a time |
| When does it run? | After data leaves the application | When a span starts and ends |
| Can one change cover all services? | Yes | No, each application must be updated |
| Can it see the live request context? | No, only the finished data | Yes, such as the current user or request details held in memory |
| Cost to the application | None, apart from sending data | Uses the application’s CPU and memory |
An SDK span processor is like a note you write on a parcel while you are packing it at home. You know everything about what is inside. A Collector processor is a clerk at the sorting center who stamps parcels from many homes. The clerk cannot see inside the box, but can stamp thousands of parcels the same way, all at once.
Which one should you pick?
Use an SDK processor when you need live information that disappears after the request, such as who the logged-in user is, or when you must stop sensitive data before it ever leaves the application. Use a Collector processor when the change is the same for many services, depends on data that lives outside the application, or should be changeable without redeploying anything. This guide focuses on the Collector, because that is where custom processors are most useful and where the engineering is deepest. Chapter 22 mentions an SDK-side example for comparison.
3Collector Refresher: Pipelines, Components, and the Data Model
To build a part, you need to know the machine it fits into. Here is a gentle tour.
The five kinds of Collector component
Receiver
The front door. It accepts data from applications or other systems, in formats such as OTLP.
Processor
The middle stages. They change, filter, or hold data. This is what we are building.
Exporter
The back door. It sends data to its final home, such as a tracing backend or storage.
Connector
A bridge between two pipelines. It acts as an exporter for one and a receiver for another, for example turning spans into metrics.
Extension
A helper that is not part of the data path, such as a health check endpoint or a credential provider.
Pipelines
A pipeline connects these components for one kind of data. A traces pipeline has receivers, then processors in a fixed order, then exporters. The same Collector can run several pipelines at once, such as one for traces and one for logs. Processors run in exactly the order you list them, and that order matters a great deal.
flowchart LR
R1["OTLP receiver"] --> P1["Memory limiter"]
R2["Other receiver"] --> P1
P1 --> P2["Your custom processor"]
P2 --> P3["Attributes processor"]
P3 --> P4["Batch processor"]
P4 --> E1["Exporter to backend"]
P4 --> E2["Exporter to archive"]
In Figure 1, data enters on the left from two receivers. The memory limiter comes first, to protect the Collector. Your custom processor comes next, then another standard processor. The batch processor is last, grouping data for efficient sending. Both exporters receive a copy.
The shape of the data
Inside the Collector, telemetry is held in a structure called pdata (short for pipeline data). Understanding its shape is the key to writing processors, because every processor walks through it. For traces, the shape is a set of nested boxes.
flowchart TD
T["Traces batch"] --> RS["Resource spans, one per source such as a service instance"]
RS --> RA["Resource attributes such as service name"]
RS --> SS["Scope spans, one per instrumentation library"]
SS --> SP["Span, one step of a request"]
SP --> SA["Span attributes, events, links"]
Look at Figure 2. A batch contains many resource boxes. A resource describes who sent the data: the service name, the version, the machine, the cluster. Inside each resource are scope boxes that describe which library produced the items. Inside each scope are the spans themselves, each with its own attributes. Logs follow the same layout with log records at the bottom, and metrics have metrics with data points at the bottom.
This layout matters for performance. A single resource, such as “checkout service, instance 7”, may hold thousands of spans that all share the same resource attributes. A smart processor does its expensive work once per resource, not once per span.
When you design a processor, always ask first: “Is my rule about the sender (resource), the library (scope), or the individual item (span, log, data point)?” The answer tells you which level of the nested boxes to work on, and keeps your processor fast.
4Do You Really Need a Custom Processor?
The best custom code is the code you never had to write. Let us check the ready-made options first.
The toolbox that already exists
| Need | Ready-made option | What it can do |
|---|---|---|
| Add, change, delete, or hash labels by simple rules | Attributes processor | Set fixed values, copy between labels, delete, hash, and extract |
| Change labels that describe the sender | Resource processor | Edit resource-level labels |
| Drop items that match conditions | Filter processor | Remove spans, logs, or metrics by conditions written in a small expression language |
| Rewrite almost anything with flexible rules | Transform processor | Uses the OpenTelemetry Transformation Language (OTTL) to change names, labels, and values with conditions and functions |
| Mask sensitive values | Redaction processor | Allow-list labels and mask values that match patterns |
| Add Kubernetes details | Kubernetes attributes processor | Looks up pod, namespace, and workload details and caches them |
| Detect where the Collector runs | Resource detection processor | Adds cloud and host details |
| Keep a portion of traces | Probabilistic and tail sampling processors | Sample by percentage or by whole-trace rules |
| Protect memory, group for sending | Memory limiter, batch processor | Safety and efficiency |
What the transformation language can already do
OTTL is powerful. With the transform and filter processors, you can write conditions such as “if the service name matches this pattern and the status is an error, then set this label to this value”. You can combine labels, split text, use patterns, and work on traces, metrics, and logs. Many teams are surprised how far this goes. It is a good rule to try OTTL first and write custom code only when it clearly falls short.
Where OTTL and the standard processors fall short
- Outside lookups. They cannot call your internal catalog service or read a file that changes every hour, and then keep the result fresh.
- State across batches. Rules that need memory of earlier data, such as “keep the first ten errors of each kind per minute”, need a place to keep counts.
- Complex algorithms. Custom hashing, special rate limiting, or business calculations may not fit in expressions.
- Special performance needs. A very hot path may need tuned code instead of general expressions.
- New behavior shared across teams. A rule that many teams will use may deserve a proper component with its own settings and documentation.
flowchart TD
Q1{"Can the change be done in the application with a small code change"} -->|"yes and only one or two services"| A1["Change the application or its SDK"]
Q1 -->|"no or many services"| Q2{"Can standard processors or OTTL express the rule"}
Q2 -->|"yes"| A2["Use standard processors and configuration only"]
Q2 -->|"no"| Q3{"Does it need outside data, state across batches, or special algorithms"}
Q3 -->|"yes"| A3["Build a custom processor"]
Q3 -->|"no"| A4["Re-check the rule, it may be simpler than it looks"]
A custom processor is a long-term promise. You must keep it compatible with every Collector upgrade, test it, secure it, and explain it to new teammates. Treat “write custom code” as a purchase with a monthly fee, not as a free choice.
5Anatomy of a Processor Component
A Collector processor is a small Go module with a handful of files, each with one clear job.
Think of opening a small shop. You need a sign and a license (the identity), a rulebook for how customers can order (the settings), a person who actually does the work (the logic), a notice board telling the city what you sell (the metadata), and a checklist to prove the shop works (the tests). A processor module has the same five parts.
| File or folder | Job | Plain-words purpose |
|---|---|---|
| go.mod | Declares the module and its dependencies | The list of parts you borrow, with exact versions |
| factory file | Creates the processor and its default settings | The front desk that builds a new processor when the Collector asks |
| config file | Defines the settings people can put in the configuration | The order form for your processor |
| processor file | Holds the real logic | The cook doing the work |
| metadata.yaml | Describes the component for tooling: name, stability, signals, internal metrics | The notice board |
| generated metadata folder | Code created from metadata.yaml by a tool called mdatagen | Auto-written helper code you do not edit by hand |
| test files | Unit, golden-file, and benchmark tests | The proof that it works |
| README | Explains settings, behavior, and limits | The manual for users |
How the pieces talk to each other
When the Collector starts, it reads the configuration, finds the processor name, and asks that processor’s factory to create an instance. The factory hands the Collector a processor object. For every batch of telemetry that arrives, the Collector calls the processor’s function, which does the work and passes the result to the next stage. When the Collector shuts down, it tells the processor to stop. Chapter 8 follows this path in detail.
Naming and identity
Every component has a type name, a short lowercase word such as batch or attributes. This is what appears in the configuration file. Choose a clear, specific name that describes what the processor does, not who wrote it. A name like ownershipenricher tells a reader what to expect, while mycompanyprocessor tells them nothing. Names should not clash with the ones in the official repositories, because your build may someday include both.
Stability levels
Collector components declare how mature they are: development, alpha, beta, or stable. Even for internal code, being honest here helps your users. A new processor should start at a low level and move up only after it has run in production, has full tests, and has settings that you promise not to break without notice.
Start your module by copying the layout of a small, well-kept processor from the official repositories, then replace its logic with yours. Following the same layout means that the official tools and any future contribution work without surprises.
6The Factory and the Configuration
These two parts decide how pleasant your processor is to use, long before any telemetry flows.
The factory: a builder that knows your recipe
The factory has four small duties. It reports the processor’s type name. It can create the default configuration, so that users only need to write the settings they care about. It states which kinds of telemetry the processor supports, for example traces only, or traces and logs. And for each supported kind it has a function that creates the processor from the settings and a handle to the next stage. Each supported kind also states a stability level.
The Collector provides helper functions to build factories and processors, so you rarely need to write the plumbing by hand. Exact names and details change between Collector releases, so always read the guide for the version you build against rather than copying old examples blindly.
Supporting more than one signal
If your rule makes sense for traces, metrics, and logs, support all three, but think carefully. The three kinds of data have different shapes and different risks. A logic that is easy on spans may be tricky on metric data points, whose meaning depends on their type, such as gauges, sums, and histograms. Many good processors start with one signal and add the others later, each with its own tests.
The configuration: the order form
The configuration describes every setting a user can choose. In Go it is a plain structure with a label for each field so that the Collector can fill it from the YAML file. Good configuration follows a few simple habits.
- Sensible defaults. The processor should work with almost no settings. Every required setting is a chance for someone to make a mistake.
- Validation at startup. A configuration object can check itself. If a time setting is negative, or a file path is empty, the Collector should refuse to start and say exactly why. Failing loudly on the first minute is far better than failing quietly on the first night.
- Explicit units. Use time durations and say what they mean. A setting called “interval” is vague. A setting called “reload interval” with a duration is clear.
- Strictness. The Collector rejects settings it does not recognize, which catches spelling mistakes. Keep it that way.
- Secrets by reference. Never ask users to paste passwords into the file. Allow references to environment variables or secret stores, which the Collector’s configuration system already supports.
- Few knobs. Every setting must be documented, tested, and supported forever. Offer fewer choices with better defaults.
| Good setting design | Weak setting design |
|---|---|
| Reload interval as a duration with a default of five minutes | A number called “time” with no unit |
| On-miss behavior chosen from a short list: leave unchanged, use default, or drop | A free text field that is parsed in different ways |
| Validation error: “reload interval must be at least ten seconds” | Silent clamp to some other value |
| One section for each feature, clearly named | Dozens of flat, similar-looking fields |
Write the settings guide as you go
For every setting, write three lines in the README as soon as you add it: what it does, what the default is, and an example of when you would change it. Add a short complete example configuration that someone can copy and run. Documentation written a month later is always thinner and often wrong, because the reasons behind each choice have been forgotten. A good test of your documentation is to hand it to a teammate who has never seen the processor and see whether they can set it up without asking you a single question.
A configuration setting is a public promise. If you rename or remove it later, every team’s configuration file breaks on upgrade. Think twice before adding a setting, and plan a gentle path, such as warning in one release and removing in a later one, if you must change it.
7Writing the Processing Logic
This is the heart of the component: the function that is called again and again with batches of telemetry.
The shape of the work
For each kind of data, the processor has one function. It receives the context of the call and a batch of data. It returns the batch, possibly changed, and an error if something went wrong. The Collector provides a helper that wraps your function into a full processor object, adds the standard behavior around it, and wires it to the next stage. Your job is only the middle: look at the batch, change what needs changing, and hand it back.
Imagine a clerk at a conveyor belt. A tray of letters arrives. The clerk looks at the sender on each bundle, stamps the bundle, removes any letter that breaks the rules, and slides the tray onward. The clerk never keeps a letter after sliding the tray away, because the next clerk now owns it.
Walking the nested boxes
Remember the layout from Chapter 3: resources contain scopes, and scopes contain items. Almost every processor follows the same walk. It loops over the resources. For each resource it may do resource-level work, such as reading the service name. Then it loops over the scopes, and over the items inside them, doing item-level work when the rule needs it. To remove items, the data structures offer a helper that removes every item matching a condition in one pass, which is safer and faster than deleting while looping. If a resource or scope ends up empty after removal, a tidy processor removes the empty box too.
Declare whether you change the data
Every processor tells the Collector whether it mutates data. This one flag is easy to forget and causes strange bugs. Here is why it matters. When one receiver feeds two pipelines, the Collector would like to share a single copy of the data to save memory. But if one pipeline changes the data while another is still reading it, the second pipeline sees corrupted results. So the Collector makes a private copy for any consumer that says “I mutate”. If your processor changes data but forgets to say so, it may silently alter data that other pipelines are using.
Ownership: the “do not touch” rule
Once you pass the batch to the next stage, it belongs to that stage. Do not read it, change it, or keep a reference to it afterward, and never hand it to a background task that outlives the call. If you need to remember something from a batch, copy the values you need into your own structures before you pass the batch on.
Concurrency: many calls at once
The Collector may call your function from several threads at the same time. Any shared state, such as a lookup table, a counter, or a cache, must be safe for parallel use. There are two common styles. One is a lock that protects the state, with many readers allowed together. The other, usually better for read-heavy cases, is an immutable snapshot: build a fresh, complete lookup table in the background, then swap a single pointer to it in one atomic step. Readers always see a whole, consistent table, never a half-built one, and they never wait.
The three signals are not the same
The same processor idea needs different care for each kind of data. Traces are made of spans. A span has a name, a kind, a status, start and end times, attributes, small notes called events, and links to other spans. If your processor renames spans, remember that backends group spans by name. A name that contains an ID, such as a user number, creates millions of tiny groups and makes charts useless.
Logs are made of log records. Each has a time, a severity such as “info” or “error”, a body that holds the message, attributes, and often the IDs of the trace and span it belongs to. If your processor changes or masks the body, keep the trace and span IDs intact, because they are the thread that connects logs to traces. The body may be plain text or a structured value, so check what you received before assuming it is text.
Metrics are the trickiest. A metric has a name, a unit, and a type, such as a gauge (a value at one moment), a sum (a running or per-interval total), or a histogram (counts in buckets). Each metric holds data points, and each data point has its own attributes. Sums also carry a temporality, which says whether each point is a running total since the start or only the change since the last point, and a flag that says whether the value can only go up. If your processor removes or changes an attribute on data points, two separate series may become identical and collide, and totals can become wrong or double counted. Any processor that touches metric attributes must think about how series merge.
Errors: what to do when something goes wrong
Errors travel back to whoever sent the data, which may be an application or another Collector. Two kinds exist. A retryable error says “try again later”, and the sender may resend the same batch. A permanent error says “this will never work”, and the sender should give up. Think carefully about which one you mean, because a wrong choice can create a storm of useless retries or lose good data.
For enrichment-style processors, a strong habit is to fail open. If a lookup fails, pass the data on unchanged, count the failure in a metric, and keep going. Blocking the whole pipeline because a side lookup is down would punish every service for a minor problem. For rules where passing data unchanged would be unsafe, such as removing secrets, do the opposite and fail closed: drop or reject rather than leak. Decide per rule, write the decision down, and make it visible to users in the documentation.
If your processor removes every item in a batch, there is nothing left to send. The helper library offers a special “skip” signal for this case, which tells the framework to stop the batch quietly without passing an empty batch downstream and without reporting an error.
| Do | Do not |
|---|---|
| Do expensive work once per resource | Repeat the same lookup for every span |
| Copy values you need before passing data on | Keep references to data after handing it downstream |
| Declare that you change data when you do | Mutate quietly and hope that no one shares the batch |
| Swap in immutable snapshots | Rebuild shared tables in place while readers are using them |
| Fail open for convenience features and closed for safety features | Let a side lookup stop the whole pipeline |
| Respect cancellation signals in the context | Block forever on network calls inside the hot path |
| Check existing labels before overwriting | Overwrite what applications deliberately set, unless asked |
Adding labels is not free. Labels with many different values, such as user IDs, are fine on individual spans but can create an explosion of separate time series if they reach metrics. Before adding a new label, ask which signals will carry it and how many different values it can take.
8Lifecycle: Start, Run, Shutdown
A processor is born, works, and dies. Doing each stage well is what separates a hobby project from production code.
flowchart LR
C["Read configuration"] --> V["Validate settings"]
V --> F["Factory creates the processor"]
F --> S["Start: open files, begin background tasks"]
S --> RUN["Run: handle batches, often in parallel"]
RUN --> SH["Shutdown: stop tasks, flush state, release resources"]
Creation: keep it light
When the factory creates the processor, it should only build the object and store the settings. Do not open files, call services, or start background work here. Creation may happen in situations where the Collector is only checking that a configuration is valid, and heavy work at that moment slows down checks and can cause surprising side effects.
Start: get ready, but never block
The Collector calls Start once all components are created. Here you do the real preparation: load the first copy of your lookup data, open connections, and begin background tasks. Start receives a handle to the “host”, through which you can find extensions, such as a shared credential provider. Two rules apply. First, Start must return promptly. If you must wait for something slow, do it in the background. Second, if the processor cannot work at all, such as a missing required file, return an error and let the Collector refuse to start. Failing at startup is good, because someone is watching at that moment.
Run: the steady state
During the run, batches arrive and background tasks do their periodic work, such as refreshing data. Everything shared between these two worlds, the batch handlers and the background tasks, needs the safe-sharing methods of Chapter 7.
Shutdown: leave no mess behind
The Collector calls Shutdown when it is stopping or rebuilding its pipelines. Your processor must stop its background tasks, close files and connections, and, if it holds data in memory that should be sent, flush it within the allowed time. Shutdown should be safe to call more than once and should respect the deadline carried in its context. A common bug is the leaked background task: a loop that never stops. Every restart then leaves one more loop running, and memory slowly climbs for days.
Starting a shop means unlocking the door and turning on the lights. Closing means telling the last customer, putting away the stock, switching off the lights, and locking the door. If you walk out leaving the oven on, the next morning’s problems are yours.
What about configuration changes?
Collectors are usually restarted or have their pipelines rebuilt when the configuration changes. In that case, old processor instances are shut down and new ones are created. Design for that: keep no important state only in memory unless you can afford to lose it, and make startup and shutdown quick and clean.
The standard generated tests include a basic lifecycle check that creates, starts, and shuts down your component. Keep that test and extend it with a check that no background tasks are left running after shutdown.
9Stateful and Advanced Processors
Some processors remember things between batches. This power is useful, and it brings extra risks.
A stateless processor treats every batch on its own, like a clerk with no memory. A stateful processor keeps information across batches, for example counting how many errors it saw this minute, remembering which traces it has already seen, or holding items until a rule is satisfied. Stateful designs allow smart behavior such as aggregation, de-duplication, rate limiting, and grouping, but they raise hard questions.
| Kind of state | Typical use | Main risk | How to stay safe |
|---|---|---|---|
| Read-mostly lookup table | Enrichment from a catalog | Stale or corrupt data | Atomic swap, age metric, size limit |
| Counters and rate limits | Per-key quotas | Unbounded number of keys | Cap the number of keys, expire idle ones |
| Buffers and timers | Aggregation, grouping | Memory growth, lost data on restart | Strict size and time limits, flush on shutdown |
| Seen-before sets | De-duplication | Memory growth | Time-to-live and a maximum size |
Always bound your memory
Any structure that grows with the data must have two limits: a maximum size and a maximum age. Without them, one unusual day, such as a flood of new label values, will push your Collector out of memory and take down telemetry for everyone. When the limit is reached, decide in advance what gives: drop the oldest, drop the newest, or pass data through unprocessed. Then count each time it happens.
Timers and flushing
Processors that hold items need a timer to release them. Choose the time basis carefully. Using the time the data arrived is simple. Using the time written inside the data is more accurate but must cope with late and out-of-order items. Always flush what you hold when shutting down, within the time allowed.
A small design: a per-key rate limiter
Suppose you want to stop any single service from sending more than a set number of spans per second, so that one noisy service cannot flood the backend. A simple design keeps a small counter for each service name. Each time a batch arrives, the processor looks at how many spans each service sent in the current second, compares that with the limit, and drops the extra spans. The structure behind it is a map from service name to a token bucket, a counter that refills steadily over time. The risks are the ones named above. The map must have a maximum number of entries and an expiry for idle services, or a flood of invented service names will grow it without end. The limit applies per Collector copy, so with five copies the real ceiling is five times higher unless traffic is routed by service name. And every drop must be counted per service, so that the owners can see they are being limited and fix the cause.
Many instances, one truth
In real deployments you run many Collector copies for capacity and safety. Each copy has its own memory, so state is per copy. A counter that says “no more than ten errors per minute” is actually “ten per minute per copy”. If a rule depends on seeing all related data in one place, such as all spans of one trace, you need routing in front so that related data reaches the same copy. The load balancing exporter does this for traces, as used in tail sampling. Without such routing, stateful rules give approximate results at best.
One instance per pipeline
Here is a detail that surprises many builders. If the same processor name appears in two pipelines, the Collector creates two separate instances, one per pipeline. They do not share memory. If both need the same lookup table, each loads its own copy, doubling the work and the calls to the catalog. A common cure is to put the shared part, such as the catalog client and its cache, in an extension, which exists once per Collector. The processors then ask the extension for answers. This design also makes the shared piece easier to reuse across different processors.
Do not try to hide a database inside a processor. A processor that needs large, durable, shared state is often a sign that the work belongs in a proper service, with the processor only asking it questions.
10A Worked Design: The Ownership Enricher
Let us design a complete example from need to behavior, in plain words, so that every earlier idea meets a real case.
The need
An imaginary company, which we will call Northwind (a made-up name), has 2,000 services. A central file lists, for each service, the owning team, the service tier (such as “critical” or “internal”), and a cost center. The file changes daily as teams move and services are born or retired. The company wants every trace, log, and metric to carry team.name, service.tier, and cost.center, so that dashboards, alerts, and cost reports can be grouped by owner. Applications must not need changing.
Can the standard tools do this? Static rules in the transform processor could hold a fixed mapping. But 2,000 entries that change daily, loaded from an external source, are a poor fit for hand-edited configuration. This is a real reason for custom code, under the test from Chapter 4: outside data that changes.
The data flow
flowchart LR
CAT["Ownership catalog file or service"] -->|"reload every few minutes"| LOAD["Background loader"]
LOAD -->|"swap in a new snapshot"| SNAP["In memory lookup table"]
IN["Incoming batch"] --> PROC["Enricher: one lookup per resource"]
SNAP --> PROC
PROC --> OUT["Batch with team and tier labels"]
PROC --> MET["Internal metrics: hits, misses, reload status"]
The settings
| Setting | Purpose | Sensible default |
|---|---|---|
| Catalog source | Where to read the ownership list from | Required |
| Reload interval | How often to refresh | Five minutes, minimum ten seconds |
| Key attribute | Which resource label identifies the service | The service name |
| Labels to add | Which catalog columns become labels | Team, tier, cost center |
| On miss | What to do if the service is not in the catalog | Add “unknown” values |
| Overwrite existing | Whether to replace labels already present | No |
| Maximum entries | Upper limit on the catalog size | 100,000 |
| Fail on first load error | Whether a missing catalog stops startup | Yes |
The behavior, step by step
Start loads the first snapshot
At startup the processor reads the catalog, checks it, and builds the lookup table. If this first load fails and the setting says so, the Collector refuses to start.
A background task refreshes on a timer
Every few minutes it reads the catalog again, validates it fully, builds a new table off to the side, and swaps it in with one atomic step.
Each batch is walked by resource
For every resource, the processor reads the key label, looks it up once, and decides what to add. The spans, log records, or data points below it are not touched at all, because the labels belong to the sender.
Existing labels are respected
If an application already set a team label, the processor keeps it unless the user chose to overwrite. Running the processor twice gives the same result, which makes it safe in complicated pipelines.
Misses are handled by policy
If the service is not in the catalog, the processor adds “unknown” values or leaves data alone, as configured, and counts the miss. A growing miss count is a signal that the catalog is out of date.
Failures never block data
If a refresh fails, the old snapshot keeps serving, a failure counter goes up, and a gauge shows how old the snapshot is. Data keeps flowing.
Shutdown stops the loop cleanly
The timer task is cancelled and the processor waits for it to finish before returning.
The numbers
The catalog has 2,000 services at about 100 bytes each, so the table takes roughly 200 kilobytes of memory, which is tiny. Suppose the pipeline handles 400,000 spans per second and a typical batch has about 100 spans per resource. That means about 4,000 lookups per second. A lookup in an in-memory table takes well under a microsecond, so the total cost is a small fraction of one percent of a single processor core. Looking up once per span instead would be 100 times more calls and many more allocations. The “one lookup per resource” design is what keeps this processor almost free.
Where several processors or pipelines need the same catalog, split the design in two: a small extension that owns the loading, caching, and refreshing, and a thin processor that only asks it questions. Each pipeline’s instance then shares one copy of the data, as suggested in Chapter 9.
11Metadata and Self-Observability
A processor that cannot explain itself is a black box in the middle of your most important pipeline.
The metadata file and generated code
Collector components describe themselves in a file called metadata.yaml. It states the component’s type name, which signals it supports and at what stability, who maintains it, and which internal metrics it produces. A tool named mdatagen reads this file and writes helper code: constants for the type and stability, builders for the internal metrics, and standard tests such as the lifecycle test. Because the code is generated, you keep the description and the code in agreement by changing only the file and running the tool again.
Internal telemetry: telling the truth about your own work
The Collector gives each component a set of tools for its own telemetry: a logger, a meter for metrics, and a tracer. Use them. For our enricher, a good set of internal metrics looks like this. The exact names get a standard prefix from the framework and may vary by version.
| Metric idea | Kind | What it tells you |
|---|---|---|
| Lookups that found an owner | Counter | How much data was enriched |
| Lookups that missed | Counter | Services missing from the catalog |
| Resources without the key label | Counter | Data that cannot be enriched at all |
| Reloads that succeeded or failed | Counter with a result label | Health of the refresh task |
| Age of the current snapshot | Gauge | How stale the answers might be |
| Number of entries in the catalog | Gauge | Sudden drops signal a bad file |
| Time spent per batch | Histogram | Speed of the hot path, if cheap enough to measure |
Logging habits
- Log events, not items. Say “catalog reloaded, 2,014 entries” once per reload. Never log a line for each span, or the logging will cost more than the processing.
- Choose levels carefully. Startup and reload results are informational, repeated failures are warnings, and fatal configuration problems are errors.
- Keep secrets and personal data out of logs. Do not print label values that might be sensitive.
- Limit repeats. If the catalog is down for an hour, one warning per attempt is fine. A warning for every batch is not.
A good elevator has a display showing the floor, a counter of trips, and a service sticker with the last inspection date. You never need to open the doors to know whether it is healthy. Internal metrics are the display and the sticker for your processor.
12Testing Strategy
A processor sits in the path of all your telemetry. Testing it well is not optional.
Before a new car reaches the road, it goes through many different checks: parts tested on a bench, the engine run on a stand, the whole car crash-tested, and a long trial drive. A processor needs the same layers of testing, from small pieces to the whole system.
| Layer | What it checks | Examples for the enricher |
|---|---|---|
| Configuration tests | Valid and invalid settings are accepted or rejected as planned | Reload interval too small, missing source, unknown setting |
| Logic unit tests | The core rule on small hand-made data | Hit, miss, missing key, existing label, overwrite on and off |
| Golden-file tests | Whole input files produce stored expected output files | A mixed batch with many resources compared with a reviewed result |
| Lifecycle tests | Create, start, run, and shut down cleanly | No leftover background tasks after shutdown |
| Concurrency tests | Safe under parallel use, run with the race detector turned on | Many batches while the table is being swapped |
| Failure tests | Behavior when the world misbehaves | Missing file, corrupt file, slow source, empty batch, huge batch |
| Benchmarks | Speed and memory use per batch | Time and allocations on a large realistic batch |
| Pipeline tests | The processor inside a real Collector build, with a real sender and a test receiver of output | Send telemetry, confirm labels on the other side |
| Upgrade tests | Building and passing tests against newer Collector versions | A scheduled job that tries the latest release |
Properties worth asserting
- Idempotence. Running the processor twice gives the same result as running it once.
- Order independence. The result does not depend on the order of items within a batch.
- No loss. Unless a rule says to drop, the count of items out equals the count in.
- No input surprises. Empty batches, missing labels, labels of the wrong type, and extremely long values never crash the processor.
- Bounded resources. Memory stays flat under a long run with changing label values.
A golden-file test in plain words
A golden-file test works like checking homework against an answer key. You keep two files in the test folder: an input file that holds a realistic batch, and an expected-output file that holds exactly what the processor should produce. The test feeds the input through the processor, reads the result, and compares it with the expected file. If they differ, the test fails and prints the difference. When you change behavior on purpose, you review the new output with your own eyes, and only then replace the expected file. This habit catches small, accidental changes that no one would think to write a separate check for.
Keep a small folder of real, anonymized telemetry samples from production, including the strange ones. Messy real data finds bugs that clean hand-made data never will.
13Building Your Own Collector with the Builder
A processor is only a part. To run it, you must assemble a Collector that includes it.
Why you need your own build
The standard Collector programs that you download contain a fixed list of components. Your processor is not in that list, and there is no plug-in folder where you can drop it in at runtime. Instead, you create a custom distribution: a Collector program built from a chosen list of components, including yours. A tool called the OpenTelemetry Collector Builder, often shortened to ocb, does this assembly for you.
The manifest: your shopping list
You give the builder a small YAML file called a manifest. It has a section describing the output and a list for each kind of component.
| Manifest section | What goes in it |
|---|---|
| Distribution details | The name of your Collector, where to write the output, and which Collector version to build against |
| Receivers, processors, exporters, connectors, extensions | One entry per component, each naming the Go module and its exact version |
| Local module paths | For your own processor during development, a path to its folder instead of a published version |
| Replacements | Rules that swap one module for another, such as a patched copy of a dependency |
| Providers | How configuration can be loaded, such as from files, environment variables, or other sources |
flowchart LR
SRC["Your processor module"] --> MAN["Builder manifest listing all components"]
CORE["Official components at pinned versions"] --> MAN
MAN --> OCB["Collector builder tool"]
OCB --> GEN["Generated main module"]
GEN --> BIN["Custom Collector binary"]
BIN --> IMG["Container image"]
IMG --> DEP["Deploy to clusters"]
Build problems you are likely to meet
- Version clash errors. The builder complains that two components need different versions of the same core module. Align every component to one release.
- Missing or stale modules. A local path points to a folder that moved, or an old downloaded copy is being used. Clear the module cache and check the paths in the manifest.
- A component that no longer exists under its old name. Names and locations change between releases. Read the release notes and update the entry.
- A binary that runs on your laptop but not in the cluster. The platform differs, such as a different processor architecture. Build explicitly for the target platform.
- A very large binary and slow starts. Too many components were included. Remove the ones you do not use.
Keep the versions in step
Collector components are released together and are built against the same core version. If your processor is compiled against one version and the rest of the Collector is another, the build may fail, or worse, compile but behave strangely. The safe habit is to pin every component to the same release and update them all together. The builder’s own version should match the version of the Collector that you aim for.
Make the build repeatable
- Run it in continuous integration, not on a laptop, so every build uses the same tools.
- Pin versions exactly. A build today and a build next month from the same manifest should produce the same program.
- Include only what you use. Every extra component adds size, startup time, and attack surface. A lean Collector with twelve components beats a heavy one with two hundred.
- Produce a software bill of materials and sign the result, so that you can later answer “what exactly is running?”.
- Build for each platform you run, such as Linux on both x86 and ARM processors.
While developing, point the manifest at your local folder so that changes show up in the next build. Before releasing, switch to a tagged, published version of your module, so that the released build never depends on files that exist only on one person’s computer.
14Wiring It into a Pipeline
The last step of making a processor useful is placing it in the right spot, with the right neighbors.
Two places in the configuration
A processor appears twice in a Collector configuration file. First, it is declared in a section for processors, with its settings. Then it is listed by name inside one or more pipelines in the service section. Declaring without listing does nothing. Listing without declaring is an error. If you need the same processor with different settings in different places, you give each a short suffix after a slash, which makes them separate named instances.
Order is behavior
Processors run in the order they are listed, so the order is part of your design. A common, sensible order looks like this.
| Position | Kind of processor | Reason |
|---|---|---|
| First | Memory limiter | Protect the Collector before any work is done |
| Early | Resource detection, Kubernetes attributes | Add facts about where data came from |
| Middle | Your custom enricher | Adds owner labels so later steps can use them |
| Middle | Cleaning: redaction, filter, transform | Rules can use the labels added earlier, and secrets are removed before data spreads |
| Later | Sampling processors | Decide with full labels, and after unwanted data is gone |
| Last | Batch processor | Group data for efficient sending |
Think about why each step sits where it does. If you filter by team before the team label exists, the filter finds nothing. If you enrich after sampling, you waste effort on data that was already thrown away, but you also lose the chance to sample by team. Neither choice is always right. What matters is that you choose on purpose and write the reason beside the configuration.
A wiring story
A platform team added the ownership enricher to the gateway’s traces pipeline and tested it. The labels appeared. Then the team tried to filter out data from a retired team, and nothing was filtered. The cause was the order: the filter was listed before the enricher, so at that point the team label did not exist yet, and the rule never matched. Moving the enricher earlier fixed it in a minute. The lesson is that a processor can work perfectly and still look broken, because of its neighbors. When something seems wrong, print the data after each stage and look at where the label first appears.
Checking your work
- Validate before you deploy. The Collector can check a configuration file without running it and report mistakes.
- Look at the output. A debug exporter prints what passes through, so that you can see with your own eyes that the new labels are present.
- Use the Collector’s own metrics. Compare items received and items sent around your processor to confirm that nothing disappears by accident.
- Try it with a copy first. Run a second pipeline with the new processor on a small share of traffic, or on a test environment, before touching production.
Remember that each pipeline that lists your processor gets its own instance. If you list it in the traces, metrics, and logs pipelines, you get three instances, each with its own memory and its own background tasks. Plan for that in your resource limits.
15Performance and Scalability
A processor runs on every batch, all day. A tiny cost per item becomes a large bill when multiplied by millions.
Do the multiplication
Imagine a busy pipeline handling 400,000 spans per second at its peak. If your processor adds a fixed amount of work for each span, here is what it costs in processor cores.
| Extra time per span | Work per second at 400,000 spans | Cores used |
|---|---|---|
| 0.1 microsecond | 0.04 seconds | About 4% of one core |
| 1 microsecond | 0.4 seconds | About 40% of one core |
| 5 microseconds | 2 seconds | 2 cores |
| 50 microseconds | 20 seconds | 20 cores |
A slow rule, such as a poorly written pattern match that takes fifty microseconds, quietly eats twenty cores. This is why small habits matter.
Habits that keep a processor fast
Work at the right level
Do resource-level rules once per resource. In the enricher, that cut the work by a hundred times.
Prepare ahead
Compile patterns and build tables at startup, not inside the loop.
Avoid needless copies
Read values in place, and create new strings or lists only when truly needed.
Avoid waiting
Never call out to the network inside the batch function. Use a cache refreshed in the background.
Share without locking
Use immutable snapshots, so readers never queue behind each other.
Measure, do not guess
Write benchmarks on realistic batches and use profiling tools on a running Collector.
Your processor runs in the sender’s waiting time
In a Collector, the path from receiver through the processors is normally followed in one go, in the call that delivered the data. Until your processor returns, the receiver cannot answer the sender. A slow processor therefore slows the acknowledgement that applications and other Collectors wait for, and when many are waiting, queues build up in front of it. Exporters have their own queues that decouple sending from processing, but the processors in front of them are in the direct path. Keep the work in the hot path short, and move anything slow to the background.
Memory and garbage
The Collector is written in a language that cleans up unused memory automatically, and the cleaning has a cost that grows with the number of objects created. A processor that creates many short-lived objects for every span makes the cleaner work harder and adds pauses. Prefer reusing structures, reading values in place, and building new lists only when needed. Size the memory of each Collector copy with room for peaks, and use the memory limiter together with a soft memory limit for the runtime, as the Collector’s own documentation recommends for your version. Do not invent your own pools and tricks before a measurement proves that they help, because clever memory handling is a rich source of bugs.
Where the processor sits relative to batching
The batch processor groups many small deliveries into big ones. If your processor runs before it, it receives many smaller batches, so any per-call overhead is paid more often. If it runs after it, it receives fewer, larger batches, which spreads fixed costs but increases the peak memory touched at once. For processors with real per-call costs, placing them after batching can help. For processors whose output the batch should see, such as those that drop or merge items, placing them before is better. Measure both with real data.
Scaling out
Stateless processors scale easily: add more Collector copies behind a load balancer. Stateful ones need more thought, as Chapter 9 explained. Whatever the type, keep the CPU and memory limits of each copy realistic, test with peak traffic rather than average traffic, and use the Collector’s memory limiter so that overload turns into polite refusals instead of crashes.
Set a speed budget for your processor in writing, for example “no more than one microsecond per span on average”, and make the benchmark test fail when the budget is exceeded. That turns a vague worry into a guard rail that every code change must respect.
16Security and Privacy
Your processor sees all the telemetry that passes through it. That makes it a trusted insider, and trusted insiders must be careful.
Treat processor code like production code
Telemetry often contains customer identifiers, internal addresses, error messages with fragments of private data, and sometimes secrets that applications leaked by mistake. A bug in your processor, or malicious code hiding in one of its dependencies, could read or send out all of it. So apply the same standards you apply to your most sensitive services: peer review for every change, automated scans of dependencies, and a clear owner on call.
Supply chain care
- Pin and review dependencies. Keep the list small and update deliberately.
- Scan for known vulnerabilities in continuous integration and on a schedule.
- Build from trusted sources and sign the result, so that deployments can verify what they run.
- Avoid surprise network calls. A processor that quietly contacts unknown hosts is a red flag. List and approve every outside address it may reach.
Never trust the incoming labels
Labels on telemetry come from applications, and applications can be wrong or even hostile. Your processor must cope with extremely long values, strange characters, wrong types, and missing fields without crashing or using unlimited memory. There is also a subtle spoofing risk. Suppose cost reports are grouped by the team label. If your enricher keeps labels that applications already set, a careless or dishonest service could claim to belong to another team and shift its costs. For labels that must be authoritative, such as team and cost center, overwrite whatever the application sent, and record the original under a different name if it is useful for debugging.
Protect the data the processor reads
If your processor loads a catalog from a file or a storage bucket, give it read-only access to exactly that one place. Keep credentials out of configuration files and out of logs. Check the catalog’s size and shape before using it, so that a corrupted or oversized file cannot exhaust memory or put nonsense into every span.
Do not leak through your own telemetry
Internal logs and metrics are data too. Do not write label values into logs, and avoid metric labels that hold user-level detail. Sensitive data in the Collector’s own logs is a surprisingly common leak.
Run it safely
Run the Collector as a normal, non-administrator user, with a read-only file system where possible, a minimal base image, and only the network access it needs. These steps limit the damage if something does go wrong.
If your processor is meant to remove secrets, decide up front what happens when it fails. A processor that fails open and lets a secret through is worse than one that fails closed and drops the item. Safety rules should lean toward dropping.
17Deployment and Cloud
A tested binary is not yet a running service. Here is how a custom Collector reaches production without surprises.
Where it runs
Collectors usually run in one or two layers. An agent layer sits close to the applications, as a sidecar next to each workload or as one copy per machine. A gateway layer is a central, scalable group that receives from the agents and sends to the backends. Enrichment from a central catalog is usually best placed in the gateway, because only the gateway needs the catalog, and the agents stay small and simple. Rules that must happen before data leaves a machine, such as removing secrets, belong in the agent.
| Concern | Common approach on a cloud such as AWS |
|---|---|
| Packaging | A container image built in continuous integration and stored in a private registry such as Amazon ECR |
| Running | Kubernetes on Amazon EKS or tasks on Amazon ECS; a gateway as a scalable deployment, an agent as a per-node or sidecar workload |
| Distribution choice | AWS Distro for OpenTelemetry is a ready-made distribution with AWS-focused components. Your own processor requires your own build, which can include the same AWS-related components you need |
| Reading the catalog | A file in Amazon S3, read through a narrowly scoped role given to the workload, checked for changes before each full download |
| Settings | Configuration files stored in version control and delivered as configuration maps; secrets from a secrets service |
| Health checks | A health extension for readiness and liveness checks, so that the platform restarts or removes bad copies |
| Scaling | Scale on CPU and memory, with conservative limits, and an autoscaler that adds copies before the memory limiter starts refusing data |
Configuration, secrets, and change control
Treat the Collector configuration like code. Keep it in version control, review every change, and test it with the validation command before it is shipped. Use environment variable references and secret stores for passwords and tokens, so that no secret ever appears in the repository. Keep one base configuration for all environments and small overrides for differences such as addresses and limits. This way, what you tested in the staging environment is almost exactly what runs in production.
Roll out in small, reversible steps
Test environment
Run the full test suite, then a soak test with realistic traffic for several hours, watching memory and the internal metrics.
One small slice of production
Deploy to one namespace, one cluster, or a few copies of the gateway. Compare counts in and out with the old version.
Widen gradually
Increase the share in steps, leaving a pause between each so problems have time to show.
Keep the rollback ready
The previous image and configuration stay available and tested, and everyone knows who can trigger the rollback.
Stamp the version of your custom Collector into its own internal telemetry. When a dashboard looks odd, being able to ask “which build produced this?” saves hours.
18Advantages, Disadvantages, and Trade-offs
Custom code buys freedom and charges for it. Let us look at both sides honestly.
What you gain
- Rules that standard tools cannot express.
- One central change that covers every service and language.
- Company knowledge, such as ownership, applied automatically.
- Control over speed, memory, and failure behavior.
- No need to touch application code.
What you pay
- You own the code, its tests, and its security forever.
- Every Collector upgrade may need your attention.
- You must build and distribute your own Collector.
- Bugs in a processor can affect all telemetry.
- New teammates must learn one more moving part.
Comparing the ways to get the job done
| Approach | Reach | Flexibility | Upkeep | Best when |
|---|---|---|---|---|
| Change the application | One service | Highest, sees live context | Spread across teams | Few services, or live request data needed |
| SDK processor | Services using that SDK | High | Each language separately | Data must be stopped before leaving the app |
| Standard processors with OTTL | Everything through the Collector | Medium | Low, configuration only | The rule is expressible with built-in functions |
| Custom Collector processor | Everything through the Collector | High | High, real software | Outside data, state, or special logic is needed |
| Rules in the backend | Everything stored there | Depends on the vendor | Low | The backend offers the needed feature, and data volume or privacy is not a concern |
Trade-offs to discuss with your team
- Speed to ship versus long-term cost. Custom code is quick to write and slow to retire. Ask who will own it in two years.
- Central power versus central risk. One processor can fix every service at once, and it can break every service at once too.
- Flexibility versus simplicity. A configurable processor is helpful, and every option is something to test and support.
- Own it versus contribute it. A processor useful to others may be better shared with the community, which brings reviews and maintenance help, at the price of public standards and slower changes.
19Design Patterns and Anti-patterns
Patterns are habits that worked for many builders. Anti-patterns are the traps they fell into.
Patterns that work
Open for convenience, closed for safety
Enrichment passes data on when it fails. Secret removal drops data when it fails.
Snapshot swap
Build a new lookup table fully in the background, then replace the old one in one atomic step.
Work per resource
Do sender-level rules once per sender, not once per item.
Shared helper in an extension
Put shared clients and caches where they exist once, and let processors ask them questions.
Thin component, thick library
Keep the core logic in a plain package that knows nothing about the Collector, so it is easy to test and to reuse.
Small configuration surface
Few settings, strong defaults, early validation, strict rejection of unknown fields.
A metric for every decision
Hit, miss, skip, drop, reload, failure. If the processor decided something, it counted it.
Canary pipeline
A second pipeline carrying a slice of real traffic through the new version before everyone else.
Anti-patterns to avoid
What people do
Call an outside service for every batch, or worse every item, from inside the processing function.
Why it hurts
Each call waits on the network. When the service slows, the whole pipeline slows, queues fill, and data is refused. A small convenience becomes a company-wide telemetry outage.
Do this instead
Refresh a cache in the background and read from memory in the hot path. Use timeouts and keep serving the old data if a refresh fails.
What people do
Keep a map of every label value or key ever seen, with no limit and no expiry.
Why it hurts
One bad day with a flood of new values grows memory until the Collector is restarted by the platform, losing the data it held and everything that arrived meanwhile.
Do this instead
Give every structure a maximum size and a maximum age, decide what yields when the limit is reached, and count each time it does.
What people do
Change the data without declaring that the processor mutates it.
Why it hurts
When a batch is shared between pipelines, one pipeline’s changes leak into another’s, and the bugs appear only in some deployments and only sometimes.
Do this instead
Declare mutation clearly, and add a test with two pipelines fed by one receiver.
What people do
Build one “do everything” processor that enriches, filters, renames, samples, and masks, with dozens of settings.
Why it hurts
It becomes impossible to test, to reason about, or to reorder. Small changes in one feature break another.
Do this instead
Build small processors with one job each, and compose them in the pipeline. Use the standard processors for everything they already do well.
20Best Practices and Common Mistakes
A short list of habits to adopt, and the slips that appear again and again.
Best practices
- Write the rule in one sentence first. If you cannot, the design is not ready. “Add team, tier, and cost center to every resource from the ownership catalog” is a good sentence.
- Try the standard tools first and keep the evidence of why they were not enough.
- Start with one signal. Make traces perfect, then add logs and metrics with their own tests.
- Decide the failure behavior for each rule and write it in the README.
- Bound everything. Sizes, ages, retries, and time spent.
- Make it observable. Metrics for each decision and an age gauge for every cached thing.
- Document for users, not just for yourself. Settings, defaults, examples, order advice, and known limits.
- Own it on purpose. A named team, a place for questions, and a runbook for the night something breaks.
Common mistakes and quick fixes
| Mistake | What you will notice | Fix |
|---|---|---|
| Changing data without declaring it | Odd labels in other pipelines, bugs that come and go | Declare mutation, and test with two pipelines |
| Background task never stops | Memory creeping up after each restart or configuration change | Cancel and wait for the task in shutdown, and test it |
| Per-item work for a per-resource rule | High CPU on busy gateways | Move the rule up to the resource level |
| Component versions out of step | Build errors, or strange behavior at runtime | Pin every component to the same release and upgrade together |
| Adding high-variety labels | Metric bills jump, backends slow down | Limit labels to small, fixed sets, especially on metrics |
| Logging every item | Log volume and cost explode | Log events and counts, not items |
| Testing only clean data | Crashes on the first strange batch | Add real, messy samples and empty and huge batches |
| Renaming settings casually | Teams’ configuration files break after upgrade | Deprecate with a warning, then remove later |
| No stated position in the pipeline | The processor placed in the wrong spot, rules that never match | Document the recommended order and the reason |
Run a short “pre-mortem” before release: imagine the processor has caused an outage, and write down the three most likely reasons. Then add a test or a metric that would catch each one.
21Keeping It Alive: Upgrades, Versions, and Contributing Upstream
Writing the processor is the first month of its life. Maintaining it is the next several years.
The Collector keeps moving
The Collector is released often, and many of its internal programming interfaces are still allowed to change between releases, especially those not yet marked stable. Your processor depends on those interfaces. A release that renames a helper, changes a function’s inputs, or retires an old way of doing things can stop your build until you adapt. This is normal for a fast-growing project, and it means that upgrades must be a routine habit, not a rare emergency.
An upgrade routine that works
Watch the release notes
Read the change notes for each release, looking for anything marked as breaking or deprecated in the parts you use.
Build against the newest release automatically
A scheduled job tries the latest version and reports failures. You learn about breakage while it is still small.
Upgrade in small steps
Moving one release at a time is far easier than jumping across a dozen, because each break is fresh and small.
Run the whole test ladder
Configuration, logic, lifecycle, concurrency, failure, benchmark, and pipeline tests all run before the new build is released.
Roll out gradually
Use the same canary steps as for any other change.
Version your own work
Give your processor its own version numbers with a clear meaning: a small number change for fixes, a middle change for new features that keep old configuration working, and a big change when settings or behavior change in ways that can break users. Keep a short change log. Users will thank you.
Retiring a custom processor
Good news sometimes arrives: the standard tools grow and one day cover your need. When that happens, switching back to a standard component removes your maintenance cost entirely. Keep an eye out for such changes, and plan the move with the same gradual steps as any rollout. A custom processor that you can delete is a success, not a failure.
Contributing upstream
If your processor solves a problem that many companies share, consider offering it to the community repository. The usual path is to describe the idea in a public issue first, find a maintainer willing to sponsor it, follow the project’s component template and documentation rules, provide tests, name the people who will maintain it, and start at a low stability level that rises as real use grows. Contributing brings reviews and shared maintenance. It also means your design will be shaped by others, and the pace follows the community’s schedule. Processes change from time to time, so read the contribution guide of the repository before you start.
Even if you never contribute, build as if you will. Follow the same layout, naming, tests, and documentation style as the official components. It makes your own maintenance easier and keeps the door open.
22Real-World and Industry Examples
The patterns in this guide appear in components that you can read today, plus one clearly labeled made-up story.
Kubernetes attributes processor: lookup and cache, in the open
This widely used component in the community repository adds details such as pod, namespace, and workload names to telemetry. It does not ask the cluster about every span. It watches the cluster for changes, keeps a cache of the answers, and matches incoming data to the cache. That is the same shape as our enricher: a background refresh, a fast in-memory lookup, and enrichment at the sender level.
Tail sampling processor: stateful design with hard limits
This component holds spans in memory until it has seen enough of each trace to decide. It has a maximum number of traces, a waiting time, and a cache of past decisions for late data. It also needs all spans of a trace to arrive at the same copy, which is why it is paired with a load balancing exporter in front. It is a good study of the limits, timers, and routing concerns from Chapter 9.
Redaction processor: safety that leans toward removal
This component works from an allow list of labels and masks values that match patterns. The allow-list approach follows the “closed for safety” idea: whatever is not clearly permitted is not passed on as it is. It is a good model for any processor whose job is protection.
Vendor and cloud distributions: curated custom builds
The AWS Distro for OpenTelemetry, the Splunk distribution of the Collector, Elastic’s distribution, and Grafana’s Alloy all package selected OpenTelemetry components, often with extra components of their own. They are an everyday proof that the custom build approach works at scale, and they show the benefits: a tested, supported list of components that does not carry everything.
An SDK-side comparison (made up for teaching)
Imagine an online learning company wanting each span to carry the subscription level of the logged-in student. That fact lives only in the live request, inside the application, and is gone by the time data reaches a Collector. The right tool here is an SDK span processor in each service’s language that reads the level at the moment the span starts. A Collector processor could not know it. This shows why the choice in Chapter 2 depends on where the information lives.
Northwind and the ownership enricher (made up for teaching)
Northwind, the imaginary company from Chapter 10, adopted the enricher. Before, only a minority of telemetry carried an owner label, because applications set it inconsistently, and cost reports needed manual spreadsheets. After the change, nearly all telemetry carried correct owner labels from a single catalog, the catalog could change daily without any redeploy, and a growing miss counter told the platform team which new services had not yet been added to the catalog. Treat the story as a picture of the shape of the benefit, not a promise of results.
23Advanced Topic: Dropping, Grouping, and Aggregating
Some processors do more than add labels. They shrink, reshape, or combine data, and each of these powers brings its own care.
Dropping
Dropping is the simplest way to reduce cost, and the easiest way to hurt yourself. A processor that drops must be able to explain every drop. Count dropped items by reason, never drop silently, and keep a switch that turns the dropping off during an investigation. If a rule can drop an entire batch, remember the special “skip” signal from Chapter 7, so that nothing empty travels onward. When unsure whether data is safe to drop, pass it on. It is cheaper to keep a little too much than to lose the one trace you needed.
Grouping
Grouping moves items into new boxes by a shared label. For example, spans that arrive in boxes by sending instance may need to be regrouped under a box for their service. The standard library has a processor that does this by attributes. When you write your own, remember the nested layout from Chapter 3: items live inside scopes inside resources, and moving an item to a new resource means building that resource’s box first. Grouping increases the number of boxes, which can slow downstream processors that work once per box, so measure the effect.
Aggregating
Aggregating combines many items into fewer summary items. A common case is turning many metric data points into one point per minute. Others include counting spans by name and status over a window. Aggregation saves money, but it asks hard questions. What is the window, and which clock decides it? What happens to late items that arrive after the window closed? What happens to a half-filled window when the Collector shuts down? Which attributes are kept, and which are removed, and will series collide when they are merged? How are the types of metrics combined? A sum can be added, but a gauge cannot be added meaningfully, and histograms must have matching buckets. Each question needs an answer in the documentation and a test.
Aggregating is like a teacher turning a stack of daily attendance sheets into a single weekly report. It saves space, but the teacher must decide how to count a student who arrived after the sheet was collected, and what to do with a sheet that was never handed in. A processor must settle the same kinds of questions before it can be trusted.
Check the standard library first
Many common reshaping jobs already have standard processors, including regrouping by attributes, converting running totals to per-interval changes, and transforming metrics by rule. Aggregation and conversion logic is subtle, and well-used components have already met the strange cases. Write your own only for the parts that are truly specific to your company.
Every power in this chapter makes data in the backend differ from data at the source. Tell users clearly, in the processor’s documentation and in dashboards, which numbers were dropped, grouped, or aggregated, so that nobody mistakes a summary for the raw truth.
24Frequently Asked Questions
Short answers to the questions people ask most when they start.
25Summary and Key Takeaways
Key Takeaways
- Check the standard tools first. Attributes, resource, filter, transform, and redaction processors with OTTL cover a lot. Build only when you need outside data, state, or special logic.
- Know which “processor” you mean. Collector processors work centrally on finished data. SDK processors work inside applications on live context.
- A processor is a small Go module. A factory, a configuration with validation, the processing function, metadata, and tests, built into your own Collector with the builder tool.
- Respect the data rules. Declare mutation, never touch data after passing it on, expect parallel calls, and do per-resource work at the resource level.
- Never block the pipeline. Refresh in the background, serve from memory, bound every cache, and choose between fail-open and fail-closed on purpose.
- Test, measure, and watch. Layered tests, speed budgets, and internal metrics for every decision keep a central component trustworthy.
- Plan for years, not days. Pin and upgrade versions together, secure the supply chain, roll out gradually, and be glad on the day you can retire your code in favor of a standard one.