The Go half of Envoy's golang HTTP filter contrib, extracted into a standalone module so Apoxy EdgeFunction plugins can depend on a tagged artifact instead of pinning the whole Envoy source tree.
import (
"github.com/apoxy-dev/envoy-go-sdk/api"
"github.com/apoxy-dev/envoy-go-sdk/http"
)| package | upstream |
|---|---|
api/ |
contrib/golang/common/go/api (+ api.h) |
api_impl/ |
contrib/golang/common/go/api_impl |
utils/ |
contrib/golang/common/go/utils |
http/ |
contrib/golang/filters/http/source/go/pkg/http |
The tree is upstream verbatim apart from rewritten import paths, a collapsed
cgo include path, and the diffs in patches/. UPSTREAM records
the Envoy tag it currently tracks.
A plugin is a .so that Envoy dlopen()s, and api/api.h is the C ABI
between them. A struct layout or prototype that differs from the Envoy binary
loading the plugin is not a compile error, it is a segfault or silent memory
corruption at runtime.
So module tags carry the Envoy tag they were cut from:
v1.36.1-apoxy.1 # Envoy v1.36.1, first Apoxy revision
v1.36.1-apoxy.2 # same ABI, SDK-side fix
v1.38.2-apoxy.1 # Envoy v1.38.2 -- every plugin must be rebuilt
Pin the tag that matches the backplane's Envoy. Never mix.
The Envoy callbacks the http package calls (envoyGoFilterHttpContinue and
32 others) are supplied by the Envoy process at dlopen() time. A test binary
has no Envoy, so without help it links and then dies on a null PC.
http/envoy_stub.go defines all of them as no-ops behind //go:build envoystub:
go build -buildmode=c-shared -o plugin.so . # production: NO tag, ever
go test -race -tags envoystub ./... # tests: opt inThe tag is opt-in on purpose. The obvious design — stubs on by default,
excluded by a cshared tag in the Dockerfile — fails open: a Dockerfile that
forgets the tag ships a plugin whose stubs shadow the real Envoy callbacks, so
the filter loads cleanly and does nothing. Opting in means a production build
passes no tag at all and cannot contain a stub.
make check-example asserts both directions with nm, which is what each
plugin's Dockerfile used to do by hand.
One consequence worth knowing if you edit the stub: it lives in package
http, whose cgo preamble includes api.h. cgo concatenates every file's
preamble into _cgo_export.c, so a stub whose signature does not match the
api.h prototype is a conflicting types compile error. That is deliberate —
the C compiler checks the stubs against the ABI for us.
Envoy stats reach Prometheus through Envoy's own /stats endpoint, with
envoy_ prepended and . mapped to _. Stats carry no labels, so every
dimension has to be baked into the name, and only from a closed vocabulary —
never from request or config data, or the stats scope grows without bound.
Define metrics once, in Parse, and hold them on the config:
type config struct {
requests api.CounterMetric
latency api.HistogramMetric
}
func (p *parser) Parse(_ *anypb.Any, callbacks api.ConfigCallbackHandler) (interface{}, error) {
if callbacks == nil {
return &config{}, nil // route-level config: no callbacks
}
return &config{
requests: callbacks.DefineCounterMetric("example.requests_total"),
latency: callbacks.DefineHistogramMetric("example.decode_headers_duration_us"),
}, nil
}
func (f *filter) DecodeHeaders(header api.RequestHeaderMap, endStream bool) api.StatusType {
start := time.Now()
f.config.requests.Increment(1)
// ...
f.config.latency.Record(uint64(time.Since(start).Microseconds()))
return api.Continue
}HistogramMetric is Record-only. Increment and Get exist on counters and
gauges because the C++ side implements them for those types; for a histogram
they would cross the ABI and do nothing.
Upstream has never wired the histogram up on the Go side (// TODO Histogram
in ConfigCallbacks) even though FilterConfig::defineMetric and
recordMetric have handled MetricType::Histogram for releases. That gap is
patches/0001-histogram-metric.patch.
See example/ for a filter that compiles.
scripts/sync-upstream.sh v1.38.2
go mod tidy
make test build-example check-exampleThe script re-copies the four packages from the module cache, rewrites import
paths, restores the SDK-owned files, writes UPSTREAM, and replays
patches/*.patch. Read the api/api.h diff before committing: any change
there is an ABI break, and every plugin has to be rebuilt and re-pinned.
If a patch conflicts, resolve it by hand and regenerate:
git diff HEAD -- api/filter.go http/filter.go > patches/0001-histogram-metric.patch