How to Develop Custom Filters in the Easegress Pipeline: A Complete Guide
Developing a custom filter in Easegress requires implementing the filters.Filter interface, registering a filters.Kind descriptor in your package's init() function, and importing the package in pkg/registry/registry.go so the pipeline can instantiate it.
The Easegress pipeline processes traffic through a chain of filters that transform, validate, or route requests. When you need custom business logic that the built-in filters do not provide, you can extend the system by developing your own filter in Go. This guide walks through the complete process for developing custom filters in the Easegress pipeline, from package structure to pipeline registration.
Step 1: Create the Filter Package
Create a new directory under pkg/filters/<filter-name> to house your implementation. For example, pkg/filters/headercounter contains the reference implementation used throughout this guide. This location follows the standard Easegress project layout and keeps your code alongside the core filter library.
Step 2: Define the Configuration Spec
Define a Spec struct that holds the filter’s configuration fields. The struct uses YAML tags to map pipeline configuration values to Go fields.
type Spec struct {
Headers []string `yaml:"headers"`
}
This struct must implement filters.Spec, which is satisfied by embedding the base spec provided by the framework.
Step 3: Implement the Filter Struct
Create the main filter struct that maintains a reference to *Spec and any runtime state such as counters, mutexes, or client connections. The struct serves as the container for your business logic and state management.
type HeaderCounter struct {
spec *Spec
countMutex sync.Mutex
count map[string]int64
}
Step 4: Implement the filters.Filter Interface
Your filter must satisfy the filters.Filter interface defined in pkg/filters/filters.go. Implement all eight methods to ensure proper lifecycle management and request handling.
Required methods:
Name() string– Returnsspec.Name()to identify the filter instance.Kind() *filters.Kind– Returns the package-levelkindvariable describing the filter type.Spec() filters.Spec– Returns the concrete*Specfor configuration access.Init()– Prepares runtime state; typically calls a privatereload()method to initialize maps or connections.Inherit(previousGeneration filters.Filter)– Handles hot updates; usually callsInit()or copies state from the previous generation so the filter survives pipeline reloads without dropping connections.Handle(*context.Context) string– Contains the core request-processing logic. Return an empty string to continue normal pipeline flow, or return a custom result string to trigger conditional routing viajumpIf.Status() interface{}– Returns optional status information for admin APIs; returnnilif not needed.Close()– Cleans up goroutines, connections, or file descriptors.
Step 5: Declare the filters.Kind Metadata
Declare a package-level filters.Kind variable that describes your filter to the Easegress registry. This metadata tells the pipeline how to instantiate your filter and what result codes it may return.
const HeaderCounterKind = "HeaderCounter"
var kind = &filters.Kind{
Name: HeaderCounterKind,
Description: "HeaderCounter counts the number of requests which contain the specified header.",
Results: []string{}, // Add custom result codes for jumpIf
DefaultSpec: func() filters.Spec { return &Spec{} },
CreateInstance: func(spec filters.Spec) filters.Filter {
return &HeaderCounter{spec: spec.(*Spec)}
},
}
The CreateInstance function acts as a factory, casting the generic spec to your concrete type and returning a configured filter instance.
Step 6: Register the Filter in init()
Register the kind in the package’s init() function so the supervisor’s registry can discover it:
func init() { filters.Register(kind) }
This registration makes the filter type available for instantiation by name in pipeline configurations.
Step 7: Import the Filter in the Global Registry
To include your filter in the compiled binary, add a blank import in pkg/registry/registry.go:
import (
// other imports …
_ "github.com/megaease/easegress/v2/pkg/filters/headercounter"
)
The blank import triggers the init() function from Step 6, registering your filter with the global registry without exposing the package’s symbols elsewhere.
Step 8: Add the Filter to a Pipeline Spec
Reference your custom filter in a pipeline YAML configuration using the kind and name fields defined in your code:
filters:
- kind: HeaderCounter
name: headerCounter
headers: ["Cookie", "Authorization"]
When the Easegress server loads this configuration, it instantiates your filter using the factory defined in your filters.Kind.
Step 9: Leverage the jumpIf Mechanism (Optional)
If your Handle method returns a non-empty string listed in Kind.Results, you can use the jumpIf directive in your pipeline to branch execution. This is useful for early exits or conditional routing based on your filter’s processing outcome.
Complete Implementation Example: HeaderCounter Filter
Below is a runnable skeleton that follows all nine steps. This example counts requests containing specific headers and demonstrates proper mutex handling, lifecycle methods, and registration.
package headercounter // import "github.com/megaease/easegress/v2/pkg/filters/headercounter"
import (
"sync"
"github.com/megaease/easegress/v2/pkg/context"
"github.com/megaease/easegress/v2/pkg/filters"
"github.com/megaease/easegress/v2/pkg/protocols/httpprot"
)
// Spec holds the configuration.
type Spec struct {
Headers []string `yaml:"headers"`
}
// HeaderCounter implements the filters.Filter interface.
type HeaderCounter struct {
spec *Spec
countMutex sync.Mutex
count map[string]int64
}
// Name returns the filter instance name.
func (hc *HeaderCounter) Name() string { return hc.spec.Name() }
// Kind returns the filter kind descriptor.
func (hc *HeaderCounter) Kind() *filters.Kind { return kind }
// Spec returns the configuration spec.
func (hc *HeaderCounter) Spec() filters.Spec { return hc.spec }
// Init prepares runtime state.
func (hc *HeaderCounter) Init() { hc.reload() }
// Inherit handles hot updates by re-initializing state.
func (hc *HeaderCounter) Inherit(prev filters.Filter) { hc.Init() }
// Handle processes the request and counts headers.
func (hc *HeaderCounter) Handle(ctx *context.Context) (result string) {
for _, key := range hc.spec.Headers {
v := ctx.InputRequest().(*httpprot.Request).HTTPHeader().Get(key)
if v != "" {
hc.countMutex.Lock()
hc.count[key]++
hc.countMutex.Unlock()
}
}
return "" // Continue normal pipeline flow
}
// Status returns optional admin status (nil if unused).
func (hc *HeaderCounter) Status() interface{} { return nil }
// Close cleans up resources.
func (hc *HeaderCounter) Close() {}
// reload initializes the counter map.
func (hc *HeaderCounter) reload() {
hc.count = make(map[string]int64)
}
// Kind registration.
const HeaderCounterKind = "HeaderCounter"
var kind = &filters.Kind{
Name: HeaderCounterKind,
Description: "Counts the number of requests containing configured headers.",
Results: []string{},
DefaultSpec: func() filters.Spec { return &Spec{} },
CreateInstance: func(spec filters.Spec) filters.Filter {
return &HeaderCounter{spec: spec.(*Spec)}
},
}
func init() { filters.Register(kind) }
Place this file at pkg/filters/headercounter/headercounter.go and import it in pkg/registry/registry.go to activate it.
Key Source Files to Reference
pkg/filters/filters.go– Defines theFilterinterface and theKindmetadata structure used by every filter.pkg/registry/registry.go– Shows where custom filter packages are imported so the compiled server knows about them.docs/06.Development-for-Easegress/6.1.Developer-Guide.md– Contains the full walkthrough of filter creation, registration, and pipeline integration.
Summary
- Package Structure – Create your filter under
pkg/filters/<name>following the standard layout. - Interface Compliance – Implement all eight methods of
filters.FilterincludingHandle,Init, andInheritfor hot-reload support. - Metadata Declaration – Define a
filters.Kindwith factory function and register it ininit(). - Binary Integration – Use blank imports in
pkg/registry/registry.goto include your package in the build. - Pipeline Usage – Reference the filter by
kindin YAML configuration; return custom strings fromHandleto enablejumpIfrouting.
Frequently Asked Questions
What Go interface must a custom Easegress filter implement?
A custom filter must implement the filters.Filter interface defined in pkg/filters/filters.go. This includes eight methods: Name(), Kind(), Spec(), Init(), Inherit(), Handle(), Status(), and Close(). The Handle method receives a *context.Context and returns a string result code that controls pipeline flow.
How do I register a custom filter so the Easegress pipeline can discover it?
Declare a filters.Kind variable describing your filter, then call filters.Register(kind) inside your package’s init() function. Finally, add a blank import of your package in pkg/registry/registry.go. This registration pattern allows the supervisor to instantiate your filter by name when parsing pipeline configurations.
Can custom filters return result codes to control pipeline execution flow?
Yes. If your Handle method returns a non-empty string declared in Kind.Results, the pipeline can use the jumpIf directive to branch to a labeled filter or exit early. Return an empty string to continue normal sequential processing through the pipeline.
Where should I store runtime state that must survive configuration reloads?
Store runtime state in fields of your filter struct (e.g., counters, connection pools). Implement the Inherit method to copy state from the previous generation during hot updates, or simply call Init() if your filter can safely reinitialize. This ensures zero-downtime configuration changes while preserving accumulated state.
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 →