How Do You Build a Custom Processor in OpenTelemetry?

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.

Analogy

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

Reason 1

Enrichment from private data

Adding labels that come from your own systems, such as team, cost center, or customer tier.

Reason 2

Company-specific cleaning

Removing or masking values that only your company knows are sensitive, in your own formats.

Reason 3

Special routing or limiting

Applying your own budget or fairness rules that no general tool offers.

Reason 4

Format repair

Fixing telemetry from old systems whose names or units do not match your standards.

Reason 5

Aggregation

Combining many small items into fewer summary items to save cost.

Reason 6

Integration with internal services

Looking something up in an internal catalog, with caching, so that data carries more meaning.

i
Tip

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.

QuestionCollector processorSDK span processor
Where does it run?In the separate Collector programInside each application
What language?GoThe application’s own language
What does it see?Batches from many services at onceSpans of one application, one at a time
When does it run?After data leaves the applicationWhen a span starts and ends
Can one change cover all services?YesNo, each application must be updated
Can it see the live request context?No, only the finished dataYes, such as the current user or request details held in memory
Cost to the applicationNone, apart from sending dataUses the application’s CPU and memory
Analogy

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

Component

Receiver

The front door. It accepts data from applications or other systems, in formats such as OTLP.

Component

Processor

The middle stages. They change, filter, or hold data. This is what we are building.

Component

Exporter

The back door. It sends data to its final home, such as a tracing backend or storage.

Component

Connector

A bridge between two pipelines. It acts as an exporter for one and a receiver for another, for example turning spans into metrics.

Component

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"]
    
Figure 1: A traces pipeline. Receivers feed processors in a fixed order, and exporters send the result onward.

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"]
    
Figure 2: The nested layout of trace data. Logs and metrics have the same idea with their own inner items.

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.

i
Tip

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

NeedReady-made optionWhat it can do
Add, change, delete, or hash labels by simple rulesAttributes processorSet fixed values, copy between labels, delete, hash, and extract
Change labels that describe the senderResource processorEdit resource-level labels
Drop items that match conditionsFilter processorRemove spans, logs, or metrics by conditions written in a small expression language
Rewrite almost anything with flexible rulesTransform processorUses the OpenTelemetry Transformation Language (OTTL) to change names, labels, and values with conditions and functions
Mask sensitive valuesRedaction processorAllow-list labels and mask values that match patterns
Add Kubernetes detailsKubernetes attributes processorLooks up pod, namespace, and workload details and caches them
Detect where the Collector runsResource detection processorAdds cloud and host details
Keep a portion of tracesProbabilistic and tail sampling processorsSample by percentage or by whole-trace rules
Protect memory, group for sendingMemory limiter, batch processorSafety 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"]
    
Figure 3: A decision path that checks the cheaper options before writing a custom component.
!
Watch out

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.

Analogy

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 folderJobPlain-words purpose
go.modDeclares the module and its dependenciesThe list of parts you borrow, with exact versions
factory fileCreates the processor and its default settingsThe front desk that builds a new processor when the Collector asks
config fileDefines the settings people can put in the configurationThe order form for your processor
processor fileHolds the real logicThe cook doing the work
metadata.yamlDescribes the component for tooling: name, stability, signals, internal metricsThe notice board
generated metadata folderCode created from metadata.yaml by a tool called mdatagenAuto-written helper code you do not edit by hand
test filesUnit, golden-file, and benchmark testsThe proof that it works
READMEExplains settings, behavior, and limitsThe 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.

i
Tip

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 designWeak setting design
Reload interval as a duration with a default of five minutesA number called “time” with no unit
On-miss behavior chosen from a short list: leave unchanged, use default, or dropA 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 namedDozens 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.

!
Watch out

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.

Analogy

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.

DoDo not
Do expensive work once per resourceRepeat the same lookup for every span
Copy values you need before passing data onKeep references to data after handing it downstream
Declare that you change data when you doMutate quietly and hope that no one shares the batch
Swap in immutable snapshotsRebuild shared tables in place while readers are using them
Fail open for convenience features and closed for safety featuresLet a side lookup stop the whole pipeline
Respect cancellation signals in the contextBlock forever on network calls inside the hot path
Check existing labels before overwritingOverwrite what applications deliberately set, unless asked
!
Watch out

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"]
    
Figure 4: The life of a processor, from configuration to shutdown.

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.

Analogy

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.

i
Tip

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 stateTypical useMain riskHow to stay safe
Read-mostly lookup tableEnrichment from a catalogStale or corrupt dataAtomic swap, age metric, size limit
Counters and rate limitsPer-key quotasUnbounded number of keysCap the number of keys, expire idle ones
Buffers and timersAggregation, groupingMemory growth, lost data on restartStrict size and time limits, flush on shutdown
Seen-before setsDe-duplicationMemory growthTime-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.

!
Watch out

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"]
    
