This directory is the in-repo source home for SkyWalking-maintained
post-trace-pipeline sampler plugins. Every plugin here is a first-class Go
package: it participates in go build ./..., go vet, golangci-lint, and
the license-header check, and it is compiled into the -plugins variant of
the banyand server image (see docs/operation/plugins.md).
plugins/
README.md # this file
skywalking/
internal/tracesampler/ # shared §6.1/§6.2 sampler engine (not a plugin)
sw-trace-sampler/main.go # segment schema (sw_trace): duration/is_error/tags rules
zipkin-trace-sampler/main.go # zipkin_span schema (sw_zipkinTrace): duration/query rules
internal/tracesampler is a shared library, not a plugin: it has no main.go,
so make build-plugins (which only builds plugins/*/*/ dirs containing a
main.go) skips it, and the sw-trace-sampler / zipkin-trace-sampler mains
each compile it into their own .so. The two mains differ only in a Schema
value naming the columns each trace schema keeps the rule inputs in — see
First-party sampler config for that mapping and
the keys both plugins accept.
Because every keepTagRules entry resolves to the flattened array, a rule naming
a first-class column could never match. Each Schema therefore carries its
model's full column inventory (FirstClassColumns) and the engine rejects such a
rule at construction instead of letting it silently never fire — so
{tagKey: service_id, equals: ...}, the natural way to write "keep everything
from the payment service", fails loudly rather than dropping exactly those traces.
The skywalking/ namespace holds plugins maintained by the SkyWalking
project itself. A future third-party vendor would get its own sibling
namespace (e.g. plugins/acme/) — but see the "Third-party plugins" section
below: this repo only documents and builds first-party plugins; a
third-party .so is built and shipped by its own vendor, not committed here.
This is only the source layout. At runtime, the first-party .so files
built here are shipped in a carrier image (the -plugins-carrier-tagged image
on the same apache/skywalking-banyandb repo) and mounted into the
plugin-capable -plugins host image at /plugins (the trusted plugin
directory, empty in the host image); third-party .so files are mounted at
the reserved subdirectory /plugins/thirdparty — see
docs/operation/plugins.md.
A plugin is a package main built with go build -buildmode=plugin that
exports exactly two symbols, defined by pkg/pipeline/sdk:
var ABIVersion = sdk.ABIVersion // re-export, unchanged
func NewSampler(config []byte) (sdk.Sampler, error)
func main() {} // required by -buildmode=pluginABIVersionmust equal the host's compiledsdk.ABIVersionexactly. A mismatch is a fail-fast load error (never silent corruption).NewSampleris the constructor the engine looks up by default; aSamplerPlugin.symbolin the group's pipeline config can override the symbol name for a plugin package that exports more than one sampler.configis the canonical JSON encoding of theSamplerPlugin.configgoogle.protobuf.Struct; the plugin unmarshals it into its own typed config.- The
sdk.Samplerinterface (Kind,Project,Decide,Close) and the vectorizedsdk.TraceBatch/sdk.TraceBlocktypes are the full contract — see the package doc onpkg/pipeline/sdkfor the authoritative reference.
sw-trace-sampler and zipkin-trace-sampler share one engine
(internal/tracesampler) and accept the same keys. This is the behavioral
reference; for bydb.yml wiring, the SW_STORAGE_BANYANDB_* environment
overrides, and tuning guidance, see SkyWalking's
BanyanDB storage guide.
| Key | Type | Meaning |
|---|---|---|
durationThresholdMs |
int | Keep a trace whose end-to-end envelope reaches this many milliseconds. 0 disables. |
keepErrors |
bool | Keep a trace carrying an error. Rejected on a schema that expresses error neither as a column nor as an array key. On the Zipkin schema see the truncation caveat below. |
errorTag |
string | Override the tag keepErrors reads. Ignored unless keepErrors is set. |
keepTagRules |
list | string | Sure-keep rules over the searchable tags: {tagKey, exists|equals|in|regex}, or the compact key=value,key=~regex,key string form. |
healthySampleRate |
float | Fraction in [0,1] of the remaining traces to keep, by FNV-1a hash of the trace ID. Accepts a quoted number, since SkyWalking's config loader stringifies float placeholders. |
Unknown keys are rejected. Every option is a keep rule, so a key that
silently missed would leave a sampler with no rules that drops the whole group —
the failure mode is data loss, not a stale setting. A wholly empty config ({},
which pipeline_loader also substitutes for an absent SamplerPlugin.config) is
rejected for the same reason. Note Go matches field names case-insensitively, so
this catches wrong words, not wrong capitalization; the reference
_example/segment-tail-sampler uses snake_case, so a config copied from it is
refused outright.
Zipkin keepErrors misses long error values. OAP's SpanForward skips both
the bare key and key=value when either the value or the joined string exceeds
Tag.TAG_LENGTH (256 chars), so an error tag carrying a long exception message
reaches query in neither form and keepErrors cannot see it — the loudest
errors are the ones that go missing. Until OAP records the bare key before that
length check, catch those with a keepTagRules entry on a short-valued tag, such
as an http.status_code regex.
Per-schema inputs. The two mains differ only in this mapping:
| Input | sw-trace-sampler |
zipkin-trace-sampler |
|---|---|---|
| Searchable tags | tags |
query |
| Error signal | is_error column |
error key inside query |
| Per-row start | start_time |
timestamp_millis |
| Per-row duration | latency (ms) |
duration (µs) |
Both start columns are BanyanDB timestamp columns, so both arrive in
nanoseconds whatever the tag is called; only the duration unit differs, and
Schema.DurationTagNanosPerUnit normalizes it. durationThresholdMs is
milliseconds on both.
Duration is an envelope, not a per-span test:
envelope = max(start + duration) − min(start) over every row of the trace
keep if envelope ≥ durationThresholdMs
so a trace slow only through sequential spans is caught. It is deliberately not
the intrinsic MaxTS − MinTS, which is the spread of per-row start timestamps
(and 0 for a single-row trace).
Order and fail-open. Rules are OR-ed in the order duration, errors, tag rules, healthy sample; the first match wins, so order affects cost only.
A rule that cannot be evaluated keeps the trace. An absent duration or error column means "can't tell", not "not slow" / "no error": those columns are schema-declared, so their absence implies the block was written under a different schema — typically the wrong plugin attached to the group — and dropping there would discard exactly what the option was enabled to save.
Two cases are deliberately NOT treated that way, because they are ordinary
data rather than a schema mismatch: an absent tag array (a trace with no
searchable tags simply matches no tag rule), and an error column that is
present but not truthy (an unset flag really does mean "not an error", unlike
an absent measurement). A row carrying a start but no duration still anchors
the envelope's left edge — Zipkin's duration is optional, so requiring both
would measure a trace from a later span and understate it.
A Go plugin is loaded via plugin.Open, which requires the .so and the
host process to share the exact same:
- Go toolchain version (this repo pins
go 1.25.12viago.mod;GOTOOLCHAIN=autoresolves it identically in CI, in the-pluginsDocker builder stage, and in a localmake build-plugins), pkg/pipeline/sdkpackage build (and its full transitive module graph),- CGO mode (
CGO_ENABLED=1) and race-detector mode (-raceor not), - and a compatible libc (the
-pluginsruntime image isgcr.io/distroless/base-debian12and its builder stage isgolang:1.25-bookworm— both Debian 12/bookworm, same glibc 2.36, for exactly this reason).
Any drift and plugin.Open rejects the .so outright ("different version of
package …"). This is by design: mismatch is always safe and loud, never
silent. For the first-party plugins in this directory, parity is guaranteed
by lockstep — the -plugins Dockerfile builder base compiles the host binary
and every plugin .so from the same commit in the same CI run, and the host
image (skywalking-banyandb:<tag>-plugins) and the carrier image
(skywalking-banyandb:<tag>-plugins-carrier, same repo) are published in
lockstep (see docs/operation/plugins.md). Always deploy the carrier at the
host's tag + -carrier; do not build a first-party .so on a different
machine/toolchain and expect it to load.
Requires a C toolchain (gcc or clang) because Go plugins require
CGO_ENABLED=1. Go plugins are not supported on Windows.
make build-plugins # builds every plugins/<vendor>/<name>/main.go into
# $(PLUGIN_OUTPUT_DIR) (default build/bin/plugins)This reuses the same -trimpath flags as make build-trace-pipeline-server
(root Makefile), matching the flags the -plugins Docker builder stage
uses, so a locally built .so loads into a locally built
build-trace-pipeline-server binary — but NOT necessarily into the released
-plugins image (which is built by its own Dockerfile stage; see the
toolchain-lock note above).
Every plugin under this directory is built into the
apache/skywalking-banyandb:<tag>-plugins-carrier carrier image, which is
mounted at /plugins into the plugin-capable
apache/skywalking-banyandb:<tag>-plugins host image (whose /plugins is
empty). An operator enables plugin hosting on the data node (the role that
runs merge/finalize) with two flags:
-trace-pipeline-native-plugin-enabled=true
-trace-pipeline-trusted-plugin-dir=/plugins
then references a plugin by filename in the group's pipeline config, e.g.
SamplerPlugin.path = "sw-trace-sampler.so". See
docs/operation/plugins.md for the full deployment guide (image-volume and
initContainer mount mechanisms, third-party plugins, and Kubernetes examples).
- Create
plugins/skywalking/<name>/main.goimplementingsdk.Sampler(ABIVersion,NewSampler, emptymain()). - Add a
_test.gousingpkg/pipeline/sdk/sdktestto verifyDecideoffline (no.so, no cluster) before ever building a.so— seepkg/pipeline/sdk/sdktestand the seed plugin's test for the pattern. make build-pluginslocally to confirm it compiles as a plugin.- The carrier image picks up any new
plugins/*/*/main.goautomatically (the builder stage globs the directory) — no Dockerfile change needed for a new plugin, only for a new toolchain requirement.
Third-party/operator plugins are not stored in this repository. An
operator builds their .so against the pinned SDK + toolchain published for
each release (see docs/operation/plugins.md) and mounts it at the reserved
subdirectory /plugins/thirdparty, keeping the first-party carrier mounted at
/plugins.