Classifiers
Plugin: netflow-plugin Module: classifiers
Overview
Annotate network flows with exporter and interface labels derived from reusable
classification rules. Where static metadata
forces you to enumerate every exporter and every ifIndex by hand, classifiers
let you express the network design once -- "anything matching ^edge- is the
edge tier", "any interface with BACKBONE-LUMEN in its description is on
Lumen", "any interface at 100Gbps is a core uplink" -- and apply that labelling
across the whole flow stream.
The plugin ships two rule lists, evaluated in YAML order:
enrichment.exporter_classifiers-- runs once per exporter (cached). Sees the exporter's IP and friendly name, and any classification slots already filled by static metadata or by earlier rules. Can setEXPORTER_GROUP / ROLE / SITE / REGION / TENANT.enrichment.interface_classifiers-- runs once per(exporter, interface)pair, applied twice per flow record (once for the input interface, once for the output). Sees everything an exporter rule sees plusInterface.Index / Name / Description / Speed / VLAN. Can setIN_IF_PROVIDER / OUT_IF_PROVIDER,IN_IF_CONNECTIVITY / OUT_IF_CONNECTIVITY,IN_IF_BOUNDARY / OUT_IF_BOUNDARY(1=external, 2=internal), and overrideIN_IF_NAME / DESCRIPTION/OUT_IF_NAME / DESCRIPTION.
The expression language is Akvorado-compatible for the documented operators
and actions. It implements a subset of Akvorado's expr-lang-derived grammar. Akvorado rules
using only equality, comparison, in, contains, startsWith, endsWith,
matches, &&, ||, !, parentheses, and the documented Classify* /
Reject / Format actions will work; arithmetic, ternaries, lambdas, and
arbitrary expr-lang features are not supported.
Output values written by Classify* actions are lowercased and stripped to
ASCII alphanumerics + . + + + - before they reach the flow record. So
ClassifyRegion("EU West") becomes euwest. Use SetName / SetDescription
when you want to preserve case and whitespace -- those write directly without
normalisation.
For the cross-cutting Enrichment concept (where classifiers sit in the merge order vs static metadata, GeoIP, IPAM, BGP routing), see Enrichment.
Each rule is a single boolean expression; an action with no condition (e.g.
Classify("edge") at top level) is treated as always-true and always fires.
Rules are AND/OR-composed, so the typical shape is condition && Classify*(...).
The plugin evaluates the list top to bottom, first-write-wins per slot:
once EXPORTER_GROUP is set, no later rule can change it. Order rules from
most-specific to least-specific.
Two short-circuit rules end the loop early. For exporter rules, the loop stops
when group + role + site + region + tenant are all non-empty. For
interface rules, the loop stops when connectivity + provider + boundary are
all set. SetName / SetDescription /
Reject do not contribute to short-circuit.
A rule that throws at runtime (e.g. comparing a string with >) breaks out of
the loop for that record and keeps whatever was set so far. Use matches, startsWith, or contains
on string fields instead of > / < to avoid this.
Akvorado parity: if metadata_static already filled any classification
slot for the target, the matching classifier list does not run for that
target -- operator-provided classification has priority and the rules cannot
override it. Don't try to mix static and rule-based labelling on the same
exporter or interface; pick one tool per target.
Results are cached. The exporter cache keys on ExporterInfo (ip + name). The
interface cache keys on (exporter, exporter_classification, interface) -- so
when the exporter's classification changes (for example after you push new
static metadata and restart) the interface caches naturally invalidate. The
cache TTL is enrichment.classifier_cache_duration (default 5 minutes). It is
a last-access TTL so entries live as long as they're queried.
This integration is only supported on the following platforms:
- Linux
This integration runs as a single instance per Netdata Agent.
Default Behavior
Auto-Detection
Disabled by default. Both rule lists are empty; populate enrichment.exporter_classifiers and / or enrichment.interface_classifiers to enable.
Limits
Resource use scales with rule count and the number of distinct exporters and interfaces. The classifier cache limits repeat evaluation for stable exporter/interface inventories.
Performance Impact
Rules run at decode time, in the flow-pipeline hot path, so cost matters.
The cache absorbs nearly all of it: per (exporter, interface) the rule list
evaluates only on cache miss. Tune
enrichment.classifier_cache_duration upwards (15-60 minutes) for very
high-cardinality exporter / interface pools where the default 5 minutes
still yields visible misses; tune downwards (30-60 seconds) when iterating
on rule changes during a config session.
Setup
Prerequisites
Know what to classify
Classifiers shine when there is a pattern to match -- exporter naming
conventions (edge-..., core-...), management-IP subnets per site,
SNMP interface descriptions that follow a template (BACKBONE-<carrier>,
TRANSIT-..., IX-...), or 100Gbps-equals-core conventions. If your
fleet has no such pattern, static metadata
is the better fit -- it lets you list each exporter and ifIndex by hand.
Configure interface metadata first if you want interface rules
The plugin does not poll SNMP itself, so Interface.Name,
Interface.Description, and Interface.Speed are populated only from
enrichment.metadata_static (the static-metadata integration card). If
you have not configured interfaces: under metadata_static.exporters,
those identifiers will be empty strings / zero, and any rule that
matches against them will never fire. Interface.Index and
Interface.VLAN come from the flow record itself and are always available.
Configuration
Options
Both lists live under enrichment:. Each entry is a free-form string
containing a single rule expression. The cache TTL is one global setting.
Config options
| Option | Description | Default | Required |
|---|---|---|---|
| enrichment.exporter_classifiers | Ordered list of rules applied per exporter. Each rule is a string expression. Available identifiers: Exporter.IP, Exporter.Name, CurrentClassification.Group / .Role / .Site / .Region / .Tenant. Available actions: Classify / ClassifyGroup, ClassifyRole, ClassifySite, ClassifyRegion, ClassifyTenant, plus the *Regex(input, pattern, template) variants of each, plus Reject(). Interface-only actions (ClassifyProvider, ClassifyConnectivity, ClassifyExternal / ClassifyInternal, SetName, SetDescription) fail at runtime if used here. | [] | no |
| enrichment.interface_classifiers | Ordered list of rules applied per (exporter, interface) pair. Sees everything an exporter rule sees, plus Interface.Index, Interface.Name, Interface.Description, Interface.Speed (bits per second), Interface.VLAN, and the per-interface CurrentClassification.Connectivity / .Provider / .Boundary / .Name / .Description. Available actions: ClassifyProvider, ClassifyConnectivity, ClassifyExternal(), ClassifyInternal(), SetName, SetDescription, Reject(), plus the *Regex variants of provider / connectivity. Exporter-only Classify* actions fail at runtime if used here. | [] | no |
| enrichment.classifier_cache_duration | Last-access TTL for both classifier caches (exporter and interface). Values below 1 second are rejected. The cache prunes opportunistically -- entries idle longer than the TTL are dropped on the next prune pass, capped at one prune every TTL or 30 seconds, whichever is smaller. Restart the plugin to clear caches outright when you change rules. | 5m | no |
via File
The configuration file name for this integration is netflow.yaml.
You can edit the configuration file using the edit-config script from the
Netdata config directory.
cd /etc/netdata 2>/dev/null || cd /opt/netdata/etc/netdata
sudo ./edit-config netflow.yaml
Examples
Exporter classification by name pattern
Tag exporters by the prefix of their friendly name -- the simplest and
most common pattern. Falls back to a regex capture for the region code
when the name encodes one. The final Reject() rule drops a test
exporter from collection entirely.
enrichment:
exporter_classifiers:
# Group by name prefix.
- 'Exporter.Name startsWith "edge-" && Classify("edge")'
- 'Exporter.Name startsWith "core-" && Classify("core")'
- 'Exporter.Name startsWith "agg-" && Classify("aggregation")'
# Site by management-IP subnet.
- 'Exporter.IP startsWith "10.1." && ClassifySite("ny-dc1")'
- 'Exporter.IP startsWith "10.2." && ClassifySite("par-dc1")'
# Region from a name suffix like "edge-fra-01" -> "fra".
- 'ClassifyRegionRegex(Exporter.Name, "-([a-z]{3})-[0-9]+$", "$1")'
# Drop a lab exporter entirely.
- 'Exporter.IP startsWith "192.0.2." && Reject()'
Interface classification from SNMP descriptions
Encode the boundary, the provider, and the connectivity tier from the
interface description that your network team already maintains. The
(?i) regex flag is the Rust regex inline-case-insensitive prefix.
Config
enrichment:
interface_classifiers:
# Provider tag from a description prefix.
- 'Interface.Description startsWith "BACKBONE-LUMEN" && ClassifyProvider("Lumen")'
- 'Interface.Description startsWith "BACKBONE-COGENT" && ClassifyProvider("Cogent")'
- 'Interface.Description startsWith "BACKBONE-NTT" && ClassifyProvider("NTT")'
# Transit links: external boundary + connectivity tag.
- 'Interface.Description contains "TRANSIT" && ClassifyConnectivity("transit") && ClassifyExternal()'
# Peering and IX -- case-insensitive regex.
- 'Interface.Description matches "(?i)^(IX|peering)-.*" && ClassifyConnectivity("peering") && ClassifyExternal()'
# Internal customer-facing access ports.
- 'Interface.Description startsWith "CUSTOMER-" && ClassifyConnectivity("customer") && ClassifyInternal()'
Boundary inferred from interface speed
A pragmatic shorthand when descriptions are unreliable but speed is
consistent. 100Gbps and faster interfaces are core, 10Gbps are
aggregation, 1Gbps and slower are access. Interface.Speed is in bits
per second -- numeric comparisons are safe.
Config
enrichment:
interface_classifiers:
- 'Interface.Speed >= 100000000000 && ClassifyConnectivity("core")'
- 'Interface.Speed >= 10000000000 && ClassifyConnectivity("aggregation")'
- 'Interface.Speed > 0 && ClassifyConnectivity("access")'
Combining exporter context with interface rules
Interface rules see the exporter's already-resolved classification
via CurrentClassification.*. Use it to scope interface rules to
specific tiers -- for example: every interface on an edge exporter
without a more-specific match falls back to "external".
Config
enrichment:
exporter_classifiers:
- 'Exporter.Name startsWith "edge-" && Classify("edge") && ClassifyRole("border")'
- 'Exporter.Name startsWith "core-" && Classify("core") && ClassifyRole("backbone")'
interface_classifiers:
# Specific provider rules first (most-specific to least-specific).
- 'Interface.Description startsWith "BACKBONE-LUMEN" && ClassifyProvider("Lumen")'
- 'Interface.Description startsWith "BACKBONE-COGENT" && ClassifyProvider("Cogent")'
# Generic transit rule.
- 'Interface.Description contains "TRANSIT" && ClassifyConnectivity("transit") && ClassifyExternal()'
# Fallback: any unclassified interface on an edge box is external.
- 'CurrentClassification.Role == "border" && CurrentClassification.Boundary == 0 && ClassifyExternal()'
Building values with Format and human-readable names
Format(pattern, args...) mimics Go's fmt.Sprintf for %s, %v,
%d, %%. Classify*
normalises (lowercase + strip non-alphanumeric); SetName and
SetDescription do not, so they preserve the case and spaces of the
computed value.
Config
enrichment:
exporter_classifiers:
# Tenant computed from name, normalised on write -> "tenant-edge01".
- 'ClassifyTenant(Format("tenant-%s", Exporter.Name))'
interface_classifiers:
# Human-readable name = "<exporter>:if<index>". Preserved verbatim.
- 'SetName(Format("%s:if%d", Exporter.Name, Interface.Index))'
Tuning the cache for a large fleet
The default 5-minute last-access TTL is right for steady-state. Raise it when the (exporter, interface) population is large enough that evicted entries are quickly re-queried. Lower it when actively iterating on rule changes so misses pick up the new rules quickly.
Config
enrichment:
classifier_cache_duration: 30m
exporter_classifiers:
- 'Exporter.Name startsWith "edge-" && Classify("edge")'
interface_classifiers:
- 'Interface.Speed >= 100000000000 && ClassifyConnectivity("core")'
Plugin fails to start with a parser error
A rule failed to parse. The journal log includes the index in the list
and a parser context (unsupported rule term, unsupported value expression, Reject() does not accept arguments, etc.). Common causes:
missing && between condition and action; an action used in the wrong
list (ClassifyExternal in an exporter rule); strings written with
single quotes (only JSON-style double quotes are accepted); regex literals
that fail to compile.
Classifier rules never run for an exporter or interface
Likely cause: metadata_static already set any classification field
on that target. By design, the matching list is suppressed entirely when
the classification is non-empty. Either remove the static-metadata entry for that target, or
keep static-metadata as the sole source for it.
A value appears differently in the dashboard than in the rule
Classify* actions normalise output to [a-z0-9.+-] only -- so
ClassifyRegion("EU West") lands as euwest, and
Classify("Edge_Tier_1") lands as edgetier1. Use SetName /
SetDescription to preserve case and whitespace; those write the value
verbatim.
First rule always wins, later rules never fire for the same slot
First-write-wins is by design and per slot. Order your
rules from most-specific to least-specific. If you want a tiered
fallback, use distinct slots (e.g. Classify for the broad group and
ClassifyRole for the tier within that group).
A working rule stops matching some time after startup
Cached results expire after classifier_cache_duration (default 5
minutes, last-access). When you change rules, restart the plugin so the
caches clear immediately -- otherwise stale cached classifications keep
returning until they idle out.
A rule with > or < aborts the rule list
Comparing a string-typed identifier with > / < / >= / \<= raises
a runtime error, and the loop breaks out for that record. Subsequent rules in
the list are skipped for that record. Use matches, startsWith,
endsWith, contains, or == / != on string fields. Keep > / <
for Interface.Index, Interface.Speed, and Interface.VLAN (the
numeric identifiers).
ClassifyExternal fires only on one side
Interface classifiers run twice per flow record -- once for the input
interface, once for the output. Both invocations see the same rule list. If your rule conditions on
Interface.Index == 42 and that ifIndex appears in IN_IF of one flow
and OUT_IF of another, the rule fires correctly in both places. But
the IN_IF_BOUNDARY / OUT_IF_BOUNDARY columns are independent -- a
rule firing on the output side of a flow only sets the output side's
boundary, and vice versa.
Interface fields are empty in the rule even though SNMP is configured
The plugin does not poll SNMP -- Interface.Name, Description, and
Speed come exclusively from enrichment.metadata_static.exporters.<ip>.interfaces.<index>.
If you populate them through an external SNMP discovery and write them
into metadata_static, the rules will see them. Otherwise those fields
resolve to empty strings / zero, and any rule that conditions on them
never matches.
Referencing Interface.* in an exporter rule silently does nothing
Field resolution does not error when the wrong context is missing -- it
returns the type's zero value. So Interface.Speed >= 1 written in an exporter_classifiers rule
resolves to 0 >= 1 (false) on every call. Use
interface_classifiers for any rule that needs an interface field.
Do you have any feedback for this page? If so, you can open a new issue on our netdata/learn repository.