Figure 5: The enricher reads from an in-memory snapshot that a background loader refreshes. Batches never wait on the catalog.

The settings

SettingPurposeSensible default
Catalog sourceWhere to read the ownership list fromRequired
Reload intervalHow often to refreshFive minutes, minimum ten seconds
Key attributeWhich resource label identifies the serviceThe service name
Labels to addWhich catalog columns become labelsTeam, tier, cost center
On missWhat to do if the service is not in the catalogAdd “unknown” values
Overwrite existingWhether to replace labels already presentNo
Maximum entriesUpper limit on the catalog size100,000
Fail on first load errorWhether a missing catalog stops startupYes

The behavior, step by step

1

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.

2

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.

3

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.

4

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.

5

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.

6

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.

7

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.

i
Tip

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 ideaKindWhat it tells you
Lookups that found an ownerCounterHow much data was enriched
Lookups that missedCounterServices missing from the catalog
Resources without the key labelCounterData that cannot be enriched at all
Reloads that succeeded or failedCounter with a result labelHealth of the refresh task
Age of the current snapshotGaugeHow stale the answers might be
Number of entries in the catalogGaugeSudden drops signal a bad file
Time spent per batchHistogramSpeed 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.
Analogy

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.

Analogy

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.

LayerWhat it checksExamples for the enricher
Configuration testsValid and invalid settings are accepted or rejected as plannedReload interval too small, missing source, unknown setting
Logic unit testsThe core rule on small hand-made dataHit, miss, missing key, existing label, overwrite on and off
Golden-file testsWhole input files produce stored expected output filesA mixed batch with many resources compared with a reviewed result
Lifecycle testsCreate, start, run, and shut down cleanlyNo leftover background tasks after shutdown
Concurrency testsSafe under parallel use, run with the race detector turned onMany batches while the table is being swapped
Failure testsBehavior when the world misbehavesMissing file, corrupt file, slow source, empty batch, huge batch
BenchmarksSpeed and memory use per batchTime and allocations on a large realistic batch
Pipeline testsThe processor inside a real Collector build, with a real sender and a test receiver of outputSend telemetry, confirm labels on the other side
Upgrade testsBuilding and passing tests against newer Collector versionsA 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.

i
Tip

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 sectionWhat goes in it
Distribution detailsThe name of your Collector, where to write the output, and which Collector version to build against
Receivers, processors, exporters, connectors, extensionsOne entry per component, each naming the Go module and its exact version
Local module pathsFor your own processor during development, a path to its folder instead of a published version
ReplacementsRules that swap one module for another, such as a patched copy of a dependency
ProvidersHow 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"]
    
Figure 6: From source code to a running custom Collector. The manifest is the single list that controls what the final program contains.

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.
i
Tip

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.

PositionKind of processorReason
FirstMemory limiterProtect the Collector before any work is done
EarlyResource detection, Kubernetes attributesAdd facts about where data came from
MiddleYour custom enricherAdds owner labels so later steps can use them
MiddleCleaning: redaction, filter, transformRules can use the labels added earlier, and secrets are removed before data spreads
LaterSampling processorsDecide with full labels, and after unwanted data is gone
LastBatch processorGroup 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.
!
Watch out

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 spanWork per second at 400,000 spansCores used
0.1 microsecond0.04 secondsAbout 4% of one core
1 microsecond0.4 secondsAbout 40% of one core
5 microseconds2 seconds2 cores
50 microseconds20 seconds20 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

Habit

Work at the right level

Do resource-level rules once per resource. In the enricher, that cut the work by a hundred times.

Habit

Prepare ahead

Compile patterns and build tables at startup, not inside the loop.

Habit

Avoid needless copies

Read values in place, and create new strings or lists only when truly needed.

Habit

Avoid waiting

Never call out to the network inside the batch function. Use a cache refreshed in the background.

Habit

Share without locking

Use immutable snapshots, so readers never queue behind each other.

Habit

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.

i
Tip

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.

!
Watch out

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.

ConcernCommon approach on a cloud such as AWS
PackagingA container image built in continuous integration and stored in a private registry such as Amazon ECR
RunningKubernetes on Amazon EKS or tasks on Amazon ECS; a gateway as a scalable deployment, an agent as a per-node or sidecar workload
Distribution choiceAWS 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 catalogA file in Amazon S3, read through a narrowly scoped role given to the workload, checked for changes before each full download
SettingsConfiguration files stored in version control and delivered as configuration maps; secrets from a secrets service
Health checksA health extension for readiness and liveness checks, so that the platform restarts or removes bad copies
ScalingScale 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

1

Test environment

Run the full test suite, then a soak test with realistic traffic for several hours, watching memory and the internal metrics.

2

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.

3

Widen gradually

Increase the share in steps, leaving a pause between each so problems have time to show.

4

Keep the rollback ready

The previous image and configuration stay available and tested, and everyone knows who can trigger the rollback.

