How to Write Custom Processor Plugins for Telegraf: A Complete Developer Guide
To write custom processor plugins for Telegraf, implement the telegraf.Processor interface for synchronous batch processing or the telegraf.StreamingProcessor interface for asynchronous workflows, register your plugin using processors.Add() or processors.AddStreaming() in an init() function, and embed a sample.conf file for configuration discovery.
Telegraf’s processor plugin architecture enables you to transform, filter, and enrich metrics as they flow between inputs and outputs. Writing custom processor plugins for Telegraf involves creating a Go package that conforms to interfaces defined in processor.go and hooks into the global registry system. This guide explains the exact development patterns used in the influxdata/telegraf repository, including interface implementations, registration mechanics, and required file structures.
Core Interfaces: Processor vs. StreamingProcessor
Telegraf defines two distinct processing models in [processor.go](https://github.com/influxdata/telegraf/blob/master/processor.go). Both interfaces embed PluginDescriber, which automatically provides SampleConfig() and Description() methods based on your struct tags.
telegraf.Processor (Synchronous)
- Method signature:
Apply(metrics ...telegraf.Metric) []telegraf.Metric - Use case: Stateless transformations that operate on complete batches of metrics
- Execution: Blocking; the agent waits for the entire batch to process before continuing
telegraf.StreamingProcessor (Asynchronous)
- Method signatures:
Start(acc telegraf.Accumulator) error,Add(m telegraf.Metric, acc telegraf.Accumulator) error,Stop() - Use case: Stateful operations, background workers, or tasks requiring goroutines (e.g., DNS lookups, external API calls)
- Execution: Non-blocking; metrics flow through individually
Registering Your Plugin with the Global Registry
All processor plugins must register themselves in the global processors map located in [plugins/processors/registry.go](https://github.com/influxdata/telegraf/blob/master/plugins/processors/registry.go). Registration occurs inside an init() function to ensure discovery at startup.
- Legacy processors: Call
processors.Add(name, creator)wherecreatorreturns atelegraf.Processor - Streaming processors: Call
processors.AddStreaming(name, creator)wherecreatorreturns atelegraf.StreamingProcessor
The plugin name passed to these functions must exactly match the TOML configuration stanza (e.g., processors.Add("myprocessor", ...) corresponds to [[processors.myprocessor]] in telegraf.conf).
Minimal Processor Implementation
Below is a complete skeleton for a synchronous processor. This pattern mirrors the reference implementation in [plugins/processors/unpivot/unpivot.go](https://github.com/influxdata/telegraf/blob/master/plugins/processors/unpivot/unpivot.go).
package myprocessor
import (
_ "embed"
"fmt"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/plugins/processors"
)
//go:embed sample.conf
var sampleConfig string
// MyProcessor demonstrates the synchronous Processor interface.
type MyProcessor struct {
Prefix string `toml:"prefix"` // Maps to "prefix" key in TOML config
}
// Init validates configuration after TOML parsing but before metric processing.
func (p *MyProcessor) Init() error {
if p.Prefix == "" {
return fmt.Errorf("prefix must be set")
}
return nil
}
// SampleConfig returns the embedded configuration example.
func (p *MyProcessor) SampleConfig() string {
return sampleConfig
}
// Apply implements the core processing logic.
// It receives a batch of metrics and must return the processed slice.
func (p *MyProcessor) Apply(metrics ...telegraf.Metric) []telegraf.Metric {
out := make([]telegraf.Metric, 0, len(metrics))
for _, m := range metrics {
// Example transformation: prepend prefix to metric name
newName := fmt.Sprintf("%s_%s", p.Prefix, m.Name())
m.SetName(newName)
out = append(out, m)
}
return out
}
// init registers the plugin at import time.
func init() {
processors.Add("myprocessor", func() telegraf.Processor {
return &MyProcessor{}
})
}
Implementing Streaming Processors for Asynchronous Work
For operations requiring background goroutines or external I/O, implement telegraf.StreamingProcessor. The reverse_dns processor in the Telegraf codebase provides a complete production example of this pattern.
package mystream
import (
"sync"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/plugins/processors"
)
// MyStreamingProcessor handles metrics asynchronously.
type MyStreamingProcessor struct {
acc telegraf.Accumulator
wg sync.WaitGroup
}
// Start initializes resources when the Telegraf agent starts.
func (p *MyStreamingProcessor) Start(acc telegraf.Accumulator) error {
p.acc = acc
// Initialize workers, open connections, etc.
return nil
}
// Add receives individual metrics for asynchronous processing.
func (p *MyStreamingProcessor) Add(m telegraf.Metric, acc telegraf.Accumulator) error {
p.wg.Add(1)
go func() {
defer p.wg.Done()
// Perform async work (e.g., DNS lookup, API enrichment)
// Then write the result:
// acc.AddMetric(transformedMetric)
}()
return nil
}
// Stop waits for all background goroutines to complete during shutdown.
func (p *MyStreamingProcessor) Stop() {
p.wg.Wait()
}
// init registers as a streaming processor.
func init() {
processors.AddStreaming("mystream", func() telegraf.StreamingProcessor {
return &MyStreamingProcessor{}
})
}
File Structure and Required Conventions
Organize your custom processor according to Telegraf’s standard directory layout to ensure compatibility with the build system and documentation generators.
Directory Structure
plugins/processors/<myplugin>/— Source code directory containing Go files andsample.confplugins/processors/all/<myplugin>.go— Import file with build tags ( ensuresinit()runs)docs/PROCESSORS.md— Developer documentation (follow existing style guidelines)
Required Files
<plugin>.go: Main implementation with struct definition and interface methodssample.conf: TOML configuration example embedded via//go:embedREADME.md: User-facing documentation explaining configuration options
Naming Conventions
- The TOML stanza name must match the registration string exactly
- Configuration struct fields use
toml:"field_name"tags - Call
metric.Drop()to filter out metrics; otherwise ensure metrics are forwarded via the returned slice oracc.AddMetric()
After placing your code in the correct directory and ensuring it is imported in plugins/processors/all/*.go, compile Telegraf and reference your processor in telegraf.conf:
[[processors.myprocessor]]
prefix = "production"
Summary
- Implement either
telegraf.Processorfor batch operations ortelegraf.StreamingProcessorfor async workflows, both defined inprocessor.go - Register your plugin using
processors.Add()orprocessors.AddStreaming()inside aninit()function inplugins/processors/registry.go - Embed a
sample.conffile and expose it viaSampleConfig()to enabletelegraf --input-listdiscovery - Place source files in
plugins/processors/<name>/and ensure build tags inplugins/processors/all/*.goimport your package
Frequently Asked Questions
What is the difference between Apply() and Add() in Telegraf processors?
Apply() is the method signature for synchronous processors that receive complete batches of metrics (as implemented in the Processor interface), while Add() is used by streaming processors to handle metrics one-by-one asynchronously. According to the Telegraf source code in processor.go, Apply() must return the full processed slice, whereas Add() writes results directly to the provided telegraf.Accumulator and can spawn background goroutines.
How do I drop metrics that should not be forwarded to outputs?
Call metric.Drop() on any metric you want to filter out. In the Apply() method, simply exclude the metric from the returned slice. For streaming processors using Add(), do not call acc.AddMetric() for metrics you wish to discard. Dropping metrics at the processor stage prevents them from reaching aggregators or outputs.
Why is my custom processor not appearing in telegraf --input-list?
Your plugin must be imported into the Telegraf binary. Ensure your package is imported in a file under plugins/processors/all/ (which uses build tags to orchestrate imports), and verify your init() function correctly calls processors.Add() or processors.AddStreaming() with the exact name matching your configuration stanza. Also confirm that your SampleConfig() method returns non-empty content embedded from sample.conf.
Can I maintain state between metrics in a processor?
Yes, but only when using the telegraf.StreamingProcessor interface. Synchronous processors implementing Apply() should remain stateless because they receive independent batches. Streaming processors maintain state through struct fields and can use goroutines managed via Start() and Stop() lifecycle hooks, as demonstrated in the reverse_dns implementation where DNS cache state persists across multiple Add() calls.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →