Upgrading¶
This page lists breaking changes by release, most recent first, plus notes for users migrating from the upstream AthennaMind exporter. Full details for every release: Changelog.
Upgrading to v4.0 from v3.x¶
Project renamed: opnsense-exporter -> opnsense2otel¶
The project, repository, Go module, container image, Helm chart path, docs URL and environment variable prefix are all renamed in this release. Metric names and Grafana dashboard UIDs are the exception - see the last item below.
- Container image - pull
ghcr.io/rknightion/opnsense2otelinstead ofghcr.io/rknightion/opnsense-exporter. - Environment variable prefix -
OPNSENSE_EXPORTER_*becomesOPN2OTEL_*. This is a hard break: there are no back-compat aliases for the prefix itself. Rewrite compose files, systemd unitEnvironment=lines, and Kubernetes manifests before upgrading:
This does not touch the unprefixed OPS_API_KEY_FILE / OPS_API_SECRET_FILE /
PYROSCOPE_AUTH_USER_FILE / PYROSCOPE_AUTH_PASSWORD_FILE file-secret aliases -
those are a separate, unrelated convention and are unchanged.
- Helm chart path - the chart moved from charts/opnsense-exporter to
charts/opnsense2otel. Update any local clone path or helm upgrade invocation
that references the old directory.
- Docs site - https://m7kni.io/opnsense-exporter/ becomes
https://m7kni.io/opnsense2otel/.
- Go module path - github.com/rknightion/opnsense-exporter becomes
github.com/rknightion/opnsense2otel/v4. Relevant only if you import packages
from this module directly rather than running the binary or image.
- Metric names and dashboard UIDs did not change. Every opnsense_* and
opnsense_exporter_* metric keeps its existing name, and the Grafana dashboard
UIDs stay opnsense-exporter / opnsense-exporter-health on purpose - changing
a UID breaks every existing bookmark and every alert's __dashboardUid__ link.
Existing dashboards, alert rules, and PromQL/LogQL queries keep working
unmodified; only the image reference, environment variables, chart path and
docs URL need updating.
- CPU metrics are now cumulative counters fed by a stream, not percentage gauges -
opnsense_activity_cpu_user_percentand itsnice/system/interrupt/idlesiblings are removed. CPU utilisation now comes fromopnsense_cpu_seconds_total{mode="user|nice|system|interrupt|idle"}, so a panel or alert reading the old gauges must become100 * rate(opnsense_cpu_seconds_total[$__rate_interval]). The bundled dashboard and rules are already migrated.
This is a better number, not merely a renamed one. The old gauges came from
diagnostics/activity/get_activity, which is a 2.15-second call on the firewall
because OPNsense runs top -aHSTn -d2 and waits out top's inter-display delay - a
permanent 14% firewall duty cycle at a 15s poll, 1.9 GB/day of payload, and it
sampled two seconds in every fifteen, so 87% of the timeline was never observed.
The exporter now holds one Server-Sent Events connection to
api/diagnostics/cpu_usage/stream (iostat -w 1), sees every second, and costs
the firewall about 70 bytes/sec and no process-table walk. The activity collector
survives for thread-state counts only and has moved to the 60s tier.
Two operational consequences worth knowing before you upgrade:
- **A new outbound long-lived connection ships on by default.** It holds one
php-cgi worker on the firewall (~2.5% of the measured 40-worker capacity) plus a
persistent `iostat`, permanently. Disable it with `--exporter.disable-cpu`,
which leaves the box with no CPU utilisation series at all.
- **`cpu_seconds_total` goes ABSENT, not flat, when the stream dies.** The
exporter re-dials on a stall, but recovery bounds an outage rather than removing
it - during a firewall reboot there is nothing to reconnect to. After one export
interval of silence the counters are withdrawn, because a frozen counter reads
as an idle CPU under `rate()` and is silently wrong. `opnsense_cpu_stream_up`,
`opnsense_cpu_stream_last_frame_age_seconds` and
`opnsense_cpu_stream_counters_published` are exported throughout so the cause is
visible; `OPNsenseCPUStreamStalled` alerts on frame age, not on connectedness,
because this endpoint's documented failure mode is keepalives continuing after
the data stops.
-
GeoIP enrichment is now ON by default, and the image bundles a database -
--geoip.enableddefaults totrue, and the container image ships the DB-IP Lite Country and ASN databases at/usr/share/opnsense2otel/geoip/, which are the new default values of--geoip.country-database/--geoip.asn-database. A container deployment that had never configured GeoIP now emits<src|dst>.geo.*attributes on flow records and on filterlog, sshd/auth and Suricata log lines - filterlog is the highest-volume log stream on the box, so this is a real per-line ingest cost (~116 B on a line that resolves, measured) with no config change. Set--geoip.enabled=falseto opt out entirely, or--logs.syslog.geoip=falseto keep geo on flow records only. MaxMind still wins outright wherever it is configured, and an explicit database path is never overridden. Two things to know: DB-IP Lite is a reduced-accuracy subset (country is >95% accurate, city is not - see GeoIP), and non-container builds carry no database at all and simply enrich nothing.OPNsenseFlowGeoIPDatabaseStalealerts at 14 days, while DB-IP republishes monthly, so expect that alert to fire on the bundled databases unless you retune it. Attribution: IP geolocation data by DB-IP, CC BY 4.0. -
Six redundant Zenarmor structured-metadata keys are no longer shipped -
organization,policyid,src_geoip.latitude,src_geoip.longitude,dst_geoip.latitudeanddst_geoip.longitudeare no longer attributed onto each record, and the OTLP instrumentation scope name is now empty so Loki stops addingscope_name. Structured metadata is billed as ingested volume, and these measured ~231 bytes per line of pure repetition on the largest log source. Every one of them is still in the log body, which is unchanged, so a line-level lookup still has them; only the re-extracted copy is gone. A LogQL filter using| organization=,| policyid=,| scope_name=or either coordinate pair will stop matching - drop the filter, or read the value out of the body with| json. Nothing in the bundled dashboard, alert rules or exported metrics consumed any of them.--logs.zenarmor.excluderules namingorganizationorpolicyidnow fail at startup as unknown fields; such a rule was always either a no-op or a total drop, because both keys carry exactly one value per deployment. -
opnsense_exporter_otlp_*now carries theopnsense_instancelabel - the four OTLP delivery-health series (otlp_enabled,otlp_exports_total{result},otlp_consecutive_failures,otlp_last_success_timestamp_seconds) were registered without appliance identity, so on a multi-firewall stack there was no way to tell whose push pipeline had stalled. They now carryopnsense_instancelike every otheropnsense_exporter_*family. A recording rule, alert or dashboard query that aggregates these series withoutby (opnsense_instance)will now return one series per exporter instead of one overall - add the grouping, or usesum without (opnsense_instance) (...)to keep the old shape. Single-instance deployments see no change beyond the extra label. The bundled dashboard and rules are already updated. -
Scrape deadline compatibility surfaces removed - the
exporter.scrape-timeout-offsetoption (formerly passed with two leading hyphens),opnsense_exporter_scrape_skips_total, and the exporter's handling ofX-Prometheus-Scrape-Timeout-Secondshave been removed. They became inert when collection moved to the background poll scheduler:/metricsnow replays an in-memory snapshot and does not make OPNsense API calls. Remove the flag from deployment arguments and replace skip-counter alerts with scheduler freshness, duration, and failure signals. Prometheus's ownscrape_timeoutstill bounds the HTTP request. -
Log freshness gauges have stage-specific names -
opnsense_exporter_logs_last_event_timestamp_secondshas been removed. Useopnsense_exporter_logs_last_received_timestamp_seconds{source}to measure when the exporter last admitted input andopnsense_exporter_logs_last_exported_timestamp_seconds{source}to measure when the sink last acknowledged delivery. Both use the exporter clock; neither uses a sender-controlled event timestamp. -
Container health uses the native
healthsubcommand - the distroless image now runsopnsense2otel health, which probes/-/healthywithout requiring a shell,curl, orwget. Custom images and Compose overrides that copied the oldwgethealthcheck should switch to this command. Docker health status alone does not triggerrestart:; process exit or an external unhealthy-container remediation mechanism is still required for automatic replacement. -
VLAN traffic on a trunk is now attributed by subnet, so interface labels move - a NetFlow record captured on a trunk whose address falls inside exactly one VLAN child's configured subnet is now attributed to that child immediately, instead of waiting up to two seconds for the child's own copy to arrive and win. Nothing is dropped and no metric is renamed, but per-interface flow volume shifts off the trunk and onto the VLAN children, which is the correction: a production measurement found 29.2% of trunk/child pairs had a gap wider than the two-second window, and every one of those flows was being counted against the trunk. A further 247,105 trunk-captured records over 18h35m had no child copy at all and could never have been attributed by timing. If you have dashboards or alerts with a hard-coded trunk interface name, or thresholds calibrated on trunk volume, expect both to change on upgrade. Two new counters make it observable:
opnsense_flow_vlan_subnet_attributed_totalcounts the attributions andopnsense_flow_vlan_late_child_copies_totalcounts the residual that arrived too late to correct. The two-second hold remains for addresses that match no child subnet or several of them, so a VLAN with no configured subnet behaves exactly as before. -
PF packet counters renamed with a
_totalsuffix - the eightopnsense_firewall_{in,out}_{ipv4,ipv6}_{pass,block}_packetsseries are now..._packets_total. They were always emitted as Prometheus counters, but the unsuffixed names only ever appeared on a direct/metricsscrape: OTLP-to-Prometheus canonicalization appends_totalto every monotonic sum, so a stack fed through the OTLP bridge exposed only the_totalnames and every panel, recording rule, and alert written against the unsuffixed name silently returned no data. The descriptors now match what both paths expose. The sibling byte counters already carried_totaland are unchanged. Update any custom dashboard, recording rule, or alert that referenced the unsuffixed names; the bundled Grafana dashboard and rules are already updated. The generated metric catalogue also now types all sixteen pf pass/block series as Counter -
the eight packet series were previously mis-documented as Gauge.
-
IPsec and vnStat counters renamed with a
_totalsuffix - the eightopnsense_ipsec_phase{1,2}_{bytes,packets}_{in,out}series are now..._{in,out}_total(for exampleopnsense_ipsec_phase1_bytes_inis nowopnsense_ipsec_phase1_bytes_in_total), andopnsense_vnstat_total_bytesis nowopnsense_vnstat_bytes_total. All nine were always emitted as Prometheus counters, and OTLP-to-Prometheus canonicalization appends_totalto every monotonic sum regardless of the Go-declared name, so the unsuffixed names would disagree with what the supported OTLP-fed live backend exports as soon as the series is populated (an IPsec tunnel exists, or vnStat is enabled) - the same convention violation #418 fixed for the firewall pf pass/block descriptors, closed here before it could bite in production. Update any custom dashboard, recording rule, or alert that referenced the unsuffixed names; the bundled Grafana dashboard is already updated.vnstat_total_bytesbecomesvnstat_bytes_totalrather than a mechanical..._total_bytes_total: "total" here has always meant "cumulative since vnstat's database was created," never "rx+tx combined," and thedirectionlabel carrying rx/tx is untouched by this rename. -
Dashboard feature detection and log panels are now scoped to the selected instance - the hidden feature sentinels that drive conditional rendering, and every Loki panel, previously searched the whole datasource. In a multi-firewall stack that made navigation lie: selecting appliance A could expose a tab because appliance B had the feature, and raw-log and top-talker panels could show another firewall's records. Prometheus sentinels now filter
opnsense_instance, Loki queries filterservice_instance_id, and runtime metrics with no appliance label join toopnsense_up. Three visible consequences after regenerating the dashboard: tabs for features the selected appliance lacks now correctly disappear; the Traffic Shaper and Captive Portal tabs now appear when the feature is deployed but idle, where a zero-count test previously hid them; and the NetFlow receiver rows are now gated on the exporter's own receiver metric rather than OPNsense's netflow-export setting, so they hide on a box that exports netflow without this exporter receiving it.
Upgrading to v2.0 from v1.x¶
- SMART collector is now opt-in - the
opnsense_smart_*metrics are no longer emitted by default. Set--exporter.enable-smart(envOPN2OTEL_ENABLE_SMART=true) to restore them. Querying SMART data is one of the more expensive per-scrape calls, so it now has to be requested explicitly. - ARP/NDP per-entry series are opt-in - the per-entry
opnsense_arp_table_entriesandopnsense_ndp_entriesseries (one series per host, high cardinality) are no longer emitted by default. Set--exporter.enable-arp-details/--exporter.enable-ndp-detailsto restore them. Otherwise switch dashboards and alerts to the newopnsense_arp_table_entries_total/opnsense_ndp_entries_totalaggregate gauges, which are always emitted. opnsense_firewall_interface_hits_totalrenamed and re-typed - it is nowopnsense_firewall_interface_log_entries_recentand is a gauge, not a counter. It reflects the current count of recent log entries, so it no longer makes sense to wrap inrate()/increase()- plot the gauge directly. The bundled Grafana dashboard has already been updated.- Default instance label changed - when
--exporter.instance-labelis unset, theinstancelabel now defaults to the configured OPNsense address (deterministic across restarts) rather than the hostname reported by the API. To keep the old hostname-derived behaviour, set--exporter.instance-use-hostname; to pin an explicit value, set--exporter.instance-label. If you relied on the old default, existing series will change theirinstancelabel after the upgrade. - Portable Prometheus alert rules removed -
grafana/alerts/opnsense.rules.yamlno longer ships. If you were loading that file into Prometheus, Mimir, or the Grafana Cloud ruler, migrate to the Grafana-managed alert manifests undergrafana/alerts/grafana-managed/(pushed as Grafana resources). See Integration & Dashboards. - Unknown link state no longer reported as down - interfaces whose link state the API reports as unknown (e.g. some PPPoE WANs) are now distinguished from interfaces that are actually down instead of being flattened to down. Alerts that treated "not up" as "down" may fire differently; check any rules built on interface link-state metrics.
Upgrading to v1.0 from v0.x¶
- IPsec SPI labels removed - phase-2 metrics no longer carry
spi_in/spi_outlabels (remaining:description,name,phase1_name). SPIs rotate on every rekey, so the labels caused unbounded series churn. Update any PromQL that referenced them. - OpenVPN per-session metrics are opt-in - the per-session
opnsense_openvpn_sessionsseries (username and tunnel-address labels) is only emitted with--exporter.enable-openvpn-details. The aggregateopnsense_openvpn_sessions_totalandopnsense_openvpn_sessions_by_instanceseries are always emitted. Set the flag to restore the old behaviour. - WireGuard handshake metric type -
opnsense_wireguard_peer_last_handshake_secondschanged from counter to gauge (it is a Unix timestamp). Replacerate(opnsense_wireguard_peer_last_handshake_seconds[...])with the purpose-builtopnsense_wireguard_peer_handshake_age_secondsgauge. opnsense_upsemantics -opnsense_upno longer flips to 0 for a box that is reachable but self-reports as degraded (e.g. a leftover crash report). Such a box now trips the warning-levelOPNsenseCrashReports/OPNsenseFirewallUnhealthyalerts instead of the criticalOPNsenseExporterDown. If you alerted onopnsense_up == 0for these cases, switch to those signals.
Migrating from upstream (AthennaMind/opnsense-exporter)¶
In addition to the items above:
- Image and module path - pull
ghcr.io/rknightion/opnsense2otel; the Go module isgithub.com/rknightion/opnsense2otel/v4. --runtime.gomaxprocsremoved - Go now auto-detects CPUs; delete the flag from any unit files or manifests./debug/pprof/*endpoints removed - replaced by optional authenticated push profiling via--pyroscope.*flags. See Configuration.- Firmware metrics reworked - version strings consolidated into
opnsense_firmware_info(labels) plus numeric gauges (needs_reboot,upgrade_needs_reboot,last_check_timestamp_seconds,new_packages_count,upgrade_packages_count). --exporter.instance-labelnow optional - when left empty it defaults to the configured OPNsense address (see the v2.0 note above for the change from the old hostname default; set--exporter.instance-use-hostnamefor hostname-derived labels).- Many new collectors are enabled by default - review the collector switches and disable what you don't need.