i
Tip

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

ApproachReachFlexibilityUpkeepBest when
Change the applicationOne serviceHighest, sees live contextSpread across teamsFew services, or live request data needed
SDK processorServices using that SDKHighEach language separatelyData must be stopped before leaving the app
Standard processors with OTTLEverything through the CollectorMediumLow, configuration onlyThe rule is expressible with built-in functions
Custom Collector processorEverything through the CollectorHighHigh, real softwareOutside data, state, or special logic is needed
Rules in the backendEverything stored thereDepends on the vendorLowThe 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

Pattern

Open for convenience, closed for safety

Enrichment passes data on when it fails. Secret removal drops data when it fails.

Pattern

Snapshot swap

Build a new lookup table fully in the background, then replace the old one in one atomic step.

Pattern

Work per resource

Do sender-level rules once per sender, not once per item.

Pattern

Shared helper in an extension

Put shared clients and caches where they exist once, and let processors ask them questions.

Pattern

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.

Pattern

Small configuration surface

Few settings, strong defaults, early validation, strict rejection of unknown fields.

Pattern

A metric for every decision

Hit, miss, skip, drop, reload, failure. If the processor decided something, it counted it.

Pattern

Canary pipeline

A second pipeline carrying a slice of real traffic through the new version before everyone else.

Anti-patterns to avoid

ANTI-PATTERN 01Avoid
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.

ANTI-PATTERN 02Avoid
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.

ANTI-PATTERN 03Avoid
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.

ANTI-PATTERN 04Avoid
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

MistakeWhat you will noticeFix
Changing data without declaring itOdd labels in other pipelines, bugs that come and goDeclare mutation, and test with two pipelines
Background task never stopsMemory creeping up after each restart or configuration changeCancel and wait for the task in shutdown, and test it
Per-item work for a per-resource ruleHigh CPU on busy gatewaysMove the rule up to the resource level
Component versions out of stepBuild errors, or strange behavior at runtimePin every component to the same release and upgrade together
Adding high-variety labelsMetric bills jump, backends slow downLimit labels to small, fixed sets, especially on metrics
Logging every itemLog volume and cost explodeLog events and counts, not items
Testing only clean dataCrashes on the first strange batchAdd real, messy samples and empty and huge batches
Renaming settings casuallyTeams’ configuration files break after upgradeDeprecate with a warning, then remove later
No stated position in the pipelineThe processor placed in the wrong spot, rules that never matchDocument the recommended order and the reason
i
Tip

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

1

Watch the release notes

Read the change notes for each release, looking for anything marked as breaking or deprecated in the parts you use.

2

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.

3

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.

4

Run the whole test ladder

Configuration, logic, lifecycle, concurrency, failure, benchmark, and pipeline tests all run before the new build is released.

5

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.

i
Tip

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.

Analogy

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.

!
Watch out

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.

Q1Do I have to write Go?
Collector components are written in Go. If your team does not use Go, you have options: try the transformation language first, write an SDK processor in your application language, or run a separate service in any language that the Collector sends data to and receives data from. That last option adds a network hop, so weigh the cost.
Q2Can I change a processor’s behavior without restarting?
Changes to settings normally mean the Collector rebuilds its pipelines or restarts. Data your processor loads itself, such as a catalog, can be refreshed live, as in the enricher. Put frequently changing facts in data, and keep settings for things that change rarely.
Q3How do I see what my processor is doing?
Use the debug exporter to print what leaves the pipeline, read the processor’s own metrics and logs, and keep unit tests that capture batches before and after. All three together answer almost every question.
Q4Can a processor turn one kind of telemetry into another?
That is the job of a connector, such as the one that makes metrics from spans. A processor changes data of the same kind that passes through it.
Q5Is it safe to call an outside service from a processor?
Only in the background, with timeouts, caching, and a plan for when the service is down. Never make the hot path wait on the network.
Q6My new label does not appear in the backend. Why?
Check these in order: is the processor listed in the right pipeline and in the right place, does the key label exist on the data at that point, did a later step remove or overwrite it, did you add it at the resource level while the backend looks at span level, and does the backend limit or ignore unknown labels?
Q7Should I support traces, metrics, and logs at once?
Only if the rule makes sense for all of them, and only after each is tested separately. Starting with one signal reduces risk.
Q8Which Go and Collector versions should I use?
Use the versions documented for the Collector release you target, and pin them. Read that release’s notes, because supported versions and interfaces change over time.
Q9When should I contribute the processor to the community?
When the problem is common, the design is not tied to your company, you can commit people to maintain it, and you can live with the community’s pace and standards. Otherwise keep it internal.
Q10How do I justify the effort to my manager?
Compare the cost of the alternatives, such as changing hundreds of applications or living without owner labels, with the build and upkeep cost. Show a small prototype, ideally after proving that standard tools cannot do the job, and name the team that will own it.

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.