How Do You Build a Custom Receiver in OpenTelemetry?
A custom receiver teaches the OpenTelemetry Collector to listen to a data source it does not yet understand. This guide walks through the ideas, the building blocks, the design choices and the traps, from first plan to production release.
Imagine a translator who works at a busy international airport. Announcements arrive in many languages: some spoken over loudspeakers, some printed on boards, some handed over on paper slips. The airport has a standard way of recording every announcement in one common form, so that staff anywhere can read it. When a new airline arrives that uses a language nobody knows, the airport hires a new translator whose only job is to listen to that airline and write down its announcements in the common form. A custom receiver is that translator. It listens to a source the Collector does not yet understand, and it turns what it hears into the common language of OpenTelemetry.
1Why Custom Receivers Exist
Before building one, it helps to understand when building one is the right answer.
The world is full of formats
Organisations produce telemetry in countless shapes. A legacy application writes status lines to a file. A hardware device exposes numbers through a proprietary protocol. A database publishes statistics through a special query. A message queue carries events in a company-specific layout. A vendor product offers a web interface that returns readings in its own structure. None of these speak the standard OpenTelemetry protocol, and none of them will change just because you would like them to.
The OpenTelemetry Collector already ships with a large family of receivers that cover popular sources, and these should always be your first stop. But no collection of ready-made receivers can cover every system in every company. When the source you care about is unusual, private, or too new, you have two honest choices. You can modify the source so that it speaks a known protocol, which is often impossible. Or you can write a receiver that bridges the gap.
Think of travel adapters for electrical plugs. Most countries are covered by a handful of standard adapters you can buy in a shop. If you visit a country with an unusual socket, you either rewire the wall, which is rarely possible, or you build a small adapter. A custom receiver is a small adapter between a strange socket and the standard plug.
Reasons that justify the effort
Proprietary Sources
Internal systems with private formats that no public receiver will ever support.
Legacy Systems
Old software that cannot be changed but still holds valuable operating data.
Hardware and Devices
Equipment that exposes readings through specialised protocols or interfaces.
Business Metrics
Numbers held in business systems, such as order counts or queue lengths, that teams want beside technical telemetry.
Before writing anything, search the list of existing receivers and the contributed components. A receiver that already does ninety percent of the job is better to extend or configure than a new one to maintain for years.
The cost of owning a receiver
Writing the first version is the cheap part. A receiver is software that must be kept alive for years. The source will release new versions that change its output. The Collector libraries will change their programming interfaces. Security problems will be found in dependencies. Users will ask questions and report problems. Every receiver you create is a promise to do this work, so it is worth asking whether the benefit justifies the promise.
A useful test is to imagine the receiver three years from now. Who will maintain it? Who will know how it works when its author has moved to another team? Which tests will tell them they have broken something? If you cannot answer these questions, invest in documentation and tests before adding more features, or consider whether another approach might serve better.
| Option | Effort now | Effort over time | Fits when |
|---|---|---|---|
| Use an existing receiver as is | Very low | Very low | A public receiver already covers your source |
| Change the source to speak a standard protocol | Medium to high | Low | You own the source and can modify it |
| Add a small translation layer outside the Collector | Medium | Medium | The format is simple and you prefer not to maintain Collector code |
| Build a custom receiver | Medium to high | Medium to high | The source is unusual, cannot be changed, and matters enough |
2Core Concepts: The Collector in Plain Words
A receiver is only one piece of a larger machine, so we should see the whole machine first.
The OpenTelemetry Collector is a program that receives telemetry, processes it, and sends it onwards. It is assembled from components of four main kinds. Receivers bring data in. Processors change data on its way through, for example by adding labels or dropping unwanted items. Exporters send data out to another destination. Connectors join one pipeline to another. Extensions provide helpful services that sit beside the pipelines, such as authentication, health checks and storage.
These components are wired together in pipelines, and each pipeline handles one kind of signal: traces, metrics or logs. A receiver feeds the start of a pipeline, processors act in the middle, and exporters finish it. The configuration file describes which components exist and how the pipelines connect them.
flowchart LR
S1[Your data source] --> R[Custom receiver]
S2[Other sources] --> R2[Existing receivers]
R --> P[Processors]
R2 --> P
P --> E[Exporters]
E --> D[Storage or vendor]
Receiver
The component that brings telemetry into the Collector, either by listening or by fetching it.
Signal
A kind of telemetry: traces, metrics or logs. A receiver may support one or several.
Pipeline
A chain of receivers, processors and exporters for one signal.
Consumer
The next step that accepts data from a receiver. A receiver hands its data to a consumer and does not care what happens afterwards.
Factory
The object that knows how to create a receiver and its default settings. Every component type has one.
pdata
The Collector’s internal data model for telemetry, used to build traces, metrics and logs in memory.
The contract in one sentence
A receiver’s whole contract can be stated simply: it obtains data from somewhere, converts it into the internal data model, and passes it to the next consumer in the pipeline. Everything else in this guide is detail about doing those three things well.
Where data goes after the receiver
It helps to picture what happens after your receiver hands over its data. The next consumer may be a processor that adds labels, groups items into batches, limits memory use or filters unwanted data. Later steps may send the data to several destinations at once. Your receiver does not know or control any of this. That separation is deliberate: it lets the same receiver feed very different pipelines in different companies.
It also means your receiver must not make assumptions about what follows. It should not, for example, assume that a batching step exists, or that a particular label will be added later. It produces complete, self-describing data and trusts the rest of the pipeline to do its own job.
Receivers, connectors and extensions: how they differ
Newcomers sometimes confuse the building blocks. A receiver is the entry point from the outside world. A connector sits between two pipelines and acts as an exporter at the end of one and a receiver at the start of the next; it is used for tasks such as turning traces into metrics. An extension does not touch the data at all; it provides services such as authentication or health checks. If your task is to bring new outside data in, you need a receiver. If your task is to transform data already inside, you need a processor or a connector.
Receiver
Brings data in from the outside world into a pipeline.
Processor
Changes, filters or groups data already inside a pipeline.
Exporter
Sends data from a pipeline to an outside destination.
Connector
Joins two pipelines, often converting one kind of signal into another.
Extension
Provides side services such as authentication, storage and health checks.
3Two Families: Push Receivers and Pull Receivers
The first design decision is whether your receiver waits for data or goes to fetch it.
Push receivers
A push receiver opens a door and waits. Applications or devices send data to it whenever they have something to say. The receiver listens on a network port, a file, a queue or some other channel, and handles each arrival as it comes. The timing is controlled by the senders, so the receiver must be ready for bursts and quiet spells alike.
Pull receivers
A pull receiver, sometimes called a scraper, goes out on a schedule and fetches data. Every ten seconds or every minute it asks the source for its current readings, converts the answer, and passes it on. The timing is controlled by the receiver, which makes the load predictable and easy to reason about.
| Question | Push receiver | Pull receiver |
|---|---|---|
| Who decides when data moves? | The source | The receiver, by schedule |
| Typical sources | Applications that emit events, agents, devices that send readings | Databases, status pages, management interfaces, files read at intervals |
| Main design worry | Bursts, bad senders, backpressure | Slow sources, timeouts, repeated readings |
| Common signals | Traces and logs | Metrics |
| Helper support | Server settings for network listeners | Scraper helpers that run schedules for you |
A push receiver is like a letterbox: letters arrive whenever the postman comes, and you must be ready for a big pile after a holiday. A pull receiver is like checking your letterbox yourself every morning: you decide when to look, and the pile is whatever has built up.
Let the source decide. If the source can send data by itself, a push receiver is natural. If the source only answers when asked, build a pull receiver. Some sources can do both, and then the choice depends on which is easier to run safely.
Some receivers combine both families. For example, one might listen for event notifications and also poll for a periodic summary. Such combinations are possible, but they double the number of behaviours to test, so it is usually cleaner to build two separate receivers that share a library for parsing.
Hybrid sources and tricky cases
Real sources are rarely tidy. A file that grows over time is neither purely pushed nor purely pulled: the receiver watches the file and reads new lines as they appear, remembering how far it has read. A queue behaves like a push source from the receiver’s point of view, but the receiver actually asks the queue for messages and must confirm each one after handling it. A web interface that offers both a summary page and a stream of events invites two separate receivers.
For sources that must remember a position, such as a file offset or a queue position, think about what should happen when the Collector restarts. If the position is lost, the receiver may re-read old data and create duplicates, or skip data that arrived while it was down. The Collector offers storage extensions that let a receiver keep such positions across restarts, and using them is usually better than inventing a private file.
Decide what promise your receiver makes: at most once, meaning data may be lost but is never duplicated, or at least once, meaning data is never lost but may be duplicated. Neither is wrong, but users must be told which one they get, because it affects how they build alerts and reports on top of the data.
| Source style | Family | Special concern |
|---|---|---|
| Growing log file | Watcher, behaves like push | Remembering read position and handling rotation |
| Message queue | Consumer, behaves like push | Acknowledging messages only after safe handoff |
| Status page | Pull | Parsing a format that may change between versions |
| Event stream over a network | Push or long-lived pull | Reconnecting after drops without duplicating data |
4Architecture: The Parts of a Receiver
Every receiver, however different its source, is built from the same small set of parts.
Receivers for the Collector are written in the Go programming language, because the Collector itself is written in Go. A receiver is a small Go package that provides a few well-defined pieces, and the Collector assembles them at start-up. Exact names of types and functions change between versions of the Collector libraries, so always confirm them against the version you build with. The ideas, however, remain stable.
Type Name
A short, unique name that identifies the receiver in configuration files, for example the word you write on the line that declares it.
Configuration Structure
A structure describing the settings a user may supply, such as addresses, credentials, intervals and options.
Default Configuration
A function that returns sensible starting values, so a user needs to write only what differs.
Factory
The object that ties the name, the configuration and the creating functions together, and declares which signals are supported and how stable they are.
Receiver Instance
The running object, with a start action and a shutdown action, that does the real work of obtaining and converting data.
Next Consumer
The handle through which the receiver passes finished data into the pipeline.
flowchart TD
U[User configuration file] --> F[Factory]
F --> C[Default configuration merged with user settings]
C --> V[Validation]
V --> R[Receiver instance created]
R --> S[Start]
S --> W[Obtain and convert data]
W --> N[Pass to next consumer]
S -.-> X[Shutdown when Collector stops]
Metadata describes the receiver
Most mature receivers also carry a small metadata file that describes what they are: their name, their stability level, the metrics they produce, and the attributes those metrics carry. A code generator reads this file and produces repetitive code, documentation and tests. Using it keeps the receiver consistent with the rest of the ecosystem and removes a large amount of tedious writing.
The factory is like a shop’s order form. The customer fills in only the lines they care about, the shop supplies standard values for the rest, checks the form for mistakes, and then builds the product. The product, the receiver, is handed over only when the form passes inspection.
5Planning Before You Write
Most failed receivers fail in the planning, not in the code.
Study the source properly
Start by learning everything about how the source exposes data. Does it send or must it be asked? What formats does it use? How often does it change? Is there a limit on how many requests it can serve? What happens when it is unavailable? Does it require a login, and how does that login expire? Each of these questions will shape a decision in the receiver, and discovering the answers after the code is written is expensive.
Decide what the data means
Next, decide how source data maps to OpenTelemetry concepts. A reading that goes up and down, such as current temperature, maps to one kind of metric. A count that only increases, such as total requests served, maps to another. A record of something that happened at a moment in time maps to a log. A request that travels through several steps maps to a trace made of spans. Choosing the right concept is the most important modelling choice, because it decides how backends will draw and store the data.
| Source behaviour | Best-fitting concept | Reason |
|---|---|---|
| A value that rises and falls, such as a queue length | Gauge metric | It describes the current level |
| A total that only grows, such as bytes sent | Sum metric, monotonic | Backends can compute rates from it |
| A spread of measurements, such as response times | Histogram metric | It keeps the shape of the distribution |
| A single event with details | Log record | It carries text and attributes at one moment |
| A journey across steps with timing | Trace of spans | It shows parent and child relationships and durations |
Follow shared naming conventions
OpenTelemetry publishes semantic conventions, which are agreed names for common attributes and metrics, such as the names for a host, a service or a database system. Using them means that dashboards and alerts written for other sources will work for yours as well. Invent new names only for things that are truly unique to your source, and prefix them clearly so they cannot clash with future standard names.
List the source’s data
Write down every reading, event and relationship the source can provide, and note how each is obtained.
Choose what to collect
Select the subset that people will really use. Collecting everything makes the receiver heavy and the output noisy.
Map each item to a concept
Decide whether each item is a gauge, a sum, a histogram, a log or a span, and record the reasoning.
Name things using conventions
Reuse standard names and units wherever they exist, and document any new ones.
Decide push or pull
Select the family of receiver that suits the source and fix the schedule or listening method.
Write down the contract
Before coding, write a short document that states what the receiver will do. It should list the data it will collect, the names, types and units of each item, the settings it will accept, the failure behaviour it promises and the versions of the source it supports. This contract is useful in three ways. It forces you to think through gaps. It lets reviewers challenge decisions before they are expensive to change. And it becomes the seed of the user documentation.
Know the source’s limits
Every source has limits that a receiver must respect. A database may slow down if queried too often. A web interface may block clients that send too many requests. A device may accept only one connection at a time. Learn these limits during planning and design the receiver so that it stays well inside them by default. A receiver that causes trouble for its source will be switched off, however clever it is.
Ask the source’s owners
Find out how much extra load is acceptable and which times are sensitive.
Set safe defaults
Choose intervals, concurrency and request sizes that stay well below the limits.
Make limits visible in documentation
Explain which settings affect load on the source and what values are risky.
Supported Versions
Sources change over time, and their output formats may differ between versions. State which versions you have tested, detect the version at run time where possible, and fail clearly for versions you do not support instead of producing misleading data.
6The Toolchain: What You Need Installed and Why
A small number of tools make the work smooth, and each has a clear job.
The Go Language
Receivers are Go packages. You need a recent Go toolchain, of a version supported by the Collector libraries you choose.
The Collector Builder
A tool, often called ocb, that assembles a custom Collector distribution from a manifest listing the components you want, including your own.
The Metadata Generator
A code generator that reads a metadata file and produces the boilerplate for metrics, attributes, configuration and documentation.
Test Helpers
Libraries that give you ready-made fake consumers, fake hosts and settings, so you can test a receiver without running a whole Collector.
How the builder fits in
A standard Collector binary contains only the components someone chose to include. Your custom receiver will not be inside any downloaded binary, so you need to build your own. The builder reads a short manifest, which lists the Collector version, the output name, and the components to include by their module paths. For your own receiver, the manifest can point at a local folder during development and at a published module later. The builder then generates and compiles a complete Collector that contains your receiver beside the standard ones.
flowchart LR
M[Builder manifest] --> B[Collector builder]
Y[Your receiver module] --> B
O[Other chosen components] --> B
B --> G[Generated project]
G --> K[Compiled custom Collector]
A sensible project layout
Keep the receiver in its own module with a clear folder structure: one place for the factory and configuration, one for the receiver logic, one for the translation code that converts source data, and one for tests and sample data. Keeping translation separate from transport is especially valuable. It lets you test the conversion with plain sample files, without any network or timing involved, and it lets you reuse the conversion if you later add a second way of reaching the same source.
The Collector libraries are released together and evolve quickly. Pin the versions in your module, upgrade them on purpose, and read the release notes. Mixing versions of different Collector libraries in one project is a common source of confusing build errors.
Working locally and iterating quickly
A comfortable development routine saves hours. Keep a small local Collector configuration that uses your receiver and sends its output to a simple destination that prints data to the screen or a file. Rebuild with the builder after each change, run it against a test instance of the source, and look at the output directly. Seeing real output after every small change catches modelling mistakes long before they reach a dashboard.
Use the Collector’s own debugging exporter, which prints received data in readable form, to inspect what your receiver produces. Check that resource attributes are where you expect, that units appear, and that timestamps are sensible. When something looks wrong, this view is far quicker than reading code.
Small Test Configuration
A minimal file with your receiver and a printing destination, so you see output immediately.
Test Instance of the Source
A local or containerised copy of the source with safe sample data, so mistakes harm nothing.
Rebuild and Run
Use the builder after each change and run the generated Collector with the test configuration.
Inspect Output
Read what the receiver produced, checking names, units, attributes and timestamps.
Read the source of a few existing receivers of the same family before writing yours. Seeing how mature receivers organise their folders, settings and tests gives you a pattern to follow and a list of problems they had to solve.
7Designing the Configuration
The configuration is the face your receiver shows to its users, so it deserves care.
Make the common case easy
A good receiver works with almost no settings. The address of the source and perhaps a credential should be enough to get useful data flowing. Everything else should have a sensible default. Users judge a receiver by how quickly they get their first result, and a long list of required settings is the fastest way to lose them.
Reuse standard setting groups
The Collector provides ready-made groups of settings for common needs: network listening, encrypted connections, client connections, timeouts, retries and authentication. Embedding these groups in your configuration means your receiver behaves exactly like others, and users already know how to fill them in. It also saves you from writing and testing the same logic again, and from the subtle errors that come with it.
Network Server Settings
For push receivers that listen on a port, including address, encryption and limits on message size.
Client Settings
For pull receivers that connect out to a source, including endpoint, encryption, headers and timeouts.
Schedule Settings
For scrapers, covering how often to collect, how long to wait before the first collection, and how long each collection may take.
Authentication Settings
Links to authentication extensions, so credentials and checks are handled in one trusted place.
Validate early and clearly
A receiver can provide a validation step that runs when the Collector starts, before any data flows. Use it to reject impossible settings: a missing address, a negative interval, two options that cannot be used together. The error message should say which setting is wrong and what is expected. A message such as “invalid configuration” wastes time. A message that names the field and the rule saves a support request.
| Setting type | Good practice | Poor practice |
|---|---|---|
| Intervals | Default to a reasonable value and reject zero or negative values | Require the user to guess a number |
| Credentials | Allow reference to secrets from files or environment variables | Encourage typing secrets into the file |
| Endpoints | Validate format and give a clear error | Accept anything and fail later |
| Optional features | Off by default, documented, with clear names | On by default, causing surprising load |
Once users depend on a setting name, renaming or removing it breaks their deployments. Treat configuration as a public interface, and follow a deprecation period before any breaking change.
8The Factory and the Lifecycle
A receiver has a clear life: it is created, started, runs for a long time, and is shut down.
Creation
The factory declares which signals the receiver supports and how stable each is. For each supported signal it provides a function that creates a receiver given the settings and the next consumer. A single receiver may support several signals, in which case the Collector may create a shared instance when the same receiver is used in several pipelines, so that two pipelines do not open the same port twice. When you design your receiver, think about this sharing, and make sure your code behaves correctly when several consumers are attached to one running receiver.
Start
The start action is called once when the Collector begins. Its job is to begin work and return promptly. It must not block forever, because the Collector waits for it before starting other components. Long-running work, such as listening or polling, should be launched in the background, and start should return as soon as that background work is ready.
The start action receives a context and a host. The host gives access to extensions, which is how a receiver finds its authentication or storage helpers. A common and important rule is that the context given to start is meant for the start action itself and should not be kept for long-running work, because it may be cancelled once start returns. Background work should create its own context that is cancelled by the shutdown action.
Shutdown
The shutdown action is called when the Collector stops. It must stop the background work, close connections, release resources and wait for in-flight handling to finish, within the time allowed. A receiver that ignores shutdown leaves threads running, ports open and data half-processed, which is disastrous during rolling upgrades.
Create
The factory builds the receiver from validated settings and the next consumer.
Start
The receiver opens its listener or launches its schedule, then returns quickly.
Run
Data is obtained, converted and passed on, for as long as the Collector runs.
Shut down
The receiver stops accepting work, finishes what is in progress, and releases everything it holds.
Starting a background loop inside start, but forgetting to stop it in shutdown. The Collector appears to stop, yet the process lingers or leaks resources, and the problem only shows up in production restarts.
9Translating Source Data into the OpenTelemetry Model
This is the heart of the receiver, and the place where quality is won or lost.
The shape of the internal model
The internal model groups telemetry in layers. At the top is a resource, which describes the thing that produced the data, such as a host, a service or a device. Inside a resource is a scope, which describes the library or component that produced the telemetry. Inside the scope are the actual items: metric points, log records or spans. Each item carries attributes, which are labels that give it context.
flowchart TD
RES[Resource: who produced this] --> SC[Scope: which component produced it]
SC --> M[Metrics with data points]
SC --> L[Log records]
SC --> T[Spans]
M --> A1[Attributes]
L --> A2[Attributes]
T --> A3[Attributes]
Put identity at the resource level
Facts that stay the same for everything a source produces, such as the name of a device, the cluster it belongs to or the version of its software, belong on the resource. Putting them on each data point wastes memory and makes data harder to group. Facts that vary from point to point, such as which disk or which queue a reading refers to, belong in attributes of the data point.
Metric details that are easy to get wrong
Units
Record the unit of every metric, using agreed unit names. A value of “5” is meaningless without knowing whether it means seconds, bytes or percent.
Timestamps
Every data point carries a time. Use the time the reading was taken, not the time it was converted, whenever the source provides it.
Start Time for Totals
Cumulative sums need a start time that marks when counting began, so backends can detect resets and compute rates correctly.
Temporality
A sum can report running totals or changes since the last report. Pick the one that matches what the source gives you, and state it.
Do not turn unbounded values, such as user identifiers or request paths with random parts, into metric attributes. Each distinct value creates a new series, and backends become slow and expensive. Keep attribute values to a small, bounded set.
Logs and traces
For logs, fill the body with the message, set the severity from the source’s level, and use the source’s own time as the timestamp. Keep the original text intact so nothing is lost, and add parsed fields as attributes. For traces, the source must provide identifiers that link steps into a tree. Without reliable parent and child links, spans form a pile of separate dots instead of a story. If your source cannot provide this, a receiver for traces is the wrong tool, and logs or metrics are the honest choice.
Choosing metric types in practice
Mistakes in choosing metric types are the most common modelling error. Consider a source that reports “requests handled” as a number that grows forever. If you report it as a gauge, a backend will draw a line that rises endlessly and cannot tell a restart from a drop. If you report it as a monotonic cumulative sum, the backend can compute a rate per second and detect resets. The same number, reported with the wrong type, gives misleading charts.
Now consider a source that reports “current connections”. This rises and falls, so a gauge is correct. Reporting it as a sum would suggest that connections accumulate, which is not what the number means. A good habit is to ask: if I added up two readings taken a minute apart, would the result make sense? If yes, it is probably a sum. If no, it is probably a gauge.
| Source says | Question to ask | Type |
|---|---|---|
| Total bytes sent since start | Does it only grow, apart from restarts? | Cumulative monotonic sum |
| Current temperature | Does it move up and down? | Gauge |
| Items waiting in a queue | Does adding two readings make sense? | Gauge, not a sum |
| Response time distribution | Does the spread matter, not just the average? | Histogram |
| Items processed in the last interval | Is it a change since the last report? | Delta sum |
Handling missing and partial data
Sources sometimes omit fields, return placeholder values or report that a reading is not available. Do not turn missing data into zero. A zero is a real reading that means “nothing”. A missing value means “unknown”, and the honest response is to leave that data point out. Replacing unknown with zero creates false calm in charts and can trigger or hide alerts incorrectly.
Treat missing values as absent, never as zero. A chart that shows a gap is telling the truth. A chart that shows zero is making something up.
Time zones and clocks
Sources may report times in local time zones, in different formats or with clocks that drift. Convert every timestamp into a single standard form, and be cautious when the source’s clock differs noticeably from the Collector’s. If you cannot trust the source’s time, use the time of collection and document that choice.
10Building a Pull Receiver (Scraper)
Pull receivers are the simplest to run safely, and the Collector helps you build them.
The Collector provides helper libraries for scrapers. You supply a scraper whose single job is to collect one round of readings from the source and return them in the internal model. The helper takes care of the schedule: it runs your scraper at the configured interval, enforces a time limit, applies an optional initial delay, and passes the results to the consumer. This division leaves you with only the part that is unique to your source.
sequenceDiagram
participant H as Scraper helper
participant S as Your scraper
participant X as Source system
participant C as Next consumer
loop Every collection interval
H->>S: Collect now, with a time limit
S->>X: Ask for current readings
X-->>S: Readings
S->>S: Convert to metrics
S-->>H: Metrics
H->>C: Pass metrics on
end
Rules for a well-behaved scraper
Respect the time limit
If the source is slow, give up when the time is up and report the failure. Never let one slow round block the next.
Be gentle on the source
Ask for what you need in as few requests as possible, since a scraper that overloads a fragile source causes the very problems it was meant to detect.
Return partial results
If one of ten readings fails, return the nine that worked and report the one failure, rather than discarding everything.
Reuse connections
Open a connection once in start and reuse it between rounds, instead of reconnecting every time.
Make each round independent
Do not depend on hidden state from the previous round, except where a calculation such as a rate genuinely requires it.
Choosing the interval
The collection interval trades freshness against cost. A short interval gives more detail but loads both the source and the pipeline. A long interval is cheap but can miss short events. Pick a default that suits most users, such as a value in the range of tens of seconds to a minute, allow users to change it, and avoid intervals shorter than the source can sensibly answer.
Derived Values
If the source reports a running total but users want a rate, report the total as a cumulative sum and let the backend compute the rate. Computing rates inside the receiver forces you to remember earlier readings, and it breaks when the Collector restarts.
11Building a Push Receiver (Listener)
Push receivers meet the outside world directly, so they need more defensive thinking.
Opening the door
A push receiver starts a listener in its start action. For network protocols, use the ready-made server settings for the common web protocols, which handle addresses, encryption, size limits and authentication in a consistent way. For other channels, such as files, queues or special devices, use the appropriate client library and follow the same pattern: set up in start, run in the background, clean up in shutdown.
Handling each arrival
When data arrives, the receiver decodes it, converts it to the internal model, and hands it to the consumer. Decoding must be careful. Never trust the format of incoming data. Check that fields exist, that numbers are in range, that text is a sensible size, and that nesting is not absurdly deep. A single malformed message must never crash the receiver or corrupt other data.
Size Limits
Cap the size of each message so a sender cannot exhaust memory with one enormous request.
Concurrency Limits
Limit the number of simultaneous requests, so a burst cannot overwhelm the Collector.
Timeouts
Close connections that stay idle or send data too slowly, to stop slow senders from holding resources.
Authentication
Require senders to prove who they are, using the Collector’s authentication extensions rather than home-made checks.
Backpressure: telling senders to slow down
If the pipeline cannot keep up, the consumer returns an error or blocks. A well-behaved push receiver passes this message back to the sender, for example by returning a response that asks the sender to retry later. A receiver that silently drops data, or that buffers without limit, hides the problem and makes it worse. Honest signalling lets senders slow down, queue, or switch to a backup.
| Situation | Good response | Poor response |
|---|---|---|
| Pipeline is slow | Tell the sender to retry later | Buffer without limit until memory runs out |
| Message is malformed | Reject it with a clear reason and carry on | Crash, or accept it and produce nonsense |
| Sender is unknown | Refuse using the authentication extension | Accept everything from anywhere |
| Message is huge | Refuse it against a configured limit | Try to load it all into memory |
Reading data in streams
Some sources send very large messages or continuous streams. Reading such data a piece at a time, instead of all at once, keeps memory use steady. A decoder that reads one record, converts it, and moves on needs only enough memory for one record at a time, whatever the total size. This approach also lets the receiver start passing data on before the whole message has arrived, which reduces delay.
Batching what you pass on
Passing data to the consumer one item at a time is wasteful, because every handoff has fixed overhead. Group related items into reasonably sized batches before passing them on, for example all the records from one incoming message. Do not wait too long to build a batch, though, since delaying data hurts freshness. Often the best choice is to pass on whatever arrived together, and leave extra batching to a dedicated processor later in the pipeline.
One Message, One Batch
Convert everything in an incoming message into one group and pass it on together.
Do Not Hold Data Back
Avoid long waits to build bigger batches inside the receiver. Let a batch processor handle that.
Stream Large Inputs
Decode large inputs record by record to keep memory use flat.
Acknowledge After Handoff
Confirm receipt to the sender only once data has been safely passed to the consumer.
Partial acceptance
Sometimes part of a message is valid and part is not. A thoughtful receiver passes on the valid part and reports that some items were rejected, with a count, instead of refusing everything. Some protocols include a way of telling the sender exactly how many items were rejected, so the sender can decide what to do. Use such features where they exist, since they make failures understandable.
flowchart TD
A[Message arrives] --> B[Decode records]
B --> C{Record valid?}
C -->|Yes| D[Convert and add to batch]
C -->|No| E[Count and log rejection]
D --> F[Pass batch to consumer]
E --> F
F --> G[Report accepted and rejected counts to sender]
12Error Handling and Passing Data On
What a receiver does when something goes wrong matters as much as what it does when all goes well.
What the consumer can tell you
When a receiver passes data to the next consumer, the answer can be success or an error. The Collector distinguishes errors that are permanent, meaning that retrying will never help, from errors that are temporary, meaning that retrying later may work. A permanent error might be data the pipeline rejects as invalid. A temporary error might be a downstream component that is briefly busy. Your receiver should pass this distinction on: tell senders to retry for temporary problems, and report permanent ones as failures that should not be retried.
Errors from the source
Errors that come from the source need equally careful handling. Distinguish a source that is unreachable from one that returned something unreadable, and from one that refused your credentials. Each calls for a different response. An unreachable source suggests waiting and trying again at the next interval. An unreadable answer suggests a bug or a version change that needs attention. A refused login suggests a configuration problem that retrying will not fix.
Source Unreachable
Log at a suitable level, record the failure in your own metrics, and try again at the next interval without crashing.
Unreadable Data
Skip the bad item, count it, and log enough detail to diagnose it without dumping sensitive content.
Login Refused
Report a clear configuration error. Repeating the attempt quickly can lock the account.
Downstream Busy
Respect the signal. Retry for temporary problems, give up on permanent ones, and tell senders honestly.
An error that repeats every ten seconds can flood the logs. Log the first occurrence clearly, then reduce repetition, and rely on metrics for counting. A quiet log with accurate counters is more useful than a loud one nobody reads.
Retrying with care
When a pull receiver fails to reach its source, it can simply wait for the next interval. But when a receiver must keep a connection alive, for example a stream of events, it needs a retry policy. A good policy waits a short time after the first failure, then longer after each further failure, up to a maximum, and adds a little random variation so that many receivers do not retry at exactly the same moment. Without this, a source that has just recovered may be knocked over again by a crowd of simultaneous reconnections.
Fail and note it
Record the failure in measurements and write one clear log line.
Wait a short time
Pause briefly before the first retry, so brief glitches pass without drama.
Wait longer each time
Increase the pause after each failure up to a sensible maximum, with a little random variation.
Reset on success
Once a connection works, return to the short pause so the next problem is handled quickly again.
Protecting the source and the Collector
A receiver should never retry in a tight loop. Rapid retries can overload the source, fill the logs, and consume processor time in the Collector. They can also cause account lockouts when the problem is a refused login. Always put a floor under the time between attempts, and give users a setting to adjust it.
The best failure handling is almost invisible: a clear message at the start, steady counters, quiet retries and a smooth return to normal. Drama in the logs usually means the design has gaps.
13Making the Receiver Observable Itself
A receiver that cannot report on itself is a black box in the middle of your monitoring system.
The Collector offers helper tools that record standard figures about each receiver: how much data was accepted and how much was refused, for each signal. Using these helpers gives your receiver the same internal measurements as every other receiver, and these measurements appear in the Collector’s own metrics. Operators already know these figures, so they can tell at a glance whether your receiver is healthy.
| Measurement | What it tells an operator |
|---|---|
| Data accepted | How much the receiver successfully passed into the pipeline |
| Data refused | How much the pipeline or the receiver turned away, hinting at overload or bad data |
| Collection failures | How often scrapes failed, and for which reason |
| Duration of collections | Whether the source is slowing down |
| Connection state | Whether the receiver can currently reach its source |
Add measurements that are specific to your source
Beyond the standard figures, consider a few that reflect the source: the number of items parsed, the number of items skipped, the age of the newest reading, and the version of the source detected. These help diagnose the problems that are unique to your receiver. Keep them few and bounded, for the same reason you keep attributes bounded in the data you produce.
Logging that helps
Use the logger provided by the Collector rather than printing directly, so that log levels, formats and destinations follow the user’s configuration. Include context such as the receiver’s name and the source’s address, but never secrets such as passwords or tokens. At debug level, add more detail for engineers who are diagnosing a problem.
The most dangerous receiver is one that fails quietly and keeps reporting nothing. A dashboard that shows flat zero looks the same as a healthy quiet system. Always provide a signal that distinguishes “no data because nothing happened” from “no data because collection is broken”.
Health and readiness
The Collector can expose a health signal that platforms use to decide whether to send traffic or restart a process. A receiver that cannot reach its source for a long time should make this visible, but should be careful not to cause pointless restarts. If the source is down, restarting the Collector will not bring it back. Reporting the problem through measurements and logs, while the Collector continues to serve other pipelines, is usually the right response.
Dashboards for your receiver
Provide an example dashboard or at least a list of recommended charts. Typical panels show data accepted per second, data refused per second, collection duration, collection failures by reason, and the age of the newest reading. Include a panel that compares the receiver’s view with the source’s own view where possible, such as the number of items the source says it has against the number the receiver passed on. A gap between them reveals lost data.
Throughput
Items accepted and refused per second, per signal.
Failures
Collection failures by reason, such as unreachable, unreadable and refused.
Freshness
Age of the newest reading, so stale data is obvious.
Source Cross-check
The source’s own counts beside the receiver’s, to expose silent loss.
Alerts Worth Having
Alert when collection has failed for several intervals in a row, when the age of the newest reading exceeds a threshold, and when refused data stays above zero for a sustained period. These three alerts catch the great majority of receiver problems.
14Testing Strategy
A receiver runs unattended for months, so testing must be thorough.
Translation Tests
Feed sample source data into the conversion code and compare the output with expected telemetry. These tests are fast and need no network.
Configuration Tests
Check that defaults are right, that valid files load, and that invalid ones are rejected with clear messages.
Lifecycle Tests
Create, start and shut down the receiver using fake hosts and a recording consumer, checking that it stops cleanly and leaves nothing behind.
Integration Tests
Run against a real or realistic instance of the source, often in a container, to verify the whole path from source to consumer.
Golden files
A popular technique is the golden file. You save the expected output of a conversion in a file. Future runs compare their output with the saved file, and any difference fails the test. This makes accidental changes to names, units or attributes visible at once. When a change is intended, you review the difference and update the file deliberately.
Hostile and unusual input
Test what happens with empty input, enormous input, truncated input, wrong types, missing fields and unexpected characters. Test slow sources, sources that disconnect midway, and sources that return old data. Receivers meet all of these in production sooner or later.
Collect real samples
Save genuine responses and messages from the source, with sensitive details removed, to use as test material.
Write translation tests first
They are quick and give confidence in the core of the receiver.
Add lifecycle and failure tests
Check clean shutdown, slow sources and broken connections.
Run an integration test in a container
Confirm that the receiver works against the real thing, including different versions of the source.
Measure performance
Benchmark conversion and check memory use under realistic and extreme load.
Testing across versions and environments
Sources change, so a receiver tested against one version may fail against another. Where practical, run integration tests against several versions of the source, at least the oldest and newest you claim to support. Containers make this affordable, since each version can be started in seconds and discarded afterwards. Keep the list of tested versions in the documentation, and update it as part of each release.
Also test on the platforms your users run. A receiver that works on one operating system may behave differently on another, especially around file paths, line endings, time handling and network defaults. If your users run on several platforms, run your tests on all of them in your automated build.
Testing the unhappy paths on purpose
Many receivers are tested only when everything goes right. Add tests where the source returns an error, where it returns nothing, where it returns half a message, where it closes the connection, and where it responds extremely slowly. Check that the receiver reports each situation correctly and recovers once the source is healthy again. These tests are the cheapest insurance against production surprises.
| Unhappy path | What the test should confirm |
|---|---|
| Source returns an error status | The failure is counted and logged, and the next round still runs |
| Source returns empty data | No data is invented, and the receiver reports an empty round |
| Source returns truncated data | Valid parts are kept and invalid parts are counted |
| Connection drops midway | The receiver reconnects with backoff and does not duplicate data unexpectedly |
| Source is extremely slow | The time limit applies and the receiver does not block later rounds |
| Consumer refuses data | The refusal is passed back or counted, and memory does not grow |
15Security, Privacy and Governance
A receiver has access to credentials, networks and potentially sensitive data, so it carries real responsibility.
Handling credentials
The receiver may need a password, a key or a token to reach its source. Never write these into logs or error messages. Allow users to supply them through files or environment variables, and use the Collector’s types for sensitive text, which hide the value when settings are printed. Request the least access the receiver needs. A read-only account is far safer than an administrator account.
Encrypted connections
Where the source supports encryption, support it too, and make it easy to switch on. Use the standard encryption settings from the Collector so that users configure certificates the same way as with other components. Do not disable verification of certificates for convenience, even in examples, because examples are copied into real deployments.
Sensitive data in telemetry
Source data may contain personal details, secrets or internal addresses. Decide in advance which fields the receiver should pass on, which it should drop, and which it should leave for processors to handle. Offering a setting to exclude certain fields gives users control. Document clearly what the receiver collects, so that privacy and security reviews can proceed without reading the code.
Whatever your receiver collects by default is what most users will collect. Default to the minimum useful set, and make broader or more sensitive collection an explicit choice.
Supply Chain Care
Your receiver depends on libraries, and users will run your code inside a central piece of their infrastructure. Keep dependencies few, update them regularly, scan them for known problems, and publish releases in a way that lets users verify what they downloaded.
16Packaging, Distribution and Stability
Writing the receiver is only half the job. People must be able to use it, upgrade it and trust it.
Delivering it in a Collector
Most teams use the builder to produce their own Collector distribution that contains the receiver together with the other components they need. The result is a single program, which can be packaged as a container image or a standard installation package, and deployed in the same way as any other Collector. Keep the manifest in version control, so that anyone can rebuild the same distribution later.
Versioning and stability
The Collector uses stability levels to tell users how much to rely on a component: development, alpha, beta and stable. Declare an honest level for each signal your receiver supports. A new receiver should usually begin at a low level and rise as it gains users, tests and a settled configuration. Version your own module with a clear scheme, and describe changes in release notes, especially anything that alters names, units or settings.
| Stability level | What users should expect |
|---|---|
| Development | Anything may change, and the component may disappear |
| Alpha | Usable for trials, with settings and output likely to change |
| Beta | Mostly settled, with changes announced and uncommon |
| Stable | Changes that break users are avoided and follow a long notice period |
Contributing upstream
If your source is popular, consider offering the receiver to the community’s contributed components. The project has guidelines on structure, documentation, tests and ownership. Contributing brings reviews, shared maintenance and users, but also obligations: someone must answer questions and keep the receiver working as the Collector evolves. If the source is private, keep the receiver in your own repository, and still follow the same conventions, so that it can be shared later without rewriting.
Write the documentation as you build: what the receiver collects, every setting with its default, example configurations and known limits. Documentation written afterwards is usually thinner and often wrong.
Release routine and upgrades
A dependable release routine builds trust. Each release should be built automatically from tagged source, tested on all supported platforms, and published with notes that explain what changed. Users read these notes to decide when to upgrade, so highlight anything that alters settings, names, units or behaviour. Where a change would break existing users, provide a transition period in which both the old and new forms work and the old form is reported as deprecated.
Keep pace with the Collector itself. New releases of the Collector libraries bring new features, fixes and sometimes changes to programming interfaces. Plan regular upgrades, rather than letting your receiver fall many versions behind, because the longer you wait the harder the jump becomes. Automated checks that build and test against the latest Collector libraries reveal breakage early.
Tag a release
Mark a specific version of the source so that anyone can rebuild exactly the same code.
Build and test automatically
Run all tests on every supported platform before publishing anything.
Publish with clear notes
Describe changes, especially those that affect settings, names, units or behaviour.
Track upstream Collector releases
Upgrade the libraries on a regular schedule and fix breakage while it is small.
Renaming a metric, changing its unit or altering an attribute silently breaks every dashboard and alert built on it. Announce such changes, support both forms for a period, and give users time to adapt.
17Performance and Resource Use
A receiver shares a process with every other component, so its appetite affects everyone.
Memory
Memory is the most common problem. Avoid loading entire large inputs at once when they can be processed piece by piece. Release references to data once it has been passed on. Be careful with caches and maps that grow with the number of distinct items seen, because they can grow without limit over weeks of running. Bound every cache, and decide what happens when the bound is reached.
Processor time
Conversion code runs for every item, so even small inefficiencies add up. Avoid repeating work that can be done once, such as building lookup tables. Avoid unnecessary copying of text. Measure before optimising, using benchmarks and profiling tools, because guesses about speed are often wrong.
Concurrency
Push receivers handle many arrivals at once, and the code that converts and forwards them must be safe when run in parallel. Shared data needs protection, and per-request data should stay private to each request. Test with many simultaneous senders, and use the language’s race-detection tools to catch mistakes that ordinary tests miss.
Collectors usually include a memory limiter that refuses data when memory is high. Make sure your receiver reacts properly when its consumer refuses data, so the protection works as intended rather than being undermined by your own buffering.
Finding problems with measurement
Go provides profiling tools that show where time and memory are spent. Run your receiver under a realistic load, collect a profile, and read it. You will often find surprises: a conversion step that copies text repeatedly, a map that grows without limit, or a lock that many threads fight over. Fixing the biggest item on the list usually gives most of the improvement, and later rounds give smaller gains.
Test with the largest realistic input and with a sustained load over many hours. Many memory problems appear only after a long run, as small leaks accumulate. Watch the receiver’s memory use over a day under steady load. A line that climbs slowly and never falls is a leak, even if each hour looks harmless.
CPU Profile
Shows which functions use the most processor time during conversion and handling.
Memory Profile
Shows what holds memory and where allocations come from.
Long Soak Test
Runs for many hours under steady load to expose slow leaks.
Race Detector
Reports unsafe sharing of data between concurrent tasks.
Decide how much memory and processor time the receiver may use at a given load, write it down, and check it in automated tests. A written budget turns vague worries about performance into a clear pass or fail.
18Design Patterns and Anti-patterns
Some choices work again and again, and some mistakes repeat just as often.
Helpful patterns
Pattern: Separate Transport from Translation
Keep the code that obtains data apart from the code that converts it. Conversion can then be tested with plain sample files and reused for other transports.
Pattern: Reuse Standard Settings and Helpers
Use the Collector’s ready-made groups for servers, clients, schedules and authentication, and its helper libraries for scrapers and for internal measurements. Your receiver will behave like every other and need less testing.
Pattern: Metadata-driven Definitions
Describe metrics and attributes in a metadata file and generate code and documentation from it. This keeps names, units and documents consistent.
Pattern: Fail Visibly and Recover Quietly
Report failures through measurements and logs, then carry on at the next interval. The receiver should never crash the Collector because of a problem in its source.
Harmful anti-patterns
Problem
Starting background work in the start action without a way to stop it.
Why It’s Harmful
Shutdown leaves threads and connections behind, which causes leaks, stuck restarts and data loss during upgrades.
Correct Approach
Create a cancellable context for background work, and have shutdown cancel it and wait for the work to finish.
Problem
Turning unbounded values, such as identifiers or full request paths, into metric attributes.
Why It’s Harmful
Series multiply without limit, so storage costs rise and queries slow down.
Correct Approach
Use small, bounded sets of attribute values. Put detailed values in logs or traces instead.
Problem
Swallowing errors and returning empty results when the source fails.
Why It’s Harmful
Dashboards show a flat line that looks like a healthy quiet system, so real outages go unnoticed.
Correct Approach
Record failures in measurements, log them with suitable detail, and make a broken collection distinguishable from silence.
Problem
Inventing private names and units for data that standard conventions already describe.
Why It’s Harmful
Dashboards and alerts written for standard names do not work, and every user must write extra mapping.
Correct Approach
Adopt the published semantic conventions, and add custom names only where nothing standard exists.
19Best Practices and Common Mistakes
A checklist in plain language, drawn from everything above.
Check for an existing receiver first
Configuring or extending something that exists is cheaper than owning something new.
Plan the data model before the code
Decide what to collect, how to map it and what to name it, using the standard conventions.
Use the helpers the Collector provides
Settings groups, scraper helpers, test tools and the metadata generator all reduce effort and error.
Keep start quick and shutdown complete
Start returns promptly, and shutdown releases everything the receiver holds.
Treat input as hostile
Validate, limit and time-box everything that arrives from outside.
Make the receiver observable
Record accepted, refused and failed counts, and make broken collection visible.
Test at several levels and document as you go
Translation, configuration, lifecycle and integration tests, plus a written guide, make the receiver safe to adopt.
The usual errors are skipping the planning, ignoring shutdown, producing high-cardinality attributes, swallowing source errors, trusting incoming data, and forgetting that the Collector libraries change between versions. Every one has caused real outages, usually discovered long after the receiver was first deployed.
A sensible order of work
Build the translation code first and test it with samples. Then add the simplest possible transport, and verify with a recording consumer. Then add configuration and validation. Then add measurements and logging. Then harden against bad input and slow sources. Finally, package, document and release. Following this order gives you something working early and lets each layer be tested before the next is added.
20Real-World Scenarios
A few typical stories show how the ideas fit together.
Reading a Legacy Application’s Status Page
A company runs an old application that publishes a plain status page of counters. The team builds a pull receiver that fetches the page every thirty seconds, converts each counter into a cumulative sum or a gauge, and sets the application’s name and version as resource attributes. Because the page is simple, most of the effort goes into choosing correct metric types and testing with saved samples.
Receiving Events from a Proprietary Device
A manufacturer’s devices send binary messages over a network connection. The team builds a push receiver that listens on a port, decodes each message defensively, and turns it into log records with the device identifier on the resource. Size limits, timeouts and authentication protect the Collector from faulty devices, and refusals are signalled so devices can retry later.
Exposing Business Numbers
A retail team wants order counts and queue lengths from a business system beside technical metrics. A pull receiver queries a reporting interface once a minute, using a read-only account, and converts the answers into gauges. Bounded attributes such as region and channel keep cardinality low.
Bridging a Message Queue
Events from an internal queue are consumed by a receiver that converts them into logs and trace spans. The team keeps conversion in a separate package, so when the queue technology changes, only the transport part is rewritten.
What the scenarios share
In each case, the team studied the source before coding, chose the family that suited it, separated translation from transport, built in protection and measurements, and tested with real samples. Where things went wrong in other teams, one of these steps had been skipped.
A Team That Skipped Planning
A team built a receiver in a week, mapping every source counter to a gauge because it was quickest. Months later, users complained that rate charts were wrong and resets looked like crashes. Fixing the types meant changing metric definitions that dashboards already used, which caused a painful migration. An hour of planning at the start would have prevented it.
A Team That Forgot Shutdown
A receiver opened a background loop but never stopped it. During rolling upgrades, old Collector processes lingered and kept polling the source, doubling the load on a fragile system. Adding a proper shutdown that cancelled the loop fixed the problem, and a lifecycle test now guards against its return.
A Team That Made Failure Visible
When a source’s login expired, the receiver reported a clear configuration error, raised a counter, and slowed its retries. The on-call engineer saw the alert, renewed the credential and data resumed within minutes. Because the failure was visible rather than silent, nobody lost days of data without noticing.
21Frequently Asked Questions
Short, plain answers to the questions people ask most often.
Receivers for the Collector are written in Go, because the Collector itself is a Go program. Sources may be in any language, since the receiver only talks to them through a protocol or interface.
Yes. The factory declares which signals are supported, and a receiver can handle traces, metrics and logs, or any subset, with a separate stability level for each.
Let the source decide. If it can send data by itself, a push receiver is natural. If it only answers when asked, build a pull receiver using the scraper helpers.
Use the Collector builder with a manifest that lists your receiver’s module among the components. The builder produces a Collector distribution that includes it, and you configure it in the usual configuration file.
It reads a description of your metrics and attributes and generates the repetitive code, documentation and tests, so the receiver stays consistent and you write less boilerplate.
The Collector waits for each component to start before moving on. A start action that blocks would stall the whole Collector. Launch long-running work in the background and return.
Do not crash. Record the failure in measurements, log it sensibly, and try again at the next interval. Make sure operators can tell the difference between a quiet source and a broken collection.
Keep attribute values to a small, bounded set. Move values that vary without limit, such as identifiers, into logs or traces, or drop them.
Use saved sample data for translation tests, fake hosts and recording consumers for lifecycle tests, and containers running a real or simulated source for integration tests.
No. Private receivers can live in your own repository. Contributing makes sense for popular sources, and brings shared maintenance along with the obligation to keep it working.
They evolve between versions, and names of types and functions do change. Pin the library versions you use, read release notes when upgrading, and confirm details in the documentation for your version.
Usually not. Report raw totals or readings with correct types and let the backend compute rates. Calculations inside the receiver need remembered state and break on restart.
Not in the configuration text itself where possible. Allow files or environment variables, use the Collector’s types for sensitive values, and never write secrets to logs.
Watch the accepted and refused counts, collection failure counts and collection durations, and provide a clear indicator that distinguishes a quiet source from a failed collection.
22Summary and Key Takeaways
A custom receiver is a translator between a source the Collector does not yet understand and the common language of OpenTelemetry. To build one well, you study the source, decide how its data maps onto metrics, logs or traces, and choose between listening and fetching. You then assemble the standard parts: a configuration with sensible defaults and clear validation, a factory, a receiver with an honest start and shutdown, translation code that produces a well-formed data model, and a way of passing results to the next consumer.
The difference between a receiver that merely works and one that can be trusted lies in the details: bounded attributes, defensive handling of input, honest error signalling, measurements about itself, thorough tests and clear documentation. Use the tools the Collector gives you, follow its conventions, and keep translation separate from transport, and your receiver will be easier to build, safer to run and simpler to hand on to others.
Key Takeaways
- Plan before coding — study the source, decide what to collect and map each item to the right kind of telemetry using standard names.
- Choose push or pull deliberately — listeners suit sources that send data, scrapers suit sources that answer when asked.
- Assemble the standard parts — configuration, defaults, validation, factory, receiver instance and next consumer, with the Collector’s helpers wherever possible.
- Respect the lifecycle — start quickly, run in the background with its own context, and shut down completely.
- Model data carefully — identity on the resource, bounded attributes on points, correct units, timestamps, start times and metric types.
- Be honest about errors — separate permanent from temporary failures, signal backpressure, and never hide a broken collection behind a flat line.
- Test, secure and document — translation, lifecycle and integration tests, safe handling of credentials and input, and written guidance, released with an honest stability level.