Duration of the latest scheduled sub-collector poll in seconds. The metric name retains its historical scrape_collector prefix for compatibility
opnsense_exporter_scrape_collector_success
Gauge
collector
Whether the latest scheduled sub-collector poll succeeded (1 = ok, 0 = error or panic). The metric name retains its historical scrape_collector prefix for compatibility
opnsense_exporter_collector_poll_interval_seconds
Gauge
collector
Configured poll interval of a collector in seconds (the internal poll scheduler runs each collector on its own interval; #336)
Unix timestamp of a collector's last poll ATTEMPT, successful or not; scheduler liveness, not data freshness — a collector failing every poll keeps advancing this while replaying old data. Use collector_snapshot_timestamp_seconds for data age. Absent until the collector has polled at least once (#336, #382)
Unix timestamp of a collector's next scheduled poll, read from the scheduler's actual fixed-cadence deadline (not derived from last poll + interval); absent when no poller is running for the collector (#336, #385)
Unix timestamp at which a collector's stored metric buffer was last REPLACED — the true age of the data a scrape replays. Advances on a successful poll and on a partial-error poll that still emitted data; does NOT advance when a failed poll emitted nothing and the last-good buffer was retained. Absent until the collector has stored data at least once (#382)
Unix timestamp of a collector's last fully successful poll. Unlike collector_snapshot_timestamp_seconds this does NOT advance on a partial-error poll, so the two together distinguish 'refreshed but degraded' from 'fully healthy'. Absent until the collector has succeeded at least once (#382)
opnsense_exporter_series_total
Gauge
---
Total number of Prometheus series produced by the COLLECTOR registry on the most recent real /metrics scrape or OTLP export (#494) — the same set --exporter.series-budget is compared against, and what metricsnap replays to the web UI's /cardinality report. Self-metrics on the separate self registry (process_/go_, the opnsense_exporter_otlp_* delivery-health family, and this gauge itself) are NOT included, so this reads lower than a full scrape's total series count; see --exporter.series-budget's flag help for the same caveat. This gauge is itself exactly one series, so it cannot meaningfully move the number it reports. Reads 0 before the first real scrape/export has completed.
opnsense_up
Gauge
---
Whether the OPNsense API was reachable on the last health poll (1 = reachable, 0 = unreachable), updated on --collector.poll-interval independently of scrapes. A reachable box reporting a degraded subsystem stays 1; see opnsense_system_status_code and the per-subsystem status metrics.
opnsense_firewall_status
Gauge
---
Status of the firewall reported by the system health check (1 = ok, 0 = errors)
opnsense_crash_reporter_status
Gauge
---
Status of the crash reporter reported by the system health check (1 = ok/no crash reports, 0 = crash reports present)
opnsense_system_status_code
Gauge
---
Numeric OPNsense system status code from the health check (2 = OK, 1 = NOTICE, 0 = WARNING, -1 = ERROR; OPNsense >= 25.1)
opnsense_system_subsystem_status_code
Gauge
subsystem
Numeric OPNsense SystemStatusCode (2 = OK, 1 = NOTICE, 0 = WARNING, -1 = ERROR) for every health-check subsystem present in the response, by subsystem short name (e.g. diskspace, rootlock, crashreporter, firewall, plus any plugin-contributed key). OPNsense omits healthy subsystems from the report, so a subsystem's series is present only while it is unhealthy; absence should be read as healthy, the same convention as opnsense_firewall_status and opnsense_crash_reporter_status.
opnsense_exporter_scrapes_total
Counter
---
Total number of times this exporter served a /metrics scrape. Since #336 a scrape replays the in-memory poll snapshot and makes no OPNsense API call, so this counts SERVING, not collection: it tracks how often Prometheus asked, never how often the firewall was polled. For polling use opnsense_exporter_collector_last_poll_timestamp_seconds and opnsense_exporter_api_requests_total.
opnsense_exporter_endpoint_errors_total
Counter
endpoint
Total number of errors by endpoint returned by the OPNsense API during data fetching. The endpoint label is an api/* path for normal fetch errors; a recovered collector panic uses a 'panic:' sentinel value instead.
opnsense_exporter_api_requests_total
Counter
endpoint, code
Total number of OPNsense API requests made, by endpoint (api/* path) and HTTP response code (0 = no response, e.g. network error or context cancellation). Provides the denominator for a per-endpoint error rate alongside opnsense_exporter_endpoint_errors_total.
opnsense_exporter_api_request_duration_seconds
Histogram
endpoint
Duration of individual OPNsense API requests in seconds, by endpoint (api/* path). Lets operators see which underlying endpoint call regressed when a collector's scheduled poll duration spikes.
opnsense_exporter_api_cache_hits_total
Counter
endpoint, kind
Total number of OPNsense API calls served from the response cache instead of the firewall, by endpoint (api/* path) and kind. kind=\"body\" is a replayed payload from a slow-moving endpoint (--exporter.cache-ttl / --exporter.firmware-cache-ttl); kind=\"absent\" is a replayed 404 from a plugin-gated endpoint, meaning the plugin is not installed. Only endpoints with a configured TTL are counted, so this and opnsense_exporter_api_cache_misses_total form a hit rate for the cache itself.
opnsense_exporter_api_cache_misses_total
Counter
endpoint
Total number of OPNsense API calls that went to the firewall and populated the response cache - a cold cache or an expired TTL. This is the denominator for a cache hit rate alongside opnsense_exporter_api_cache_hits_total. A call whose response was never cacheable is NOT counted: notably a 200 from a plugin-gated endpoint whose plugin IS installed, whose live payload is fetched on every scheduled poll by design (only its 404 would be cached).
One series per ARP table entry (value is always 1). Only emitted when --exporter.enable-arp-details is set (high, churning cardinality). manufacturer is the OUI lookup for the MAC and is the label that actually identifies a device: hostname is empty on every entry on the reference box and is frequently empty upstream in general, so do not build a panel that depends on it. device is the raw kernel device and interface_description the assigned name; they diverge on VLAN children and bridges, and only device joins against the interfaces metrics.
ZFS ARC size by component (MFU, MRU, anonymous, header, other), from top's ARC header. MFU versus MRU is the standard read on whether the cache is serving a working set or thrashing. Absent entirely on a non-ZFS install. The ARC total is opnsense_system_memory_arc_bytes.
--exporter.disable-activity
opnsense_activity_arc_compressed_bytes
Gauge
---
Size of ZFS ARC contents as held in memory, compressed. Divide the uncompressed figure by this to get the compression ratio at query time.
--exporter.disable-activity
opnsense_activity_arc_uncompressed_bytes
Gauge
---
Logical size of ZFS ARC contents before compression. Absent when top reports no compression line.
--exporter.disable-activity
opnsense_activity_user_cpu_percent
Gauge
user
Weighted CPU percentage summed across every thread owned by this username, from top's WCPU column. Sums per thread, so a busy multi-threaded process can exceed 100.
--exporter.disable-activity
opnsense_activity_user_memory_bytes
Gauge
user
Resident memory summed across the processes owned by this username, from top's RES column. Counted once per PROCESS: top prints one row per thread and every thread repeats its process's RES, so the rows are deduplicated by PID before summing.
--exporter.disable-activity
opnsense_activity_command_cpu_percent
Gauge
command
Weighted CPU percentage summed across every thread of this command, from top's WCPU column. The command name is normalised: the {thread-name} suffix and [] kernel brackets are stripped.
--exporter.disable-activity
opnsense_activity_command_memory_bytes
Gauge
command
Resident memory summed across the processes running this command, from top's RES column. Counted once per PROCESS, deduplicated by PID — every thread row repeats its process's RES, so summing the rows would multiply a process's memory by its thread count.
--exporter.disable-activity
opnsense_activity_command_threads
Gauge
command
Number of threads running this command. The one signal here that no other exported metric carries: a process leaking threads is invisible everywhere else.
--exporter.disable-activity
opnsense_activity_commands_tracked
Gauge
---
Number of distinct command labels in the process aggregates this poll, against a cap of 128. At the cap the label set has saturated and new commands are invisible except through the other bucket.
--exporter.disable-activity
opnsense_activity_commands_capped_total
Counter
---
Cumulative count of process-table rows folded into command=\"other\" because the command label set was already at its cap. Zero on any normal firewall; a rising rate means the aggregates are no longer naming everything they measure.
Total number of active BPF listeners (raw entry count before aggregation)
--exporter.disable-bpf
opnsense_bpf_received_packets_total
Counter
process, interface
Cumulative packets received by BPF listeners for this process/interface pair
--exporter.disable-bpf
opnsense_bpf_dropped_packets_total
Counter
process, interface
Cumulative packets dropped by BPF listeners for this process/interface pair
--exporter.disable-bpf
opnsense_bpf_matched_packets_total
Counter
process, interface
Cumulative packets matched by BPF filter for this process/interface pair
--exporter.disable-bpf
opnsense_bpf_store_buffer_bytes
Gauge
process, interface
Current store buffer length in bytes for this process/interface pair
--exporter.disable-bpf
opnsense_bpf_hold_buffer_bytes
Gauge
process, interface
Current hold buffer length in bytes for this process/interface pair
--exporter.disable-bpf
opnsense_bpf_direction_listeners
Gauge
process, interface, direction
Number of open BPF descriptors for this process/interface/direction. A process holding one descriptor per port (lldpd does) shows one series per port here, which is normal; a count that climbs without the process count changing is a descriptor leak.
--exporter.disable-bpf
opnsense_bpf_direction_received_packets_total
Counter
process, interface, direction
Cumulative packets received by BPF listeners for this process/interface/direction. Per-direction breakdown of bpf_received_packets_total, which remains the sum across directions.
--exporter.disable-bpf
opnsense_bpf_direction_dropped_packets_total
Counter
process, interface, direction
Cumulative packets dropped by BPF listeners for this process/interface/direction — the consumer could not read the buffer fast enough, so the capture is incomplete. Per-direction breakdown of bpf_dropped_packets_total.
--exporter.disable-bpf
opnsense_bpf_direction_matched_packets_total
Counter
process, interface, direction
Cumulative packets matched by the BPF filter for this process/interface/direction. Per-direction breakdown of bpf_matched_packets_total.
Cumulative CPU seconds by mode, reconstructed from the api/diagnostics/cpu_usage SSE stream as (percent/100 * measured elapsed time between frames). This is a reconstruction from integer-percentage samples, NOT a kernel tick counter: each sample carries up to ±0.5% quantisation noise, which is unbiased and so does not accumulate systematically. Absent while the stream has been silent for longer than its grace window, because a frozen counter is indistinguishable from an idle CPU. NOTE the difference from node_exporter's identically-named metric: OPNsense reports CPU AGGREGATED ACROSS ALL CORES, not per core, so there is no cpu label and sum(rate(...)) by mode is 1, not the core count. Multiply a rate by 100 to read it as a percentage of the whole machine.
--exporter.disable-cpu
opnsense_cpu_stream_up
Gauge
---
1 when the CPU usage SSE stream is connected, 0 otherwise. Always exported, including while seconds_total is absent, so a stalled stream is visible rather than inferred.
--exporter.disable-cpu
opnsense_cpu_stream_last_frame_age_seconds
Gauge
---
Seconds since the last CPU sample was received. Absent until the first frame ever arrives. This is the signal to alert on: the documented failure mode of this endpoint is that keepalives keep flowing after the data has stopped, so connection liveness alone proves nothing.
--exporter.disable-cpu
opnsense_cpu_stream_reconnects_total
Counter
---
Number of times the CPU usage stream has been re-dialled since exporter start. A steadily climbing value means the connection is being torn down repeatedly.
--exporter.disable-cpu
opnsense_cpu_stream_frames_total
Counter
---
Number of CPU samples accepted from the stream since exporter start.
--exporter.disable-cpu
opnsense_cpu_stream_counters_published
Gauge
---
1 when seconds_total is being published, 0 when it has been withdrawn because the stream went silent for longer than the grace window.
Certificate valid from timestamp in seconds since epoch
--exporter.disable-certificates
opnsense_certificate_valid_to_seconds
Gauge
description, commonname, cert_type, in_use
Certificate valid to (expiry) timestamp in seconds since epoch
--exporter.disable-certificates
opnsense_certificate_info
Gauge
description, commonname, cert_type, in_use
Certificate information (value is always 1)
--exporter.disable-certificates
opnsense_certificate_total
Gauge
---
Total number of certificates
--exporter.disable-certificates
opnsense_certificate_ca_valid_from_seconds
Gauge
description, commonname
Certificate authority valid from timestamp in seconds since epoch
--exporter.disable-certificates
opnsense_certificate_ca_valid_to_seconds
Gauge
description, commonname
Certificate authority valid to (expiry) timestamp in seconds since epoch
--exporter.disable-certificates
opnsense_certificate_ca_total
Gauge
---
Total number of certificate authorities
--exporter.disable-certificates
opnsense_certificate_ca_references
Gauge
description, commonname
Number of other configuration objects that reference this certificate authority (OPNsense's own refcount). This is what separates a CA nearing expiry that 50 things depend on - an outage on a known date - from one nothing uses, which is dead config; the validity gauges alone cannot tell them apart. 0 is a real, meaningful value; a CA whose payload carries no refcount at all emits no series.
Unix timestamp of when a ClamAV signature database was built, from the freshclam build metadata, by database. Absent when the build-date fragment could not be parsed.
Per-lease information (value is expire timestamp). Only emitted when --exporter.enable-dnsmasq-details is set. device is the raw logical interface id and interface the assigned description; they diverge on VLAN children and bridges, and only device joins against the interfaces metrics (the #544 item-5 pattern). vendor is the OUI vendor-name lookup (mirroring the Kea collector's identical field), empty whenever the OUI is unknown.
--exporter.disable-dnsmasq
opnsense_dnsmasq_service_running
Gauge
---
Whether the service is running (1 = running, 0 = stopped/disabled)
--exporter.disable-dnsmasq
opnsense_dnsmasq_pool_size
Gauge
interface
Number of addresses in the configured dnsmasq DHCP ranges per interface
Cumulative number of times this BGP peer session has been established (flap counter)
--exporter.disable-frr
opnsense_frr_bgp_peer_connections_dropped_total
Counter
peer
Cumulative number of times this BGP peer session has been dropped (flap counter)
--exporter.disable-frr
opnsense_frr_bgp_peer_messages_by_type_total
Counter
peer, type, direction
Cumulative BGP messages exchanged with this peer, by message type and direction
--exporter.disable-frr
opnsense_frr_bgp_peer_last_reset_seconds
Gauge
peer
Time since this BGP peer session was last reset, in seconds. The reset reason (FRR's lastResetDueTo) is logged at debug level rather than carried as a label (unbounded free-text value).
--exporter.disable-frr
opnsense_frr_bgp_peer_prefixes_accepted
Gauge
peer, af
Number of prefixes accepted from this BGP peer after inbound policy, by address family (complements the pre-policy opnsense_frr_bgp_peer_prefixes_received)
--exporter.disable-frr
opnsense_frr_bgp_peer_queue_depth
Gauge
peer, direction
Current BGP work-queue depth for this peer, by direction (in = inbound, out = outbound)
--exporter.disable-frr
opnsense_frr_ospf_neighbors_total
Gauge
---
Total number of OSPF neighbors
--exporter.disable-frr
opnsense_frr_ospf_neighbor_adjacency
Gauge
neighbor_id, address, interface
Whether this OSPF neighbor is in Full adjacency state (1 = Full, 0 = otherwise)
--exporter.disable-frr
opnsense_frr_ospf_neighbor_nsm_state_info
Gauge
neighbor_id, address, interface, nsm_state
FRR's raw OSPF neighbor state machine state for this neighbor (value is always 1; use the nsm_state label — DependUpon/Deleted/Down/Attempt/Init/2-Way/ExStart/Exchange/Loading/Full). Distinct from ospf_neighbor_adjacency: that flag only shows Full vs. not, this shows what a non-Full neighbor is stuck at.
--exporter.disable-frr
opnsense_frr_ospf_neighbor_uptime_seconds
Gauge
neighbor_id, address, interface
How long this OSPF neighbor's adjacency has been progressing, in seconds. Absent (not zero) while the neighbor's inactivity timer isn't scheduled — FRR doesn't report this field for a neighbor that hasn't started forming an adjacency.
--exporter.disable-frr
opnsense_frr_ospf_neighbor_dead_timer_seconds
Gauge
neighbor_id, address, interface
Time remaining before this OSPF neighbor's dead interval expires, in seconds. A value repeatedly resetting close to the configured dead interval is healthy; one trending toward zero and recovering (rather than resetting cleanly on a fresh hello) is a flapping adjacency. Absent (not zero) when FRR reports the timer as \"inactive\" (no dead-interval timer currently running for this neighbor).
--exporter.disable-frr
opnsense_frr_ospf_neighbor_lsa_queue_depth
Gauge
neighbor_id, address, interface, queue
Current LSA synchronization queue depth for this OSPF neighbor, by queue (db_summary = database description summary list, ls_request = link-state request list, ls_retransmission = link-state retransmission list). Persistently nonzero ls_retransmission depth is the classic symptom of a neighbor stuck failing to acknowledge LSAs.
--exporter.disable-frr
opnsense_frr_ospf_area_interfaces_active
Gauge
area
Number of active interfaces in this OSPF area
--exporter.disable-frr
opnsense_frr_ospf_area_neighbors_full_adjacent
Gauge
area
Number of neighbors in Full adjacency state in this OSPF area
--exporter.disable-frr
opnsense_frr_ospf_area_lsa_count
Gauge
area
Number of LSAs in this OSPF area
--exporter.disable-frr
opnsense_frr_ospf_area_spf_executed_total
Counter
area
Cumulative number of SPF calculations executed in this OSPF area
Unix timestamp of this OSPF instance's last SPF calculation. Exported as an absolute timestamp rather than a \"seconds ago\" reading (FRR re-derives the age fresh on every poll; an absolute timestamp lets time()-metric compute current age at query time instead of only at last-scrape time). Absent until the instance's first SPF run.
--exporter.disable-frr
opnsense_frr_ospf_spf_last_duration_seconds
Gauge
---
Wall-clock duration of this OSPF instance's last SPF calculation, in seconds. Absent until the instance's first SPF run.
--exporter.disable-frr
opnsense_frr_ospf_interface_up
Gauge
interface, area
Whether this OSPF interface's underlying link is up (1 = up, 0 = down)
--exporter.disable-frr
opnsense_frr_ospf_interface_cost
Gauge
interface, area
OSPF cost configured on this interface
--exporter.disable-frr
opnsense_frr_ospf_interface_neighbors
Gauge
interface, area
Number of OSPF neighbors seen on this interface
--exporter.disable-frr
opnsense_frr_ospf_interface_neighbors_adjacent
Gauge
interface, area
Number of OSPF neighbors in Full adjacency on this interface
--exporter.disable-frr
opnsense_frr_ospf_interface_state
Gauge
interface, state
OSPF interface state (enum-style: 1 for the current state, 0 for the others in the fixed set DR/BDR/DROther/PointToPoint/Waiting/Down)
--exporter.disable-frr
opnsense_frr_ospfv3_interface_up
Gauge
interface, area
Whether this OSPFv3 interface is up (1 = up, 0 = down)
--exporter.disable-frr
opnsense_frr_ospfv3_interface_cost
Gauge
interface, area
OSPFv3 cost configured on this interface
--exporter.disable-frr
opnsense_frr_ospfv3_interface_state
Gauge
interface, state
OSPFv3 interface state (enum-style: 1 for the current state, 0 for the others in the fixed set DR/BDR/DROther/PointToPoint/Waiting/Down). There is no OSPFv3 neighbor endpoint in the quagga plugin API, so interface state is the best available proxy for adjacency.
--exporter.disable-frr
opnsense_frr_ospfv3_area_lsa_count
Gauge
area
Number of LSAs in this OSPFv3 area (v3 parity with opnsense_frr_ospf_area_lsa_count)
--exporter.disable-frr
opnsense_frr_ospfv3_interface_pending_lsa
Gauge
interface, queue
OSPFv3 flooding backlog on this interface: pending LSAs by queue (update = LSUpdate, ack = LSAck)
--exporter.disable-frr
opnsense_frr_ospfv3_spf_last_duration_seconds
Gauge
---
Wall-clock duration of this OSPFv3 instance's last SPF calculation, in seconds. Absent until the instance's first SPF run.
Unix timestamp of this OSPFv3 area's last SPF calculation. Exported as an absolute timestamp for the same reason as opnsense_frr_ospf_spf_last_executed_timestamp_seconds above. Absent until this area's first SPF run.
--exporter.disable-frr
opnsense_frr_bfd_peers_total
Gauge
---
Total number of configured BFD peers
--exporter.disable-frr
opnsense_frr_bfd_peer_up
Gauge
peer, interface
Whether this BFD peer session is up (1 = up, 0 = down)
Cumulative BFD control packets received from this peer
--exporter.disable-frr
opnsense_frr_bfd_peer_control_packets_sent_total
Counter
peer
Cumulative BFD control packets sent to this peer
--exporter.disable-frr
opnsense_frr_bfd_peer_session_up_events_total
Counter
peer
Cumulative BFD session-up events for this peer
--exporter.disable-frr
opnsense_frr_bfd_peer_session_down_events_total
Counter
peer
Cumulative BFD session-down events for this peer
--exporter.disable-frr
opnsense_frr_bfd_peer_diagnostic_info
Gauge
peer, diagnostic, remote_diagnostic
FRR BFD peer diagnostic reason (value is always 1; use labels). diagnostic/remote_diagnostic are FRR's diag2str() enum: ok, control detection time expired, echo function failed, neighbor signaled session down, forwarding plane reset, path down, concatenated path down, administratively down, reverse concatenated path down, unknown.
--exporter.disable-frr
opnsense_frr_bfd_peer_rtt_min_microseconds
Gauge
peer
Minimum measured BFD round-trip time for this peer, in microseconds. Reads 0 unless both ends support FRR's BFD RTT extension — a legitimate zero, not a fault.
--exporter.disable-frr
opnsense_frr_bfd_peer_rtt_avg_microseconds
Gauge
peer
Average measured BFD round-trip time for this peer, in microseconds. Reads 0 unless both ends support FRR's BFD RTT extension — a legitimate zero, not a fault.
--exporter.disable-frr
opnsense_frr_bfd_peer_rtt_max_microseconds
Gauge
peer
Maximum measured BFD round-trip time for this peer, in microseconds. Reads 0 unless both ends support FRR's BFD RTT extension — a legitimate zero, not a fault.
--exporter.disable-frr
opnsense_frr_bfd_peer_downtime_seconds
Gauge
peer
Duration this BFD peer session has been down, in seconds. Only emitted while the peer is in the down state — absent (not zero) while up, initializing, or administratively shut down, matching FRR's mutually exclusive uptime/downtime fields.
--exporter.disable-frr
opnsense_frr_route_count
Gauge
af, protocol
Number of distinct routed prefixes in the zebra RIB, by address family and protocol. Only emitted when --exporter.enable-frr-routes is set.
--exporter.disable-frr
opnsense_frr_route_nexthop_count
Gauge
af, protocol
Number of nexthop rows in the zebra RIB (ECMP width), by address family and protocol. Only emitted when --exporter.enable-frr-routes is set.
--exporter.disable-frr
opnsense_frr_ospf_route_count
Gauge
type
Number of rows in the OSPF route table, by route type. Only emitted when --exporter.enable-frr-routes is set.
--exporter.disable-frr
opnsense_frr_ospfv3_route_count
Gauge
type
Number of rows in the OSPFv3 route table, by route type. Only emitted when --exporter.enable-frr-routes is set.
--exporter.disable-frr
opnsense_frr_ospf_lsa_count
Gauge
area, lsa_type
Number of LSAs in the OSPF LSDB, by area and LSA type. Only emitted when --exporter.enable-frr-routes is set.
--exporter.disable-frr
opnsense_frr_ospfv3_lsa_count
Gauge
scope
Number of LSAs in the OSPFv3 LSDB, by flooding scope. Only emitted when --exporter.enable-frr-routes is set.
Whether a plugin-gated OPNsense feature is installed on this firewall: 1 if its API endpoint answered, 0 if it returned 404 (the plugin is absent). The series is present for every plugin-gated collector family, so a 0 means 'this box does not have it' and ABSENCE means only that availability has never been determined - an unreachable firewall leaves the previous verdict in place rather than reporting everything as gone. enabled reflects whether that collector is currently switched on, so available=1 with enabled=\"false\" is the actionable state: a plugin you have but are not scraping, whether it is an opt-in collector or a default-on one you disabled. Refreshed on the cold poll tier (15m) independent of --exporter.cache-ttl, and a collector that is already enabled is not re-probed - its own polling answers the same question (#517, #525).
The number of IPv4 incoming packets that were allowed to pass through the firewall by interface
--exporter.disable-firewall
opnsense_firewall_out_ipv4_pass_packets_total
Counter
interface
The number of IPv4 outgoing packets that were allowed to pass through the firewall by interface
--exporter.disable-firewall
opnsense_firewall_in_ipv4_block_packets_total
Counter
interface
The number of IPv4 incoming packets that were blocked by the firewall by interface
--exporter.disable-firewall
opnsense_firewall_out_ipv4_block_packets_total
Counter
interface
The number of IPv4 outgoing packets that were blocked by the firewall by interface
--exporter.disable-firewall
opnsense_firewall_in_ipv6_pass_packets_total
Counter
interface
The number of IPv6 incoming packets that were allowed to pass through the firewall by interface
--exporter.disable-firewall
opnsense_firewall_out_ipv6_pass_packets_total
Counter
interface
The number of IPv6 outgoing packets that were allowed to pass through the firewall by interface
--exporter.disable-firewall
opnsense_firewall_in_ipv6_block_packets_total
Counter
interface
The number of IPv6 incoming packets that were blocked by the firewall by interface
--exporter.disable-firewall
opnsense_firewall_out_ipv6_block_packets_total
Counter
interface
The number of IPv6 outgoing packets that were blocked by the firewall by interface
--exporter.disable-firewall
opnsense_firewall_in_ipv4_pass_bytes_total
Counter
interface
The number of IPv4 incoming bytes that were allowed to pass through the firewall by interface
--exporter.disable-firewall
opnsense_firewall_out_ipv4_pass_bytes_total
Counter
interface
The number of IPv4 outgoing bytes that were allowed to pass through the firewall by interface
--exporter.disable-firewall
opnsense_firewall_in_ipv4_block_bytes_total
Counter
interface
The number of IPv4 incoming bytes that were blocked by the firewall by interface
--exporter.disable-firewall
opnsense_firewall_out_ipv4_block_bytes_total
Counter
interface
The number of IPv4 outgoing bytes that were blocked by the firewall by interface
--exporter.disable-firewall
opnsense_firewall_in_ipv6_pass_bytes_total
Counter
interface
The number of IPv6 incoming bytes that were allowed to pass through the firewall by interface
--exporter.disable-firewall
opnsense_firewall_out_ipv6_pass_bytes_total
Counter
interface
The number of IPv6 outgoing bytes that were allowed to pass through the firewall by interface
--exporter.disable-firewall
opnsense_firewall_in_ipv6_block_bytes_total
Counter
interface
The number of IPv6 incoming bytes that were blocked by the firewall by interface
--exporter.disable-firewall
opnsense_firewall_out_ipv6_block_bytes_total
Counter
interface
The number of IPv6 outgoing bytes that were blocked by the firewall by interface
--exporter.disable-firewall
opnsense_firewall_pf_states_current
Gauge
---
Current number of active PF states
--exporter.disable-firewall
opnsense_firewall_pf_states_limit
Gauge
---
Maximum number of PF states allowed
--exporter.disable-firewall
opnsense_firewall_pf_interface_references
Gauge
interface, skipped
Number of PF state-table entries currently referencing each interface — the per-interface breakdown of the global pf_states_current. A live depth, not a cumulative counter: it rises and falls with connection count, so never wrap it in rate()/increase(). skipped=\"true\" means pf \"skip on interface\" is enabled for that device, so pf is not filtering it at all — the reference count is still real, but no rules are being evaluated. The pfctl \" (skip)\" name suffix is stripped out of the interface label so toggling that option does not rename the series. The pfctl \"all\" aggregate row is deliberately NOT emitted: it is the sum of every other row, so including it would double every total in any panel that aggregates this family — use sum() in PromQL instead. Do not add it back.
--exporter.disable-firewall
opnsense_firewall_interface_log_entries_recent
Gauge
interface
Firewall log entries per interface within the most recent ~5000-record log window (sliding, not a counter; interface=\"other\" is an aggregate of interfaces beyond the top 10)
--exporter.disable-firewall
opnsense_firewall_geoip_alias_usages
Gauge
---
Number of configured firewall aliases of type GeoIP, regardless of whether the GeoIP database itself has ever downloaded
--exporter.disable-firewall
opnsense_firewall_geoip_addresses
Gauge
---
Number of GeoIP addresses/networks currently loaded from the downloaded database (0 until a MaxMind/ipinfo key is configured and a download has succeeded)
--exporter.disable-firewall
opnsense_firewall_geoip_files
Gauge
---
Number of per-country GeoIP alias table files currently written (0 until a MaxMind/ipinfo key is configured and a download has succeeded)
Unix timestamp of the last successful GeoIP database download. Absent until the first successful download; compare against time() to alert on a stale/failed GeoIP database (e.g. an expired MaxMind license key).
Unix timestamp (UTC) of pf's last counters reset for this interface — pfctl's own \"Cleared\" marker, the same instant a pfctl -z or a filter/rule reload zeroed the pass/block packet and byte counters above for that interface. OPNsense reports this timestamp with no timezone marker at all, so it is decoded assuming UTC and can be off by the firewall's real UTC offset — treat this as \"a reset happened\" (a step change, or a value newer than your rate() window), not a to-the-minute clock. A rate()/increase() query spanning a reset already reads a bogus negative delta or spurious plateau on the pass/block counters above; use this timestamp to explain that away rather than chase it as a real traffic drop. Absent entirely when the box has never reported a parseable value for this interface.
--exporter.disable-firewall
opnsense_firewall_nat_rules
Gauge
type, enabled
Number of MVC-managed NAT rules by type and enabled state (source_nat, d_nat, one_to_one, npt). Rules created before an admin migrated to the MVC-managed NAT backend are not counted.
Number of pf table-entries slots currently in use (global)
--exporter.disable-alias
opnsense_alias_table_entries_limit
Gauge
---
Maximum number of pf table-entries slots (global)
--exporter.disable-alias
opnsense_alias_table_evaluations_total
Counter
table, result
Packet evaluations against this alias table since last reset
--exporter.disable-alias
opnsense_alias_table_packets_total
Counter
table, direction, action
Packets matched against this alias table since last reset
--exporter.disable-alias
opnsense_alias_table_bytes_total
Counter
table, direction, action
Bytes matched against this alias table since last reset
--exporter.disable-alias
opnsense_alias_table_updated_timestamp_seconds
Gauge
table
Unix timestamp of the last time this alias table's persisted content was written - i.e. when a DNS- or URL-backed alias (a threat feed, say) last refreshed. A feed that silently stops refreshing is a security control failing open, and no other metric can see it: the table still holds its stale rows, so table_entries looks healthy. Only emitted for tables that HAVE persisted content; a static host/network alias has no refresh cycle and emits no series rather than a misleading epoch 0. Derived from the file mtime as a timezone-less local timestamp and read as UTC, so the absolute value can be off by the firewall's UTC offset - compare ages, not wall clocks.
Whether applying the currently AVAILABLE update would require a reboot, or a plugin has set the reboot-required hook (1 = yes, 0 = no). This tracks update availability, NOT a completed-but-unapplied install - it can be 1 for days before anything is installed and is not cleared by rebooting. For the major-version-upgrade reboot signal see upgrade_needs_reboot.
--exporter.disable-firmware
opnsense_firmware_upgrade_needs_reboot
Gauge
---
Whether a pending major-version upgrade (product.product_check) requires a reboot to apply (1 = yes, 0 = no). Distinct from needs_reboot, which tracks base/kernel/plugin update availability.
--exporter.disable-firmware
opnsense_firmware_last_check_timestamp_seconds
Gauge
---
Unix timestamp of the last firmware update check
--exporter.disable-firmware
opnsense_firmware_new_packages_count
Gauge
---
Number of new packages available
--exporter.disable-firmware
opnsense_firmware_upgrade_packages_count
Gauge
---
Number of packages with available upgrades
--exporter.disable-firmware
opnsense_firmware_downgrade_packages_count
Gauge
---
Number of packages available to downgrade
--exporter.disable-firmware
opnsense_firmware_reinstall_packages_count
Gauge
---
Number of packages available to reinstall
--exporter.disable-firmware
opnsense_firmware_remove_packages_count
Gauge
---
Number of packages the pending update would remove
--exporter.disable-firmware
opnsense_firmware_upgrade_sets_count
Gauge
---
Number of pending upgrade sets (the synthetic base/kernel entries of a major or point upgrade, not ordinary packages)
--exporter.disable-firmware
opnsense_firmware_update_check_success
Gauge
---
Whether the firewall's stored update check actually succeeded (1 = the repository was reachable, authenticated and verified; 0 = it was not). Only emitted once a check has been stored. This is NOT the same as \"no updates pending\": before this metric existed, a DNS failure, expired subscription, revoked fingerprint or unavailable release train looked exactly like a healthy check with zero updates. Reflects the STORED result of the box's own check (refreshed roughly daily) as seen through the exporter's firmware response cache, so a state change can take up to --exporter.firmware-cache-ttl (default 12h) to appear.
--exporter.disable-firmware
opnsense_firmware_update_check_state
Gauge
component, state
Current state of one component of the firewall's stored update check (always 1; exactly one series per component). component is connection or repository. state is drawn from OPNsense's closed vocabularies - connection: error/unauthenticated/misconfigured/unresolved/ok, repository: error/untrusted/unsigned/revoked/incomplete/forbidden/ok - and anything else, including a future upstream state, collapses to unknown. Only emitted once a check has been stored.
--exporter.disable-firmware
opnsense_firmware_pending_download_bytes
Gauge
---
Total size in bytes the pending update would download, parsed from the stored check's mixed-unit download_size list (base-2 units). Only emitted once a check has been stored AND the field parsed unambiguously - a value that cannot be parsed emits no series rather than a fabricated 0. Unlike the OPNsense GUI, which truncates a fractional size, fractions are kept, so this can read slightly higher than the number the GUI displays.
--exporter.disable-firmware
opnsense_firmware_package_update_available
Gauge
name, installed_version, new_version
Pending package update (1 = update available). Only emitted when --exporter.enable-firmware-package-details is set.
--exporter.disable-firmware
opnsense_firmware_plugin_installed
Gauge
name, version
Installed OPNsense plugin (1 = installed). Only emitted when --exporter.enable-firmware-package-details is set.
--exporter.disable-firmware
opnsense_firmware_major_upgrade_available
Gauge
---
Whether a MAJOR release upgrade is on offer (1 = yes), e.g. 26.1 to 26.7. This is a different maintenance decision from the package updates upgrade_packages_count tracks - a scheduled-window job, not something to apply mid-afternoon - and upgrade_needs_reboot describes THIS upgrade, not the package ones. Only emitted once the box has a stored update check; before that there is nothing to report and a 0 would claim 'no major upgrade pending' on a firewall that has never looked.
--exporter.disable-firmware
opnsense_firmware_major_upgrade_info
Gauge
version
The release a pending major upgrade would move this firewall to (always 1; read the version label). Emitted ONLY while such an upgrade is on offer, so the series appearing is itself the signal and there is never a stale version=\"\" series hanging around. Cardinality is one series, changing at most once or twice a year.
--exporter.disable-firmware
opnsense_firmware_plugin_size_bytes
Gauge
name, version
Installed size of this OPNsense plugin, in bytes. Only emitted when --exporter.enable-firmware-package-details is set. APPROXIMATE: OPNsense reports the size as an already-humanised string with one decimal place (pkg's %sh, then formatBytes), so this is that display value converted to base-2 bytes, not the exact on-disk size - good for attributing disk pressure between plugins, not for accounting. A plugin whose size upstream could not report emits no series rather than 0.
--exporter.disable-firmware
opnsense_firmware_plugin_locked
Gauge
name, version
Whether this plugin is pkg-locked against updates (1 = locked). Only emitted when --exporter.enable-firmware-package-details is set. This is the explanation for a plugin that sits at an old version while everything else moves - a locked plugin is skipped by an upgrade rather than failing it.
--exporter.disable-firmware
opnsense_firmware_plugin_automatic
Gauge
name, version
Whether this plugin was installed automatically as a dependency rather than chosen deliberately (1 = automatic). Only emitted when --exporter.enable-firmware-package-details is set.
interface, direction, transport, category, action, source, scope, country
Bytes observed in flow records, by bounded dimension. Keys beyond --flow.top-n fold into other, which preserves the source label, so the family still sums exactly at any limit. A series that leaves the top-N and later returns resumes from the volume it accumulated while folded, so it reads as a counter reset - deliberate, since the alternative is freezing it at its last value forever. From phase 2 this family carries BOTH sources' measurement of the same traffic: pin source= in any query or it double-counts. IPs, ports, hostnames, application names, domains and connection ids are never labels; they stay as structured metadata on the shipped record. The country label is populated by default (#537) and names the REMOTE end of the flow whichever end that is; it reads empty where GeoIP cannot answer, and on a deployment without --geoip.enabled it is empty on every series. Set --flow.geoip.metric-dims=false to drop it entirely.
--exporter.disable-flow
opnsense_flow_packets_total
Counter
interface, direction, transport, category, action, source, scope, country
Packets observed in flow records, by bounded dimension. Same folding, reset and cross-source semantics as opnsense_flow_bytes_total.
--exporter.disable-flow
opnsense_flow_records_total
Counter
interface, direction, transport, category, action, source, scope, country
Flow records observed, by bounded dimension. Counts records, not connections: a Zenarmor conn document is one per connection, but a NetFlow connection produces several records.
--exporter.disable-flow
opnsense_flow_payload_byte_fallback_total
Counter
---
Flow records whose byte count came from Zenarmor's payload counter because its wire counter read zero. Zenarmor only accumulates wire bytes once it has tracked a flow past its first packets, so short UDP flows (DNS, STUN, SSDP) report zero; without the fallback those records would count toward records_total with no bytes at all.
--exporter.disable-flow
opnsense_flow_interface_unresolved_total
Counter
---
Flow records emitted with interface=\"unresolved\" because the interface table had not arrived yet. The source states a kernel device; the DESCRIPTION comes from the enrichment snapshot, which lands on the exporter's own schedule, so a push lane can ingest for minutes before it can label anything. Emitting the raw device instead would invent a SECOND series for an interface that already has one and make every sum by (interface) under-report both (#606). This is a startup artifact and it closes on its own: on the reference box every such record landed with the process less than 300 seconds old, across 7 days and 51 restarts. A rate that continues past that means the interface fetch itself is failing, not that a restart happened.
--exporter.disable-flow
opnsense_flow_rollup_keys
Gauge
---
Distinct label combinations currently tracked by the flow rollup accumulator.
--exporter.disable-flow
opnsense_flow_rollup_keys_max
Gauge
---
Configured ceiling on tracked label combinations (--flow.max-keys); 0 means unbounded. At the ceiling every NEW combination folds into other indefinitely, so compare against opnsense_flow_rollup_keys to see saturation coming.
--exporter.disable-flow
opnsense_flow_rollup_top_n
Gauge
---
Configured ceiling on emitted series (--flow.top-n); 0 means unbounded.
--exporter.disable-flow
opnsense_flow_rollup_keys_folded
Gauge
---
Tracked label combinations currently outside the top-N and therefore folded into other rather than emitted individually.
--exporter.disable-flow
opnsense_flow_rollup_capped_total
Counter
---
Flow records folded into other because the accumulator was already at --flow.max-keys when their label combination first appeared. A rising value means new dimensions are being lost to the cap, not merely folded by the top-N.
--exporter.disable-flow
opnsense_flow_unique_destinations
Gauge
interface
Distinct destination addresses seen per interface - a bounded stand-in for a per-destination series (one gauge per interface, never one per destination). A set, not a sum, so a destination reported by both the NetFlow and Zenarmor lanes counts once. Saturates at an internal per-interface cap; a value pinned at the cap means the true count is at least that high, which is itself a scanning/fan-out signal.
--exporter.disable-flow
opnsense_flow_unique_destinations_capped_total
Counter
---
Observations folded into the other interface bucket because a previously unseen interface label arrived when the distinct-destination tracker was already at its interface budget. The interface and VLAN strings are chosen by the Zenarmor sender, so without an outer bound an admitted sender could mint one map and one series per novel combination (#563). A rising value means interface labels beyond the budget are being merged, so per-interface distinct-destination counts are no longer complete - either the box genuinely has more interfaces than the budget, or a sender is inventing them.
--exporter.disable-flow
opnsense_flow_top_talker_bytes_total
Counter
host, direction
Bytes per internal host and direction, top-N with an other remainder per direction so a sum-by-direction stays exact. OPT-IN behind --flow.top-talkers because the host label is unbounded cardinality the other flow metrics refuse. Counts a single source, so on a box running both lanes a host's bytes are not doubled; it therefore has no source label. A host that leaves and re-enters the top-N reads as a counter reset on that one series.
--exporter.disable-flow
opnsense_flow_source_byte_delta_ratio
Gauge
interface
Histogram of NetFlow-over-Zenarmor byte ratios on merged flow records, by interface - the payoff of correlating the two sources (#346 decision 3). 1.0 is agreement; a value well above 1 means Zenarmor inspected far fewer bytes than crossed the wire, which is a security signal, not an error. Present only where both lanes run and correlate (--flow.log-mode=per_flow); absent otherwise, since there is no disagreement to measure. READ THE DEVIATION CAREFULLY: it is not all source disagreement (#604). NetFlow counts WIRE bytes; Zenarmor falls back to PAYLOAD bytes on roughly half of all flow records (short UDP, where it has not yet accumulated wire bytes), so on that population the histogram is reporting per-packet header overhead - the gap clusters at 28 bytes, the IPv4 IP+UDP header, and the p90 there alone reaches 1.96. The record states its own basis, so a consumer can tell the two apart. Window partials - where this window's NetFlow bytes would be compared against a whole connection's Zenarmor bytes - are EXCLUDED entirely and counted in source_byte_delta_excluded_total, so the impossible sub-1.0 tail is gone rather than explained away. For the underlying security question (is traffic evading inspection) prefer a BYTE-WEIGHTED comparison - the ratio of summed bytes on merged records - over a percentile of per-flow ratios.
--exporter.disable-flow
opnsense_flow_source_byte_delta_excluded_total
Counter
---
Merged flow records deliberately kept OUT of source_byte_delta_ratio because their two sides are not totalling the same thing (#604). Today that is exactly the window-partial case: corrKey buckets by flow-end, so a connection longer than --flow.correlate.window emits one record per window carrying only THAT window's NetFlow bytes, while the single Zenarmor conn document carries the whole connection's counters and merges into one of them. Comparing those reads as \"the firewall counted 75x fewer bytes than crossed the wire\", which is impossible. The records still SHIP with both sides' counters - only the comparison is meaningless - so this is the histogram's coverage gap, not lost volume. On a box with a long active timeout expect a steady non-zero rate; a sudden rise means connections are outliving the correlate window more often.
--exporter.disable-flow
opnsense_flow_dns_cache_entries
Gauge
---
Answers currently held in the DNS answer cache, which gives a flow to a bare IP its dst.domain (§7). Compare against --flow.dns-cache.size to see it approaching the insert cap.
--exporter.disable-flow
opnsense_flow_dns_cache_hits_total
Counter
---
DNS answer-cache lookups that resolved a domain for a flow's destination.
--exporter.disable-flow
opnsense_flow_dns_cache_misses_total
Counter
---
DNS answer-cache lookups with no cached (or a TTL-expired) answer. High against hits is normal for a mostly-IP workload; it is the denominator that tells a cold cache from a thrashing one.
--exporter.disable-flow
opnsense_flow_dns_cache_rejected_total
Counter
---
DNS answers refused insertion because the cache was already at --flow.dns-cache.size. Over the cap it stops inserting rather than evicting hot entries, so a rising value means the cap is binding and domain enrichment is going stale for new answers.
--exporter.disable-flow
opnsense_flow_geoip_lookups_total
Counter
database, result
GeoIP enrichment lookups, by database (\"country\", \"asn\" or \"skipped\") and result. \"hit\" means a record was found; \"miss\" means the loaded database has no record for that address; \"skipped\" (always with database=\"skipped\") counts addresses that never reached a database because they are not globally routable - loopback, RFC 1918, link-local, and carrier-grade NAT, which MaxMind publishes no records for at all. A database that is not loaded contributes neither hits nor misses, so a country hit rate of zero with a non-zero ASN rate means the country database specifically failed to load.
--exporter.disable-flow
opnsense_flow_geoip_reloads_total
Counter
database, result
GeoIP database hot-swaps, by database and result. A \"success\" means a changed file on disk was read and swapped in atomically, which is how an operator's geoipupdate cron or the built-in downloader replaces a database under a running exporter. A \"failure\" means the new file could not be read or parsed, in which case the PREVIOUSLY loaded database keeps serving - a bad update costs freshness, never availability. An unchanged file is not counted at all. Failures are not attributable to one database without threading the path out of the reload, so they carry database=\"skipped\", a value no real database uses.
--exporter.disable-flow
opnsense_flow_geoip_downloads_total
Counter
result
MaxMind database downloads, by result: \"updated\" (a newer build was fetched, verified against its published SHA-256, and installed), \"unmodified\" (the conditional request returned 304 - the healthy steady state, and what keeps a daily updater inside MaxMind's download limit), or \"failure\". Emitted only when --geoip.download.enabled is set; an operator-managed deployment leaves this at zero forever, which is correct.
Unix timestamp of the loaded GeoIP database's build, per database. This is the publisher's BUILD date, not when the file was downloaded, so it is the right thing to alert staleness on: time() - this > 45d catches a refresh that has silently stopped, which the fail-open design makes otherwise invisible. 45d rather than something tighter because it has to cover every database the exporter can load, and the DB-IP Lite copy bundled in the image republishes monthly while GeoLite2 rebuilds twice a week. ABSENT for a database that is not loaded, rather than zero - a zero would read as \"built in 1970\" and fire every staleness alert ever written against it.
--exporter.disable-flow
opnsense_flow_geoip_enriched_records_total
Counter
---
Flow records that gained at least one fact from OUR database. It is the \"is this feature doing anything\" signal, and it is what distinguishes a database that failed to load from a network whose traffic is genuinely all internal - both of which otherwise look like silence, because enrichment is fail-open by design.
--exporter.disable-flow
opnsense_flow_geoip_country_comparisons_total
Counter
result
Flow endpoints where BOTH our database and Zenarmor's answered with a country, by whether they agreed. This is what makes the cost of the ours-wins precedence rule measurable rather than assumed: Zenarmor's database is a commercial GeoIP2-City build (verified on a live firewall - database_type \"GeoIP2-City\", 126 MB against a stock GeoLite2's 63 MB), so overwriting its answer with a free GeoLite2 lookup can genuinely replace a better attribution with a worse one. A rising disagree rate is the signal to look; ours still wins the exported country either way, and the disagreeing value is kept on the log record as <src
dst>.geo.zen_country. Absent entirely without Zenarmor, since there is nothing to compare against.
Correlator merges where the Zenarmor record and the NetFlow record could not be lined up in EITHER orientation, so no geo was folded across (#647). Expected at or near zero: the correlator paired the two records by connection key, so their naming the same two addresses is the premise of the merge rather than a hope. A climbing value means that premise is false and merged records are silently missing Zenarmor's contribution. It is counted because the alternative this replaced was worse and invisible - endpoints were previously paired by POSITION, so a Zenarmor document describing the connection from the initiator's side attached each end's geo to the opposite address, which on a private destination produced a fabricated country carrying a maxmind provenance for an address MaxMind categorically declines to look up.
--exporter.disable-flow
opnsense_flow_correlator_entries
Gauge
---
Connection-windows the correlator is currently holding, waiting for their window to elapse or a Zenarmor conn document to arrive.
--exporter.disable-flow
opnsense_flow_correlator_emitted_total
Counter
---
Flow-log records the correlator has emitted: NetFlow fragments collapsed into one record per connection-window, merged with Zenarmor L7 where a conn document matched.
--exporter.disable-flow
opnsense_flow_correlator_matched_total
Counter
---
Subset of emitted records that carried Zenarmor enrichment (source=merged). Against correlator_emitted_total this is the join hit-rate, which #346 shows is materially lower for long flows whose NetFlow records arrive up to ~30m after the connection ended.
--exporter.disable-flow
opnsense_flow_correlator_evicted_total
Counter
---
Entries force-emitted early because the map hit --flow.correlate.max-entries. A forced emit loses no bytes, but a rising rate means the cap is binding under load and should be raised.
--exporter.disable-flow
opnsense_flow_correlator_expired_total
Counter
---
Entries emitted on the normal window-expiry path (the healthy path, as opposed to eviction).
A second Zenarmor conn document landing for a key that already held one (#590). The second REPLACES the first wholesale rather than merging - L7, verdict, enrichment and geo the first document carried and the second doesn't repeat are gone with no trace. Zero on every deployment where each conversation gets one conn document, which is the common case; a non-zero rate means Zenarmor is re-reporting a connection and only the latest report survives.
A NetFlow fragment whose interface, direction, VLAN or enrichment disagreed with the entry's first fragment OF ITS OWN ORIENTATION (#590, #605). The disagreeing fragment's bytes still count toward the emitted total; only its DIMENSIONS are dropped, silently until this counter. Two exclusions, both because the field differs by design rather than in error: TCPFlags is unioned across fragments (#585), and the conversation's reverse half mirrors interface and direction by construction - that case is counted by correlator_fragment_mirrored_total instead.
--exporter.disable-flow
opnsense_flow_correlator_fragment_mirrored_total
Counter
---
A NetFlow fragment belonging to the conversation's OTHER direction - the reverse half, which shares a correlator key by design because the community id is direction-independent. This is the expected case for any bidirectional flow, not an anomaly, and it is counted rather than merely excluded so the exclusion stays visible: a collapse to zero would mean the two halves stopped sharing a key, which would break correlation itself. Before #605 these were counted as disagreements and were 48.6% of every fragment that counter could examine.
--exporter.disable-flow
opnsense_flow_logs_emitted_total
Counter
---
Flow records shipped to the OTLP log pipeline. Zero when --flow.log-mode=off even though the correlator still runs and its metrics still move.
--exporter.disable-flow
opnsense_flow_logs_truncated_total
Counter
---
Flow log records dropped by the --flow.max-logs-per-window budget. Truncated, never sampled, and counted: a flood on the unauthenticated NetFlow ingress is visible here rather than as a silently thinned stream. Metrics are never truncated.
--exporter.disable-flow
opnsense_flow_logs_dropped_total
Counter
---
Flow log records dropped because the log pipeline was not accepting records - before it started or after shutdown began. Distinct from a budget truncation.
--exporter.disable-flow
opnsense_flow_netflow_datagrams_total
Counter
result
NetFlow datagrams by outcome. result=\"accepted\" passed the peer allowlist; \"peer_rejected\" came from outside --flow.netflow.allowed-peers; \"queue_dropped\" arrived faster than the decoders drained them (the read loop never blocks, because blocking makes the KERNEL drop datagrams where nothing can count them); \"read_error\" is a socket error. Decode outcomes are counted separately below and are a subset of \"accepted\".
--exporter.disable-flow
opnsense_flow_netflow_bytes_received_total
Counter
---
Bytes received on the NetFlow socket, before decoding. This is wire volume of the export itself, NOT the traffic it describes - opnsense_flow_bytes_total is that.
--exporter.disable-flow
opnsense_flow_netflow_records_decoded_total
Counter
---
Flow records successfully decoded out of NetFlow datagrams. The head of the funnel: decoded = emitted + dropped, with records lost before decoding counted as opnsense_flow_netflow_records_dropped_total{reason=\"no_template\"}.
--exporter.disable-flow
opnsense_flow_netflow_records_emitted_total
Counter
---
Decoded records that survived repair and reached the rollup. The tail of the funnel: compare against decoded_total to see what the repair stage removed.
--exporter.disable-flow
opnsense_flow_netflow_records_dropped_total
Counter
reason
Records the NetFlow lane discarded, by reason. \"no_template\" is a data flowset arriving before the template describing it - normal for up to ~2 minutes after either end restarts, since ng_netflow resends templates about every 2 minutes, and a sustained rate means template datagrams are being lost. \"vlan_duplicate\" is the parent-interface copy of a VLAN flow, which ng_netflow captures twice (~4% of bytes on the reference box) and which would otherwise be counted twice AND attributed to the parent. \"no_address\" is a record with unusable endpoints.
--exporter.disable-flow
opnsense_flow_netflow_records_unmapped_total
Counter
---
Records whose ifIndexes resolved to NO interface, so they carry an empty interface label. They are still emitted and still counted in the volume totals - this is not a drop, and it is deliberately not a reason on records_dropped_total. Distinct from opnsense_flow_ifindex_unmapped_total, which counts failed LOOKUPS against a map that exists: this counts RECORDS, and it is the only counter that fires at all while the map is still nil, which is the cold-start window between the receiver starting and the first interface fetch landing. A burst right after a restart is that window and is expected to stop; a sustained rate means the enumeration shifted and --flow.netflow.ifindex-map needs setting. Before #365 this window was completely silent, which put gigabytes into the empty-label bucket with every health metric reading clean.
--exporter.disable-flow
opnsense_flow_netflow_templates_total
Counter
result
NetFlow v9 template events. \"learned\" is a template id seen for the first time; \"replaced\" is a known id re-sent with a DIFFERENT field shape, which invalidates the decoder's understanding of every record behind it. A steady replaced rate means the exporter is flapping between configurations. \"evicted\" is a template dropped because the cache was already at its budget when a new (exporter, source id, template id) arrived: the listener is unauthenticated, so an admitted sender could otherwise grow the cache without bound (#564). Eviction is least-recently-used, and a refresh of an existing key never evicts — so a non-zero rate here means either genuinely more observation domains than the budget allows, or a sender minting novel ids.
--exporter.disable-flow
opnsense_flow_netflow_unexpected_field_total
Counter
field
Records carrying a field the decoder asserts is always empty on this export. field=\"out_bytes\": OUT_BYTES/OUT_PKTS are declared in the template but were zero across all 84,513 records of the reference capture, so they are ignored rather than added to the volume. field=\"src_as\"/\"dst_as\" (#586): ng_netflow hardcodes SRC_AS/DST_AS to zero on every export path under an explicit source comment, so the fields were deleted from the record entirely rather than stored - a better ASN already ships via the GeoIP asn label. A non-zero rate on ANY of these means that assumption has expired and the decoder needs revisiting - it does NOT mean volume is currently wrong.
--exporter.disable-flow
opnsense_flow_netflow_unidentified_total
Counter
kind
Things in the export this decoder could not interpret and stepped over, by kind. kind=\"unknown_field\" is a template element the decoder does not model, counted once per element when a template shape is first learned or CHANGED (never on the ~2-minutely re-send). Every element OPNsense actually sends is modelled, so this reads ZERO on a healthy box and any non-zero value means the export gained something new. It was expected-non-zero until #630, when the four elements the templates had always carried (TOS, both masks, next hop) were modelled rather than stepped over - a permanently non-zero counter and its WARN could never be acted on. kind=\"options_template\" and kind=\"unknown_flowset\" are control flowsets stepped over by length. Stepping over is the correct parse behaviour - doing it silently was not, and --flow.netflow.debug-capture=unidentified keeps the datagrams. The element and flowset IDs themselves are deliberately NOT labels: this arrives on an unauthenticated socket, so they go to the log line and the capture file instead.
--exporter.disable-flow
opnsense_flow_egress_corrected_total
Counter
---
Flow records whose egress interface was corrected from the WAN the FIB lookup named to the WAN the traffic actually left by. ng_netflow derives OUTPUT_SNMP from a route lookup, but OPNsense multi-WAN policy routing happens in pf, which ng_netflow never sees: on the reference capture this mislabelled 3.36 GB of WAN2 traffic as WAN1, a 99% under-report of WAN2. A zero rate on a single-WAN box is expected; a zero rate on a policy-routed multi-WAN box means the correction is not firing and per-WAN volume is wrong.
--exporter.disable-flow
opnsense_flow_policy_route_corrected_total
Counter
---
Flow records whose egress interface was replaced by the device pf's own state table says the traffic left by. This is the PRE-NAT copy of a policy-routed flow - the only copy that can correlate with Zenarmor - which inherits ng_netflow's OUTPUT_SNMP and therefore names the default-route WAN whatever pf did. It is distinct from egress_corrected_total, which resolves the POST-NAT copy from its source address and cannot see this case at all. A zero rate on a single-WAN box is expected; a zero rate on a policy-routed multi-WAN box means correlated per-WAN volume is still misattributed.
--exporter.disable-flow
opnsense_flow_policy_route_refused_total
Counter
reason, interface
Pre-NAT WAN-egress records the policy-route repair REFUSED to correct, by reason. reason=\"no_state\" is the mechanism's genuine miss window: the flow ended and its pf state expired before the NetFlow record arrived (short flows; the reference box runs inactiveTimeout=15). NO poll interval closes it, and the traffic is left exactly as ng_netflow reported it rather than guessed. reason=\"unresolved_device\" means pf named an egress device the interface enumeration does not know - the fix is the enumeration, and labelling the record with the raw kernel name would split one interface across two series. THE interface LABEL IS WHERE THE BYTES ARE CURRENTLY ATTRIBUTED, NOT WHERE THEY WENT. A refused record has no known real egress - that is what made it a refusal - so this names the interface ng_netflow reported. Read it as a distribution, not as a per-WAN error count: refusals piling up on the DEFAULT-ROUTE WAN are consistent with policy-routed traffic hiding inside it and mean more of this is worth recovering, while refusals spread evenly across every WAN mean the remainder is structural (records that were never a pre-NAT egress copy) and the mechanism has nothing left to find.
--exporter.disable-flow
opnsense_flow_policy_route_skipped_total
Counter
reason
Records the policy-route repair declined to act on WITHOUT it being a miss, by reason. These are NOT failures - they are the four ways the repair correctly has nothing to do - but before they were counted, three of them moved no counter at all, so a box where the repair was never running produced the same telemetry as one where it had nothing to correct. reason=\"not_wan_egress\" is the record not leaving by a WAN. It is the MAJORITY of records on any box and healthy - but it is also exactly what a wrong interface map looks like, because if the map stops reporting a device as a WAN then every record on it lands here and the repair silently ceases to exist. Watch it as a SHARE of decoded records, not as a rate: a step change in that share with no configuration change is the alarm. reason=\"post_nat\" is the copy repair 2 already resolved from its source address. reason=\"fib_agreed\" is a pf state carrying no route-to, i.e. the FIB decided and OUTPUT_SNMP was already right. reason=\"already_on_wan\" is a policy-routed state whose device ng_netflow had ALSO named, so the correction would be a no-op; it is kept apart from fib_agreed because a high fib_agreed means the box barely policy-routes while a high already_on_wan means it does and the exporter is agreeing with it.
--exporter.disable-flow
opnsense_flow_pf_state_entries
Gauge
kind
Pre-NAT pf states the policy-route repair can resolve against, by kind. kind=\"total\" is every direction=\"in\" state that could be keyed; kind=\"policy_routed\" is the subset carrying a route-to, i.e. the ones that can actually change a record's egress. kind=\"skipped\" is rows that could not be keyed at all (an unmodelled protocol, an unparseable address or port) and kind=\"conflict\" is keys that were already taken - measured zero on the reference box, so a non-zero value means the tuple stopped being unique upstream and every answer from this table wants re-checking. kind=\"carried\" is the subset of \"total\" answered from an EARLIER snapshot rather than the current one - states that have expired but are still inside the retention window, which is how a flow whose state died before its NetFlow record arrived still gets corrected. Persistently zero means that rolling union is contributing nothing.
--exporter.disable-flow
opnsense_flow_nat_pair_deduped_total
Counter
outcome
NAT'd conversations ng_netflow exported TWICE - pre-NAT where the flow entered, post-NAT where it left - resolved against pf's own nat_addr/nat_port mapping, by outcome. outcome=\"suppressed\" is the POST-NAT copy discarded because the pre-NAT copy of the same conversation was already emitted: without it those bytes are counted twice on the WAN, which read +38.5% against the kernel's interface counter on the reference box. Only the post-NAT copy is ever suppressed, because the pre-NAT copy carries the LAN host's own 5-tuple and is the only one that can correlate with Zenarmor. outcome=\"late_pre_nat\" is the residual: the pre-NAT copy arrived AFTER its twin had already been shipped, so the double count could not be prevented without discarding the correlatable copy. Measured at 10.8% of pairs; it should stay a small fraction of \"suppressed\" and the two move together. outcome=\"suppressed_by_conversation\" is the post-NAT copy discarded on the WEAKER proof: its exact twin was not in the identity table, but a pre-NAT record for the same conversation was seen recently, so the LAN side is demonstrably captured and these bytes reach the rollup anyway. It carries the whole mechanism on a QUIET WAN, where the WAN-side ng_netflow node batches its exports minutes behind the LAN node's and the exact window can never close - measured at 266s and 591s against a 120s window, which is how the reference box came to read 1.79x the kernel's counter for three days. outcome=\"unpaired\" is the GUARD: a post-NAT copy pf calls a translation that was emitted anyway because neither proof was available. Read unpaired / (unpaired + suppressed + suppressed_by_conversation) - it is the share of proven duplicates the stage failed to remove, and it goes to 1 when the mechanism breaks instead of everything simply reading zero. A small non-zero value is normal, because a post-NAT copy that legitimately arrives first is unpaired at that instant and is counted again as \"late_pre_nat\" when its twin lands. ALL ARE ZERO ON A BOX WITH NO CAPTURED ETHERNET WAN - including one whose WAN is PPPoE, which exports nothing at all - and that is correct, not a fault.
--exporter.disable-flow
opnsense_flow_nat_pair_entries
Gauge
kind
The NAT-pair de-duplication's two tables, by kind. kind=\"translations\" is how many post-NAT tuples pf's index can canonicalise - BOTH directional forms of each translated state, so roughly twice the number of translated states; kind=\"carried\" is the subset answered from an EARLIER snapshot, the same rolling union the policy-route repair uses. kind=\"identities\" is the live size of the emitted-record identity table a post-NAT copy is matched against, and kind=\"conversations\" the live size of the second, longer-lived table holding one entry per TRANSLATED conversation whose pre-NAT side has been seen - the evidence behind outcome=\"suppressed_by_conversation\". It holds one entry per conversation rather than one per export, so it scales with how much the box NATs rather than with its record rate, but its TTL is 7.5x longer and it is usually the LARGER of the two in absolute terms (4,039 against 3,026 measured). Both sit against the same bound, so watch them together. kind=\"conflict\" is a post-NAT tuple that was already mapped to a DIFFERENT conversation; a SMALL non-zero value is normal - measured 6-14 per build against ~7,000 entries on the reference box, about 0.15% - because a translation can genuinely be reused inside the retention window, and it fails safe: the conflicted tuple resolves to the wrong conversation, whose twin then will not match on volume, so the record is emitted rather than suppressed. A LARGE value means the index is mostly guessing. kind=\"skipped\" is translated rows that could not be keyed at all.
--exporter.disable-flow
opnsense_flow_pf_state_age_seconds
Gauge
---
Age of the pf state snapshot the policy-route repair resolves against. It is rebuilt every minute (the full table is ~3 MB and ~650 ms on the reference box, so this is not a free request), and a value rising past a few multiples of that means the fetch is failing and corrections are being made against a stale routing picture.
--exporter.disable-flow
opnsense_flow_dedupe_entries
Gauge
---
Flow instances currently held in the VLAN de-duplication table.
--exporter.disable-flow
opnsense_flow_vlan_child_preferred_total
Counter
---
VLAN duplicates resolved in favour of the VLAN CHILD copy after the trunk copy had already been held. ng_netflow exports the trunk hook's flows and the child hook's flows in separate datagrams, trunk first, so without this the surviving copy attributes every VLAN's traffic to the trunk interface. A zero rate on a box with VLAN interfaces means the attribution fix is not firing and per-VLAN volume is collapsed onto the trunk.
--exporter.disable-flow
opnsense_flow_vlan_subnet_attributed_total
Counter
---
Flow records moved from a TRUNK interface onto the VLAN child whose configured subnet owns the address, resolved on first sight instead of by waiting to see which copy the exporter flushed first. The 2-second hold that preceded this covers only 70.8% of real trunk/child pairs (measured p50 gap 954 ms but p95 5.7 s and p99 31.2 s), and for the other 29.2% the trunk copy had already been emitted, so every one of those flows was attributed to the trunk. This also attributes records that have NO second copy at all - 247,105 of them in an 18h35m measurement - which no hold window of any size could reach. A zero rate on a box with VLAN interfaces that carry configured subnets means the attribution is not firing and per-VLAN volume is collapsing onto the trunk.
--exporter.disable-flow
opnsense_flow_vlan_late_child_copies_total
Counter
---
VLAN duplicates that arrived attributing the flow BETTER than the copy already emitted, and therefore too late to correct it. This is the residual the repair stage cannot fix: an emitted record has been counted and shipped, so it cannot be taken back, and emitting the better copy as well would double-count real bytes. It is counted rather than left silent because it is the exact measure of remaining misattribution - it was 29.2% of pairs before subnet attribution existed and should sit near zero now, moving only for addresses that match no VLAN child subnet or several of them. A sustained non-zero rate means subnet evidence is missing for a VLAN that needs it: check that the interface has a configured subnet and that no two children overlap.
--exporter.disable-flow
opnsense_flow_repair_held_records
Gauge
---
Flow records parked in the repair stage waiting to see whether a copy on a VLAN child beats them. They are neither emitted nor dropped yet, so this is the term that closes records_in = emitted + dropped + no_address + held. A value that grows without bound means records are not being released.
--exporter.disable-flow
opnsense_flow_dedupe_entries_dropped_total
Counter
reason
Entries removed from the de-duplication table. reason=\"ttl\" is the healthy path - the instance aged out having done its job. reason=\"capacity\" means the table was full and an entry was evicted early, so a duplicate arriving afterwards is NO LONGER SUPPRESSED and reaches the rollup twice; a non-zero rate is the signal to raise the bound. reason=\"nat_capacity\" and reason=\"nat_conversation_capacity\" are the same signal for the NAT-pair stage's two tables, and read the same way - each dropped entry is a NAT'd conversation that will be counted twice on the WAN.
--exporter.disable-flow
opnsense_flow_interface_info
Gauge
device, interface, ifindex
The resolved NetFlow ifIndex map as an info metric, one series per index, always 1 - the data is in the labels. It exists to make two label spaces joinable: the box's own per-hook counters (opnsense_netflow_cache_*) are keyed by kernel DEVICE, while every flow and capture metric is keyed by the configured DESCRIPTION, and the single most valuable NetFlow health statement spans both - \"this interface is configured for capture and its OWN ng_netflow node has been frozen at zero\". Join through it with group_left: a dead hook is otherwise a two-panel eyeball correlation, because a fresh record age proves only that the interface was NAMED (ng_netflow fills the far side of each flow from a FIB lookup), never that its own hook is alive. ifindex is a POSITION in the box's ifinfo output, so it renumbers when any interface is added or removed - treat a changed value as the map having moved, not as a relabel. device is empty for index 0, which is traffic the firewall itself originated; interface is empty for a port with no OPNsense assignment, which still holds a slot in the enumeration.
--exporter.disable-flow
opnsense_flow_interface_capture_unsupported
Gauge
device, interface, reason
Interfaces whose kernel device can NEVER capture NetFlow, whatever the box's capture configuration says. Present (and always 1) only for such a device; ABSENT means capable, so this is an exception marker rather than a per-interface flag. reason=\"pppoe_framing_node\" is the only value today: ng_netflow attaches to mpd's framing node rather than the ng_iface node ng_pppoe exposes, so ngctl mkpeer on a PPPoE interface SUCCEEDS, creates the node, and then counts zero packets forever (#368). No configuration clears it, which is why this suppresses OPNsenseNetFlowHookDead rather than raising an alert of its own. Nothing is lost when it appears: ng_netflow fills the far side of every flow from a FIB lookup, so the WAN's traffic is still captured through the other interfaces' hooks and still attributed to it - 2.09 GB in 45m on the reference box while this very device's hook read zero. Untick the interface in Reporting/NetFlow to stop asking for a capture that cannot happen; leaving it selected costs only a dead netgraph node.
--exporter.disable-flow
opnsense_flow_ifindex_entries
Gauge
---
Entries in the NetFlow ifIndex-to-interface map, including the synthetic index 0 (traffic originated by the firewall itself).
--exporter.disable-flow
opnsense_flow_ifindex_conflicts
Gauge
reason
Operator overrides from --flow.netflow.ifindex-map that DISAGREE with the map derived from the API. reason=\"derived_differs\" counts pins where the derivation produced a DIFFERENT interface at that index; reason=\"derived_absent\" counts pins at an index the derivation produced nothing for at all, which is normal for an index beyond the enumeration and says nothing about the ordering. Non-zero does NOT say which side is right: a pin is a static assertion, and adding or removing ANY interface renumbers every position above it, so a pin that was correct when written goes stale on its own - that is what #516 turned out to be, a new VLAN shifting three pins by one and relabelling the WAN as the tailnet. Settle it against the box, not this counter: ifinfo order, or decisively the ifaceN hook names on its ng_netflow nodes (ngctl show netflow_<device>:). The override wins either way, so a stale pin is actively mislabelling while a correct one leaves every unpinned index suspect.
--exporter.disable-flow
opnsense_flow_ifindex_map_age_seconds
Gauge
---
Age of the ifIndex map. ng_netflow's indices are positional over ifinfo output, so adding or removing ANY interface renumbers everything and silently remaps historical series. A map that stops being refreshed is therefore a correctness problem, not a staleness nuisance.
--exporter.disable-flow
opnsense_flow_ifindex_source_disagreements
Gauge
reason
Cross-checks that failed on the derived ifIndex enumeration. reason=\"stated_index\" counts devices where the ifIndex the API states differs from the position the enumeration put them at, which means an interface was removed and every index above it shifted down. reason=\"unlisted_device\" counts interfaces the box reports that the enumeration does not contain at all, so they can never be resolved from an ifIndex. Either one non-zero means the labels on every NetFlow series are suspect - the derivation was measurably wrong this way for months (#361).
--exporter.disable-flow
opnsense_flow_ifindex_unmapped_total
Counter
---
ifIndex lookups that resolved to no interface. These records are still counted, with an EMPTY interface label - a wrong interface name is worse than a missing one. A rising rate after a network change means the enumeration shifted and --flow.netflow.ifindex-map needs setting.
RTT is the average (mean) of the round trip time in milliseconds by name and address
--exporter.disable-gateways
opnsense_gateways_rttd_milliseconds
Gauge
name, address
RTTd is the standard deviation of the round trip time in milliseconds by name and address
--exporter.disable-gateways
opnsense_gateways_rtt_low_milliseconds
Gauge
name, address
Gateway low latency threshold
--exporter.disable-gateways
opnsense_gateways_rtt_high_milliseconds
Gauge
name, address
Gateway high latency threshold
--exporter.disable-gateways
opnsense_gateways_loss_percentage
Gauge
name, address
The current gateway loss percentage by name and address
--exporter.disable-gateways
opnsense_gateways_loss_low_percentage
Gauge
name, address
Gateway low packet loss threshold
--exporter.disable-gateways
opnsense_gateways_loss_high_percentage
Gauge
name, address
Gateway high packet loss threshold
--exporter.disable-gateways
opnsense_gateways_probe_interval_seconds
Gauge
name, address
Gateway probe interval
--exporter.disable-gateways
opnsense_gateways_probe_period_seconds
Gauge
name, address
Gateway probe period
--exporter.disable-gateways
opnsense_gateways_probe_timeout_seconds
Gauge
name, address
Gateway probe timeout
--exporter.disable-gateways
opnsense_gateways_status
Gauge
name, address, default_gateway
Status of the gateway by name and address (0 = Offline, 1 = Online, 2 = Unknown, 3 = Pending, 4 = Packetloss, 5 = Latency, 6 = Offline (forced))
--exporter.disable-gateways
opnsense_gateways_force_down
Gauge
name, address
1 if the gateway is administratively forced down, 0 otherwise
--exporter.disable-gateways
opnsense_gateways_virtual
Gauge
name, address
1 if the gateway is virtual, 0 otherwise
--exporter.disable-gateways
opnsense_gateways_dynamic
Gauge
name, address
1 if the gateway is dynamically configured, 0 otherwise
--exporter.disable-gateways
opnsense_gateways_priority
Gauge
name, address
Gateway priority (lower value = higher priority)
--exporter.disable-gateways
opnsense_gateways_monitor_killstates
Gauge
name, address
1 if pf states are killed for this gateway when its monitor marks it down, 0 otherwise. Determines what a failover actually does to already-established connections through this gateway.
--exporter.disable-gateways
opnsense_gateways_monitor_killstates_priority
Gauge
name, address
1 if this gateway's state-killing on down is priority-scoped, 0 otherwise. Sibling configuration to monitor_killstates.
DMI system and BIOS identity as reported by dmidecode (value is always 1; use labels). Silent when the os-dmidecode plugin is absent.
--exporter.disable-hardware
opnsense_hardware_psu_status
Gauge
psu
Deciso DEC-series power supply status (1 = powered, 0 = not powered). Only emitted on hardware with a GPIO power-status device; silent when the os-dec-hw plugin is absent or no such hardware is detected.
Number of hosts in the discovered-host inventory, by interface, source and manufacturer. source is \"discovery\" when the hostwatch daemon is enabled (persistent inventory, survives reboots and cache expiry) or \"arp-ndp\" when it is disabled (a live fallback that duplicates the arp_table/ndp collectors). manufacturer is the OUI vendor lookup of the MAC (organization_name), same label name/convention as arp_table/ndp's manufacturer label; \"unknown\" when the box could not resolve an OUI (e.g. a randomized-MAC device). Aggregate only: never a per-host series.
--exporter.disable-hostdiscovery
opnsense_hostdiscovery_hosts_recent
Gauge
interface, source, manufacturer
Number of hosts in the discovered-host inventory whose last_seen falls within a 15 minute window, by interface, source and manufacturer. Always 0 for source=\"arp-ndp\" rows, which carry no last_seen timestamp to judge recency from.
Suricata IDS/IPS service status (1 = running, 0 = stopped, -1 = disabled/unconfigured)
--exporter.disable-ids
opnsense_ids_ips_mode_enabled
Gauge
---
Whether Suricata runs inline as an IPS that drops traffic (1) rather than a passive IDS (0)
--exporter.disable-ids
opnsense_ids_promiscuous_mode_enabled
Gauge
---
Whether the IDS monitoring interface is in promiscuous mode (1 = yes, 0 = no)
--exporter.disable-ids
opnsense_ids_alert_log_files
Gauge
---
Number of Suricata eve log files on disk (the live eve.json plus rotated copies)
--exporter.disable-ids
opnsense_ids_alert_log_size_bytes
Gauge
filename
Size in bytes of each Suricata eve log file
--exporter.disable-ids
opnsense_ids_recent_alerts
Gauge
action
Suricata alerts observed within the lookback window (--exporter.ids-alert-lookback), by action. A GAUGE, not a counter (query_alerts reads a windowed, saturating backend); a floor when more than 500 alerts fall inside the window. Requires --exporter.enable-ids-alerts.
--exporter.disable-ids
opnsense_ids_ruleset_enabled
Gauge
ruleset
Whether an installable Suricata ruleset is enabled (1) or disabled (0), by ruleset filename
Whether the IPsec phase2 (child SA) is fully installed and passing traffic (1 = INSTALLED, 0 = any other transitional/rekeying/deleting state). Phase1 being connected does not guarantee every child SA is up; check this alongside opnsense_ipsec_phase1_status to catch a tunnel where the parent IKE SA is fine but one traffic selector's child SA has failed or is stuck rekeying.
--exporter.disable-ipsec
opnsense_ipsec_service_running
Gauge
---
Whether the service is running (1 = running, 0 = stopped/disabled)
--exporter.disable-ipsec
opnsense_ipsec_pool_leases_online
Gauge
pool, net
Number of online leases in the IPsec mode-cfg pool
--exporter.disable-ipsec
opnsense_ipsec_pool_leases_offline
Gauge
pool, net
Number of offline leases in the IPsec mode-cfg pool
--exporter.disable-ipsec
opnsense_ipsec_pool_size
Gauge
pool, net
Total size (address capacity) of the IPsec mode-cfg pool
--exporter.disable-ipsec
opnsense_ipsec_sad_entries
Gauge
satype, ikeid, phase1desc
Number of installed kernel IPsec security associations (setkey -D), grouped by satype
--exporter.disable-ipsec
opnsense_ipsec_sa_age_seconds
Gauge
ikeid, phase2desc, reqid
Age in seconds of the oldest installed kernel SA in each reqid (child-SA) group
--exporter.disable-ipsec
opnsense_ipsec_sa_lifetime_hard_seconds
Gauge
ikeid, phase2desc, reqid
Soonest hard-expiry lifetime in seconds across the kernel SAs in each reqid group
--exporter.disable-ipsec
opnsense_ipsec_sa_lifetime_soft_seconds
Gauge
ikeid, phase2desc, reqid
Soonest soft-expiry (rekey) lifetime in seconds across the kernel SAs in each reqid group
--exporter.disable-ipsec
opnsense_ipsec_sa_bytes_current_total
Counter
ikeid, phase2desc, reqid
Cumulative bytes processed by the most-utilized installed kernel SA in each reqid (child-SA) group (the higher of the two during a brief rekey overlap). Resets to a small value on every rekey (a new SPI starts counting from zero) — compare against sa_bytes_soft_limit to see how close the group is to its next byte-triggered rekey.
--exporter.disable-ipsec
opnsense_ipsec_sa_bytes_soft_limit
Gauge
ikeid, phase2desc, reqid
Configured soft (rekey-triggering) byte-count limit for the kernel SAs in each reqid group. Only present when the box configures a byte-count lifetime for this child SA; a limit of 0 means unlimited and is not exported as a fabricated zero.
--exporter.disable-ipsec
opnsense_ipsec_sa_bytes_hard_limit
Gauge
ikeid, phase2desc, reqid
Configured hard (forced-expiry) byte-count limit for the kernel SAs in each reqid group. Only present when the box configures a byte-count lifetime for this child SA; a limit of 0 means unlimited and is not exported as a fabricated zero.
--exporter.disable-ipsec
opnsense_ipsec_sa_allocated_current_total
Counter
ikeid, phase2desc, reqid
Cumulative packets (\"allocations\") processed by the most-utilized installed kernel SA in each reqid (child-SA) group (the higher of the two during a brief rekey overlap). The packet-count equivalent of sa_bytes_current_total; some child SAs are configured with a packet-count rekey margin instead of, or alongside, a byte-count one.
--exporter.disable-ipsec
opnsense_ipsec_sa_allocated_soft_limit
Gauge
ikeid, phase2desc, reqid
Configured soft (rekey-triggering) packet-count limit for the kernel SAs in each reqid group. Only present when the box configures a packet-count lifetime for this child SA; a limit of 0 means unlimited and is not exported as a fabricated zero.
--exporter.disable-ipsec
opnsense_ipsec_sa_allocated_hard_limit
Gauge
ikeid, phase2desc, reqid
Configured hard (forced-expiry) packet-count limit for the kernel SAs in each reqid group. Only present when the box configures a packet-count lifetime for this child SA; a limit of 0 means unlimited and is not exported as a fabricated zero.
--exporter.disable-ipsec
opnsense_ipsec_sad_nat_traversal
Gauge
ikeid
Whether any kernel SA for the IKE SA is NAT-traversed (1 = NAT-T detected, 0 = not)
--exporter.disable-ipsec
opnsense_ipsec_spd_policies
Gauge
direction
Number of installed kernel IPsec security policies (setkey -DP), grouped by direction
--exporter.disable-ipsec
opnsense_ipsec_lease_online
Gauge
pool, user
Whether the IPsec mode-cfg lease is currently online (1 = online, 0 = offline). Per-user detail; only emitted with --exporter.enable-ipsec-lease-details
--exporter.disable-ipsec
opnsense_ipsec_config_dirty
Gauge
---
Whether there is an uncommitted (staged but not applied) IPsec configuration change (1 = dirty, 0 = clean)
--exporter.disable-ipsec
opnsense_ipsec_legacy_enabled
Gauge
---
Whether IPsec is enabled in the configuration (1 = enabled, 0 = disabled)
Per-lease ISC DHCPv4 information (value is always 1; use labels). Only emitted when --exporter.enable-dhcpv4-details is set. device is the raw logical interface id and interface the assigned description; they diverge on VLAN children and bridges, and only device joins against the interfaces metrics (the #544 item-5 pattern).
Per-lease ISC DHCPv6 information (value is always 1; use labels). Only emitted when --exporter.enable-dhcpv6-details is set. device is the raw logical interface id and if_descr the assigned description; they diverge on VLAN children and bridges, and only device joins against the interfaces metrics (the #544 item-5 pattern).
--exporter.disable-dhcpv6
opnsense_dhcpv6_pd_prefixes_total
Gauge
---
Total number of ISC DHCPv6 prefix delegation entries
--exporter.disable-dhcpv6
opnsense_dhcpv6_pd_prefixes_active
Gauge
---
Number of active ISC DHCPv6 prefix delegation entries
Bytes received on this interface by interface name and device.
--exporter.disable-interfaces
opnsense_interfaces_transmitted_bytes_total
Counter
interface, device, type
Bytes transmitted on this interface by interface name and device.
--exporter.disable-interfaces
opnsense_interfaces_received_multicasts_total
Counter
interface, device, type
Multicasts received on this interface by interface name and device
--exporter.disable-interfaces
opnsense_interfaces_transmitted_multicasts_total
Counter
interface, device, type
Multicasts transmitted on this interface by interface name and device
--exporter.disable-interfaces
opnsense_interfaces_input_errors_total
Counter
interface, device, type
Input errors on this interface by interface name and device.
--exporter.disable-interfaces
opnsense_interfaces_output_errors_total
Counter
interface, device, type
Output errors on this interface by interface name and device.
--exporter.disable-interfaces
opnsense_interfaces_collisions_total
Counter
interface, device, type
Collisions on this interface by interface name and device.
--exporter.disable-interfaces
opnsense_interfaces_received_packets_total
Counter
interface, device, type
Total packets received on this interface by interface name and device.
--exporter.disable-interfaces
opnsense_interfaces_transmitted_packets_total
Counter
interface, device, type
Total packets transmitted on this interface by interface name and device.
--exporter.disable-interfaces
opnsense_interfaces_send_queue_length
Gauge
interface, device, type
Current send queue length on this interface by interface name and device
--exporter.disable-interfaces
opnsense_interfaces_send_queue_max_length
Gauge
interface, device, type
Maximum send queue length on this interface by interface name and device
--exporter.disable-interfaces
opnsense_interfaces_send_queue_drops_total
Counter
interface, device, type
Send queue drops on this interface by interface name and device. Not emitted for an interface whose reported figure has wrapped through an unsigned 32-bit field (see input_queue_drops_total). Note this counter reads the legacy if_snd.ifq_drops and is structurally always 0 on modern buf_ring drivers, so it is not worth alerting on.
--exporter.disable-interfaces
opnsense_interfaces_input_queue_drops_total
Counter
interface, device, type
Input queue drops on this interface by interface name and device. A value that reinterprets as a negative int32 has wrapped through an unsigned 32-bit field below the exporter (prod ixl1 reports 4294958080, which is -9216) and is suppressed rather than published: the series is absent for that interface, since a fabricated 0 would falsely assert it reported no drops.
--exporter.disable-interfaces
opnsense_interfaces_link_state
Gauge
interface, device, type
Link state of this interface (1=up, 0=down, 2=unknown) by interface name and device. 2 (unknown) is reported by the kernel for carrier-less pseudo-devices such as PPPoE and tun/tailscale interfaces, which have no carrier-sense concept and are not actually down; alert on link_state==0, not link_state!=1.
--exporter.disable-interfaces
opnsense_interfaces_line_rate_bits
Gauge
interface, device, type
Line rate in bits per second on this interface by interface name and device. Not emitted for a carrier-less pseudo-device (the kernel's link state reads unknown — PPPoE, tun/tailscale, similar overlay interfaces): the kernel reports a placeholder there rather than a real negotiated rate (ng_pppoe reports a static 64000 regardless of the underlying WAN's actual speed), and publishing it would make rate(bytes)/line_rate_bits wrong by orders of magnitude (#644). Ethernet devices are unaffected.
Packets delivered to this interface that the network stack could not classify (kernel \"packets for unknown protocol\" counter) by interface name and device.
System-uptime reading at which this interface was attached OR its statistics were explicitly reset (the kernel does not distinguish the two cases) by interface name and device. Only emitted when the box reports this marker. Compute age since attach/reset with opnsense_system_uptime_seconds - opnsense_interfaces_attach_or_statistics_reset_uptime_seconds.
--exporter.disable-interfaces
opnsense_interfaces_admin_up
Gauge
interface, device
Administrative status of this interface (1 = configured up / ifconfig UP flag set, 0 = admin down). Compare with link_state for carrier detection. Join with other interfaces metrics on the device label; the interface label here is the overview description, which can differ from the traffic-based metrics' interface name for unassigned/pseudo devices.
--exporter.disable-interfaces
opnsense_interfaces_admin_enabled
Gauge
interface, device
OPNsense config-level enabled flag for this interface (1 = enabled in Interfaces > [name], 0 = administratively disabled in config). Distinct from admin_up, which reflects the ifconfig UP flag (kernel/runtime state) rather than configuration: use admin_enabled to separate a deliberately-disabled interface (expected, not an incident) from one that is enabled in config but down (a real problem). This endpoint's response is cached for --exporter.cache-ttl (#573); that is fine here since this is config data, not a live status the cache would stale-freeze.
Interface identity from the interfaces overview API (media/duplex, link type, VLAN topology) plus the kernel driver name and HW offload capability set from the traffic API. Value is always 1. Join on the device label. The media label can change on link renegotiation, starting a new series. driver is the kernel driver name (e.g. igb, ixl, ixgbe) — ixl/ixgbe/igb override IFCOUNTER_OQDROPS in their own if_get_counter, so a per-driver caveat on output_queue_drops_total and on a wrapped input_queue_drops_total figure can be checked against it. hw_offload_capabilities is the box's checksum/TSO/LRO offload state, comma-joined and sorted for stability; both are empty when the device has no matching row in the traffic API fetch.
Bytes sent from this device's addresses of this family (ipv4/ipv6).
--exporter.disable-interfaces
opnsense_interfaces_output_queue_drops_total
Counter
device
Packets the kernel dropped on this device's output queue (netstat's Drop column, IFCOUNTER_OQDROPS). CAVEAT, and it matters: on ixl, ixgbe and igb the driver overrides IFCOUNTER_OQDROPS in its own if_get_counter and reports its own figure, so on those NICs a flat zero here does NOT prove nothing is dropping — netmap's drops in particular are invisible. Treat a non-zero value as real and a zero on those drivers as no information. Covers every device netstat reports, including unassigned ports and pseudo-devices that send_queue_drops_total omits entirely (on the dev boxes tailscale0 is the only device with non-zero drops and the only one missing from that metric).
--exporter.disable-interfaces
opnsense_interfaces_lagg_info
Gauge
device, protocol, hash
LAGG (link aggregation) interface protocol/hash configuration. Value is always 1. Only emitted for interfaces that are themselves a lagg device. Join on the device label.
--exporter.disable-interfaces
opnsense_interfaces_lagg_active_ports
Gauge
device
Number of currently active (traffic-carrying) member ports in this LAGG interface. Only emitted when the box reports a lagg statistics block for this device.
--exporter.disable-interfaces
opnsense_interfaces_lagg_flapping_total
Counter
device
Cumulative count of LAGG active-port membership change (flap) events since the last stat reset. Only emitted when the box reports a lagg statistics block for this device.
--exporter.disable-interfaces
opnsense_interfaces_lagg_port_active
Gauge
device, member
Whether this LAGG member port is currently active/selected to carry traffic (1) or not (0). The device label is the owning lagg interface; member is the physical port.
--exporter.disable-interfaces
opnsense_interfaces_lagg_port_collecting
Gauge
device, member
LACP collecting state (RX distribution enabled) for this LAGG member port (1=collecting, 0=not). Only emitted for LACP laggs whose laggport reported a state; failover/loadbalance laggs never carry this state and emit no series.
--exporter.disable-interfaces
opnsense_interfaces_lagg_port_distributing
Gauge
device, member
LACP distributing state (TX distribution enabled) for this LAGG member port (1=distributing, 0=not). Only emitted for LACP laggs whose laggport reported a state; failover/loadbalance laggs never carry this state and emit no series.
--exporter.disable-interfaces
opnsense_interfaces_bridge_member
Gauge
device, member
Whether this interface is a member of the given bridge(4) interface. Value is always 1. The device label is the owning bridge interface; member is the attached interface.
--exporter.disable-interfaces
opnsense_interfaces_sfp_info
Gauge
device, vendor, part_number, serial_number
SFP/SFP+ transceiver identity for this interface's optical cage. Value is always 1. Emitted for any plugged module, optical or copper. Join on the device label.
--exporter.disable-interfaces
opnsense_interfaces_sfp_temperature_celsius
Gauge
device
SFP module temperature in degrees Celsius (Digital Optical Monitoring). Only emitted when the transceiver reports a DOM temperature reading; copper RJ45 SFPs never report DOM and emit no series here.
--exporter.disable-interfaces
opnsense_interfaces_sfp_voltage_volts
Gauge
device
SFP module supply voltage in volts (Digital Optical Monitoring). Only emitted when the transceiver reports a DOM voltage reading; copper RJ45 SFPs never report DOM and emit no series here.
--exporter.disable-interfaces
opnsense_interfaces_sfp_lane_rx_power_milliwatts
Gauge
device, lane
SFP per-lane received optical power in milliwatts, linear scale (Digital Optical Monitoring). See also sfp_lane_rx_power_dbm for the logarithmic (dBm) reading of the same measurement. Only emitted for lanes with a DOM RX power reading; copper RJ45 SFPs never report DOM and emit no series here.
--exporter.disable-interfaces
opnsense_interfaces_sfp_lane_rx_power_dbm
Gauge
device, lane
SFP per-lane received optical power in dBm, logarithmic scale (Digital Optical Monitoring). See also sfp_lane_rx_power_milliwatts for the linear (mW) reading of the same measurement. Only emitted for lanes with a DOM RX power reading; copper RJ45 SFPs never report DOM and emit no series here. NOTE (#456): before this release this series erroneously published the mW reading under the _dbm name — values will step-change to the correct (and much smaller-magnitude, often negative) dBm figure.
--exporter.disable-interfaces
opnsense_interfaces_sfp_lane_tx_bias_milliamps
Gauge
device, lane
SFP per-lane laser bias current in milliamps (Digital Optical Monitoring). Only emitted for lanes with a DOM TX bias reading; copper RJ45 SFPs never report DOM and emit no series here.
Per-lease DHCPv4 information (value is expire timestamp). Only emitted when --exporter.enable-kea-details is set.
--exporter.disable-kea
opnsense_kea_dhcp4_lease_pool_stats
Gauge
pool_state
Kea's own DHCPv4 lease pool accounting (the response's top-level stats object, computed over the FULL lease population), by pool_state (active/inactive/total). Authoritative and never truncated by bootgrid pagination, unlike a row-derived count -- a complement to the other DHCPv4 lease metrics in this collector, not a replacement for any of them.
--exporter.disable-kea
opnsense_kea_dhcp6_leases_total
Counter
---
Total number of Kea DHCPv6 leases
--exporter.disable-kea
opnsense_kea_dhcp6_leases_by_interface
Gauge
interface
Number of Kea DHCPv6 leases per interface
--exporter.disable-kea
opnsense_kea_dhcp6_leases_reserved_total
Counter
---
Total number of reserved (static) Kea DHCPv6 leases
--exporter.disable-kea
opnsense_kea_dhcp6_leases_dynamic_total
Counter
---
Total number of dynamic Kea DHCPv6 leases
--exporter.disable-kea
opnsense_kea_dhcp6_leases_by_state
Gauge
state
Number of Kea DHCPv6 leases per lease state (active, declined, expired-reclaimed)
--exporter.disable-kea
opnsense_kea_dhcp6_leases_by_type
Gauge
type
Number of Kea DHCPv6 leases per lease type (IA_NA address lease vs IA_PD prefix delegation)
Per-lease DHCPv6 information (value is expire timestamp). Only emitted when --exporter.enable-kea-details is set. prefix_len is the delegated prefix's block size (e.g. 56 for a /56) for an IA_PD (prefix-delegation) lease; for an IA_NA (address) lease it carries Kea's fixed 128 (a single address has no delegated block).
--exporter.disable-kea
opnsense_kea_dhcp6_lease_pool_stats
Gauge
pool_state
Kea's own DHCPv6 lease pool accounting (the response's top-level stats object, computed over the FULL lease population), by pool_state (active/inactive/total). Authoritative and never truncated by bootgrid pagination, unlike a row-derived count -- a complement to the other DHCPv6 lease metrics in this collector, not a replacement for any of them.
--exporter.disable-kea
opnsense_kea_service_running
Gauge
---
Whether the Kea DHCP service is running (1 = running, 0 = stopped/disabled)
--exporter.disable-kea
opnsense_kea_dhcp4_pool_size
Gauge
subnet, interface
Number of addresses in the configured Kea DHCPv4 pools for this subnet
--exporter.disable-kea
opnsense_kea_dhcp6_pool_size
Gauge
subnet, interface
Number of addresses in the configured Kea DHCPv6 pools for this subnet
--exporter.disable-kea
opnsense_kea_dhcp4_pool_used
Gauge
subnet
Number of Kea DHCPv4 leases whose address falls within this subnet's configured pool
--exporter.disable-kea
opnsense_kea_dhcp6_pool_used
Gauge
subnet
Number of Kea DHCPv6 address (non-PD) leases whose address falls within this subnet's configured pool
--exporter.disable-kea
opnsense_kea_dhcp6_pd_pool_size
Gauge
subnet, prefix
Delegable-prefix capacity of a configured Kea DHCPv6 prefix-delegation pool (2^(delegated_len-prefix_len))
Items currently allocated out of this UMA zone. A UMA zone is a kernel pool of fixed-size objects (firewall states, sockets, mbufs, ...). Multiply by opnsense_kernel_memory_zone_item_size_bytes for the bytes in use. Where the box reported the same zone name more than once (one row per NUMA domain, or one per instance of a kernel module's zone) the rows are summed into a single series.
--exporter.disable-kernel-memory
opnsense_kernel_memory_zone_free
Gauge
zone
Items sitting free in this UMA zone's caches, already charged to the kernel and available for immediate reuse. A zone with used climbing and free at zero is about to have to ask the VM system for more memory, which is where allocation failures start.
--exporter.disable-kernel-memory
opnsense_kernel_memory_zone_limit
Gauge
zone
Configured hard ceiling on items in this UMA zone. ZERO MEANS NO CEILING IS CONFIGURED, not a ceiling of zero - verified on a live OPNsense 26.1 firewall, where 'pf state keys' reports limit 0 while holding 16275 items. Any saturation ratio must therefore skip zones whose limit is 0, e.g. opnsense_kernel_memory_zone_used / (opnsense_kernel_memory_zone_limit > 0). Where several rows were merged into one series the ceilings are summed, which is the correct aggregate ceiling for that zone across the box.
--exporter.disable-kernel-memory
opnsense_kernel_memory_zone_item_size_bytes
Gauge
zone
Size in bytes of ONE item in this UMA zone - not the zone's total footprint. It is a near-constant property of the kernel build; it is exported so bytes can be derived from the item counts without hard-coding struct sizes per release.
--exporter.disable-kernel-memory
opnsense_kernel_memory_zone_requests_total
Counter
zone
Allocation requests made against this UMA zone since boot, successful or not. Useful as the denominator for a failure ratio and as the churn rate of the underlying resource.
--exporter.disable-kernel-memory
opnsense_kernel_memory_zone_failures_total
Counter
zone
Allocations this UMA zone could NOT satisfy since boot. A non-zero rate means the kernel asked for an object and was refused, and the caller had to give up: on 'pf states' a connection was not tracked, on 'socket' or 'tcp_inpcb' a connection was refused, on an mbuf zone a packet was dropped. Alert on the rate, not the level - the counter never resets before a reboot. Two zone families are noisy by design and are NOT operator-actionable: the 'N Bucket' zones (UMA's own per-CPU cache bucket allocator, which is expected to fail and fall back - all three live boxes show large non-zero values) and 'vm pgcache'. Six zones overlap the mbuf collector, which reads the separate systemMbuf endpoint: mbuf<->type=mbuf, mbuf_packet<->packet, mbuf_cluster<->cluster, mbuf_jumbo_page<->jumbop, mbuf_jumbo_9k<->jumbo9, mbuf_jumbo_16k<->jumbo16 (the mbuf collector's type=sfbufs has no UMA zone). Those are two independent readings of the same kernel counters taken from two endpoints at two different times; treat them as corroboration and NEVER sum the two families together, or every mbuf failure is counted twice.
--exporter.disable-kernel-memory
opnsense_kernel_memory_zone_sleeps_total
Counter
zone
Allocations from this UMA zone that had to BLOCK waiting for memory since boot. The softer twin of zone_failures_total: the allocation eventually succeeded, so nothing was dropped, but the requesting thread stalled - which on a packet path shows up as latency rather than loss. Sleeps rising is the warning that precedes failures. Same overlap rule as zone_failures_total: do not sum with opnsense_mbuf_sleeps_total.
--exporter.disable-kernel-memory
opnsense_kernel_memory_zone_xdomain_total
Counter
zone
Allocations this UMA zone satisfied from a REMOTE NUMA domain since boot, because the local domain had nothing free. Not an error - the allocation succeeded - but on a NUMA box a rising rate means memory is being touched across the interconnect and is a latency, not a correctness, signal. Flat zero on single-domain hardware.
--exporter.disable-kernel-memory
opnsense_kernel_memory_zone_failures_all_total
Counter
---
Allocation failures summed across EVERY UMA zone the box reports, including zones that have no dedicated panel and zones a future kernel adds. It exists so a failure in a zone nobody thought to look at is still visible on one series; use it as the catch-all alert and the per-zone series to find the culprit. It includes the benign 'N Bucket' and 'vm pgcache' families, which on every live box dominate the absolute value - so this counter is only meaningful as a rate, and a baseline above zero is normal.
--exporter.disable-kernel-memory
opnsense_kernel_memory_malloc_in_use
Gauge
type
Live allocations currently held by this kernel malloc type (vmstat -m's InUse). Malloc types are variable-size kernel allocations, as distinct from the fixed-size UMA zones above; a type whose InUse climbs monotonically over weeks of uptime is the shape of a kernel memory leak.
--exporter.disable-kernel-memory
opnsense_kernel_memory_malloc_bytes
Gauge
type
Bytes currently held by this kernel malloc type (vmstat -m's MemUse). Reported in bytes by the API despite the OPNsense UI labelling the total 'k'.
--exporter.disable-kernel-memory
opnsense_kernel_memory_malloc_requests_total
Counter
type
Allocation requests made against this kernel malloc type since boot. There is no per-type failure counter in this payload - vmstat -m does not report one - so malloc-type exhaustion is inferred from in_use/bytes growth, not observed directly. The failure signal lives on the UMA zones above.
Number of local users, by disabled state (aggregate count only - no usernames are exposed).
--exporter.disable-auth
opnsense_auth_admin_users
Gauge
---
Number of local users with administrator privileges (is_admin computed by OPNsense; aggregate count only).
--exporter.disable-auth
opnsense_auth_users_expired
Gauge
---
Number of local users whose account expiry date is in the past (aggregate count only).
--exporter.disable-auth
opnsense_auth_users_with_otp
Gauge
---
Number of local users with a TOTP seed configured (aggregate count only - the seed itself is never read into exporter memory beyond a transient presence check).
--exporter.disable-auth
opnsense_auth_api_keys
Gauge
---
Total number of local-user API keys configured (aggregate count only - key material is never decoded).
--exporter.disable-auth
opnsense_auth_groups
Gauge
---
Total number of local authentication groups configured.
--exporter.disable-auth
opnsense_auth_users_shell_warning
Gauge
---
Number of local users OPNsense flags with shell_warning: a NON-administrator account that has been given a real login shell (any shell whose path starts with /). Says nothing about which shell, and never fires for an administrator. Aggregate count only - no usernames are exposed.
--exporter.disable-auth
opnsense_auth_oldest_password_age_seconds
Gauge
---
Age in seconds of the least recently changed local password, across the accounts that have a recorded change time. NOT emitted at all when no account has one - a 0 would claim every password was just rotated. Read alongside users_password_age_unknown, which counts the accounts this maximum cannot see. Aggregate only - no usernames are exposed.
--exporter.disable-auth
opnsense_auth_users_password_age_unknown
Gauge
---
Number of local users with no usable password-change time. OPNsense records pwd_changed_at only when a password is actually changed, so this counts accounts whose password predates that bookkeeping - the worst posture on the box, and invisible in oldest_password_age_seconds. Aggregate count only - no usernames are exposed.
Firewall (filterlog) events derived from received syslog, by action, interface, rule and scope. Counts every line including passes; the raw pass lines may be sampled away (--logs.syslog.sample) while this counter still counts them.
--exporter.disable-log-events
opnsense_log_events_haproxy_total
Counter
event, backend, server, state, status_class
HAProxy events derived from received syslog, by event, backend, server, state and HTTP status class.
--exporter.disable-log-events
opnsense_log_events_sshd_total
Counter
result, method, scope
sshd authentication events derived from received syslog, by result, method and source scope.
--exporter.disable-log-events
opnsense_log_events_dhcp_total
Counter
action, interface, server
DHCP lease events derived from received syslog, by action, interface and server.
--exporter.disable-log-events
opnsense_log_events_audit_total
Counter
event, result
Audit/config events derived from received syslog, by event and result.
--exporter.disable-log-events
opnsense_log_events_ids_total
Counter
event_type, action, category, severity
Suricata IDS/IPS events derived from received syslog, by event type, action, category and severity. Signature text and SID are never labels.
--exporter.disable-log-events
opnsense_log_events_gateway_total
Counter
event, gateway
dpinger gateway alarm transitions derived from received syslog, by closed event and configured gateway monitor name. Address, alarm state, RTT and loss stay on the structured log record and are never labels.
--exporter.disable-log-events
opnsense_log_events_radius_total
Counter
event, result, client_scope
FreeRADIUS access decisions derived from received syslog, by event, result and client scope.
--exporter.disable-log-events
opnsense_log_events_vpn_total
Counter
backend, event, result, connection
IPsec (charon) and OpenVPN tunnel lifecycle transitions derived from received syslog, by backend, closed event, result and configured connection name. event is one of established, terminated, authentication_failed, liveness_failed or certificate_failed; result is success for the first two and failure for the other three. connection is the name configured on the firewall, resolved from the IPsec connection or OpenVPN instance id, and is EMPTY when the id could not be resolved - never a raw UUID. Usernames, certificate subjects and serials, IKE identities, peer addresses and ports, SPIs and daemon error text are never labels; they stay on the shipped log record. Only the grammar captured on OPNsense 27.1.a_40 (strongSwan 6.0.7, OpenVPN 2.7.5) is counted - any other line still ships as a log record but is not counted as an inferred transition.
--exporter.disable-log-events
opnsense_log_events_carp_total
Counter
event, from, to, interface, vhid
FreeBSD kernel CARP transitions derived from received syslog, by closed event, previous and current state, OS interface device and VHID. event is one of state_changed, demoted or promoted; from and to are master, backup or init, lowercased. A demotion record leaves from, to, interface and vhid EMPTY - the kernel's demotion adjustment is global to the node and names neither an interface nor a VHID. demoted and promoted are distinguished by the SIGN of the kernel's demotion delta: positive raises the node's demotion total, negative lowers it. The kernel's CAUSE for the transition (initialization complete, master timed out, hardware interface up, pfsync bulk start, pfsync bulk fail, service disruption, ...) is deliberately NOT a label: it is open-ended free text across FreeBSD versions. It ships as carp.reason on the log record, alongside carp.demotion.delta and carp.demotion.total - which are the numbers explaining WHY a node demoted, and which opnsense_carp_demotion (a current-state gauge) does not retain. Read this beside the CARP VIP Status timeline: that shows what state the node is in now, this shows the transitions that got it there. Only the grammar captured on OPNsense 27.1.a_40 (FreeBSD 15, net.inet.carp.log=1) is counted - any other kernel line still ships as a log record but is never counted as an inferred transition.
--exporter.disable-log-events
opnsense_log_events_upnp_total
Counter
event, result, protocol
miniupnpd UPnP IGD / NAT-PMP / PCP mapping events derived from received syslog, by closed event, result and protocol. event is one of expired (a mapping reached the end of its lease and was torn down - the only event whose result is ok), cleanup_failed (the daemon could not find the pf nat or redirect rule it was deleting), unauthorized (a PCP client asked to remove a mapping it does not own) or lease_file_error. protocol is tcp or udp, and EMPTY on the two cleanup-failure grammars and the lease-file error, which name none. Port numbers, the daemon's opaque addr= token, lease-file paths, mapping descriptions and client identities are deliberately NOT labels - an ephemeral port would mint a series per mapping - and ship as upnp.port.external / upnp.port.internal or inside the log record's body. THERE IS NO ACTIVE-MAPPING GAUGE and none can be derived from this: the plugin's own status page runs pfctl to list mappings rather than exposing them through an API, an event stream cannot see pre-existing mappings or survive a daemon restart, and expired is a decrement with no matching increment. A SUCCESSFUL add or delete is likewise absent - miniupnpd logs those at a verbosity OPNsense's os-upnp plugin does not expose, and no attempt line is treated as proof of success. Only the grammar captured on OPNsense 26.7.1_1 and 27.1.a_40 (miniupnpd 2.3.9_2,1) is counted; any other miniupnpd line still ships as a log record but is never counted.
Zenarmor events received over the Elasticsearch receiver, by family (flow/dns/tls/web/ids/voip), action, category, interface, DNS rcode, alert severity and HTTP status class. Fields that do not apply to a family are empty. Zenarmor ships ~2.5-3.3M records/day, so these counters are the way to ask rate questions without querying the raw log stream - and they outlive Loki's retention. Application name, IPs, ports, hostnames, MACs, JA3, session/community/connection ids, URIs and DNS queries are never labels; they stay as structured metadata on the record.
--exporter.disable-log-events
opnsense_log_events_zenarmor_device_info
Gauge
device_name, device_category, interface
Client devices Zenarmor has attributed traffic to recently, one series per device with a constant value of 1. It exists so a dashboard can ENUMERATE devices: device_name is Loki structured metadata, which label_values() cannot list and which was rejected as a Loki index label in #473 because it is not a closed set (live values include Plex-generated DNS names). Bounded on both axes - at most 512 devices tracked, and a device is retired 24h after it was last seen - so a churning name cannot grow the series set without limit; refusals surface as cardinality_capped_total{family=\"zenarmor_device\"}. Filter the log stream itself on the device_name structured metadata field, not on this metric.
--exporter.disable-log-events
opnsense_log_events_netmap_ring_full_events_total
Counter
device
Reports the FreeBSD kernel emitted that a netmap host transmit ring was full on this device - Zenarmor's packet-capture datapath having nowhere to put a packet. ONE PER KERNEL LINE, not one per interval: nm_prlim lets two lines through per second and both are counted, because they are two distinct netmap_transmit() calls that each found the ring full (#610 read the pair as one event double-logged; the capture disagrees - their uptime fields differ by ~70us). THIS COUNTS OCCURRENCES, NOT DROPPED PACKETS, and the difference matters: netmap_transmit() logs through nm_prlim(2, ...), which rate-limits the kernel to 2 lines per second, so this counter FLAT-TOPS at 2/s and under-reports hardest exactly when the condition is worst. Read a rise as 'the ring is filling', never as a packet count, and read a flat 2/s as saturation rather than as a bounded problem. Absence of growth means no report, NOT no drops. There is no true drop counter available for this: the kernel bumps IFCOUNTER_OQDROPS, but the ixl driver overrides that counter in its own if_get_counter, so netmap's increment is invisible on the exact 10G interface that drops - live-corroborated, 14 kernel reports since boot against an API-reported dropped-packets of 0. The ring indices from the log line (hwcur, hwtail, qlen) are NOT labels - they change on every occurrence and would mint a series per line - and ship as netmap.hwcur / netmap.hwtail / netmap.qlen on the log record, where hwcur == hwtail is a completely full ring.
--exporter.disable-log-events
opnsense_log_events_arp_address_moves_total
Counter
interface
Times the FreeBSD kernel reported an IP address moving from one MAC address to another on this interface - its own duplicate-address, MAC-flap and ARP-spoof detector. Polling the ARP table CANNOT replace this: a flap that resolves inside one poll interval leaves a single MAC in the table, so the scrape sees a perfectly healthy entry and the event is invisible. A steady rate usually means two hosts claiming one address, a gateway failing over, or a VM live-migrating; a burst on a user VLAN is worth investigating. The contested IP and BOTH MAC addresses are deliberately NOT labels - they name whichever hosts are fighting over the address, so the value set is unbounded and identifies individual machines - and ship as arp.address / arp.mac.previous / arp.mac.current on the log record, which is where to look to find out who.
--exporter.disable-log-events
opnsense_log_events_dhcp_client_total
Counter
interface, type
DHCP messages this firewall's OWN WAN client sent or received, by interface and message type. This is the CLIENT holding the firewall's uplink address, not the DHCP servers it runs for the LAN - those are opnsense_log_events_dhcp_total, and folding the two together would make a WAN renewal storm indistinguishable from LAN lease churn. type is a closed vocabulary resolved in code: discover, request, ack, nak, offer, decline, release, inform. The signal to watch is the REQUEST rate: a healthy lease renews at T1, so a sustained rise is a retransmit storm and a leading indicator of losing the WAN address hours before the lease actually expires (production ran an 11-12 hour storm at ~100x baseline on 2026-07-30 with no telemetry at all). Any nak deserves attention on its own. interface is EMPTY on a received message that named none - dhclient's 'DHCPACK from ' carries no interface, and it is resolved by correlating the daemon PID with the interface its preceding DHCPREQUEST named; empty means the exporter never saw that line, not that the message had no interface. The DHCP server's address is NOT a label and ships as dhcp_client.server on the record.
--exporter.disable-log-events
opnsense_log_events_dhcp_client_script_total
Counter
interface, reason
dhclient-script invocations on this firewall's WAN client, by interface and reason. reason is dhclient-script(8)'s OWN closed vocabulary, lowercased (bound, renew, rebind, reboot, expire, fail, timeout, stop, release, preinit, arpcheck, arpsend, medium, nbi); an unrecognised token is never turned into a label. This is the lease STATE MACHINE rather than the wire traffic: renew is the healthy steady state, while expire and fail mean the client has given up on the lease and the uplink address is gone or going. Only renew is observed on the production box - a working WAN never reaches the failure reasons - so the rest are modelled from dhclient-script's documented set, not from capture.
Unix time at which this interface's WAN DHCP lease last successfully bound, from dhclient's 'bound to -- renewal in seconds' line. Set only by an actual bind, so it stands still between renewals by design - time() minus this is how long the current lease has been held. Read it alongside opnsense_log_events_dhcp_client_lease_renewal_timestamp_seconds, which is set from the same line. The leased address itself is deliberately NOT a label (it is this firewall's own public IP and changes on re-bind) and ships as dhcp_client.address on the log record.
Unix time at which this interface's WAN DHCP lease renewal is next due: the time of the last 'bound to' line plus the renewal interval that line carried. A DEADLINE, not a countdown, on purpose - a countdown gauge has to be recomputed against wall-clock at scrape time and a stale one is indistinguishable from a fresh one, which is the exact failure this metric exists to catch. Alert on it directly: ' - time() < 0' means the renewal deadline passed without dhclient reporting a new bind, which is the WAN losing its address in slow motion. This is a deliberate deviation from #541's proposed dhcp_client_lease_expiry_seconds countdown. Note this is the RENEWAL (T1) deadline dhclient reports, not the absolute lease expiry - dhclient does not log the latter, so it is not invented here.
--exporter.disable-log-events
opnsense_log_events_dhcp6c_message_total
Counter
interface, direction, type
DHCPv6 messages this firewall's OWN WAN client (dhcp6c) sent or received, by interface, direction and exchange type. This is the IPv6 twin of opnsense_log_events_dhcp_client_total and is deliberately a separate metric: a v4 and a v6 uplink fail independently, and folding them together would hide a v6-only outage behind a healthy v4 rate. direction is sent or received. type is a closed vocabulary resolved in code - solicit, request, renew, rebind, release, information_request - folded from dhcp6c's two spellings of the same exchange, so a Renew and the REPLY answering it share a type. The signal to watch is the ratio: a healthy client sends a Renew and gets a REPLY back, so sent renew rising while received renew stays flat is the v6 uplink going away, hours before the delegated prefix's valid lifetime expires. interface is EMPTY on a received message - dhcp6c's 'Received REPLY for RENEW' names none and it is resolved by correlating the daemon PID with the interface its preceding 'Sending Renew on ' named; empty means the exporter started mid-lease and never saw that line, not that the message had no interface. Only the grammars captured on the production box plus their siblings read off dhcp6c's own format strings are counted; any other dhcp6c line still ships as a log record but is never counted.
--exporter.disable-log-events
opnsense_log_events_dhcp6c_event_total
Counter
interface, event, reason
dhcp6c prefix-delegation, address-configuration and script events, by interface, closed event and script reason. event is one of prefix_created / prefix_updated (dhcp6c (re)established the delegated prefix), address_added / address_removed (an address derived from that prefix was configured on a downstream interface), or the four script_* events OPNsense's dhcp6c_script.sh logs: script_executing (the reason handler ran), script_connected (the client attached to a server on a REQUEST or SOLICIT), script_prefix_updated (the new prefix was pushed into the interface configuration) and script_ignored (a REASON the script does not handle). reason is dhcp6c_script's OWN closed REASON set lowercased onto the same vocabulary as dhcp6c_message_total's type, plus exit; it is EMPTY on the prefix and address events, which name none, and on script_ignored, whose REASON is by definition a token nobody has closed. INTERFACE MEANS DIFFERENT THINGS PER EVENT and the event label says which: the WAN dhcp6c is renewing on for the prefix and script events, and the DOWNSTREAM device the delegated prefix was applied to (ixl0, ixl0_vlan100) for the address ones - which is the honest reading of each line, since an address really is configured on the LAN device. The configured address and the delegated prefix are NOT labels and ship as dhcp6c.address and dhcp6c.prefix on the log record.
Unix time at which this interface's delegated IPv6 prefix was last created or refreshed, from dhcp6c's 'create
update a prefix / pltime=, vltime=' line. Set only by an actual delegation update, so it stands still between renewals by design - time() minus this is how long the current delegation has been held. Read it alongside the two expiry gauges set from the same line. THE PREFIX ITSELF IS DELIBERATELY NOT A LABEL even though it is this firewall's own: it changes on re-delegation, which is exactly the event these gauges watch for, so it would churn the series set during the incident. It ships as dhcp6c.prefix on the log record. prefix_length IS a label - it is the delegation SIZE, stable across a re-delegation, and the only thing distinguishing a second differently-sized delegation on the same WAN; two delegations of the SAME size on one interface collapse onto one series, last write wins.
Unix time at which this interface's delegated IPv6 prefix stops being PREFERRED: the time of the last prefix line plus the pltime it carried. A DEADLINE, not a countdown, on purpose - a countdown gauge has to be recomputed against wall-clock at scrape time and a stale one is indistinguishable from a fresh one, which is the exact failure this metric exists to catch. Alert on it directly: ' - time() < 0' means the preferred lifetime lapsed without dhcp6c reporting a refresh, so every downstream address derived from the prefix is now deprecated and new connections will stop choosing it. This is the EARLIER of the two deadlines and the one to page on: an ISP deprecating a prefix ahead of withdrawing it shortens pltime first, which is why the two lifetimes are kept as separate gauges rather than collapsed.
Unix time at which this interface's delegated IPv6 prefix stops being VALID: the time of the last prefix line plus the vltime it carried. Same deadline-not-countdown shape as the preferred gauge beside it, and the same alert form, ' - time() < 0'. This is the HARD one: when it passes, every address derived from the prefix is removed from every downstream interface and IPv6 is gone across the whole network at once - which is the failure mode with no IPv4 equivalent, because losing a v4 WAN lease costs the firewall one address while losing a delegation costs every LAN its addressing. Expect it to sit ahead of the preferred deadline; the two arriving equal is normal on an ISP that sends pltime == vltime.
Unix time at which this interface's OWN WAN IPv6 address (an IA_NA lease, not a delegated prefix) was last created or refreshed, from dhcp6c's 'create
update an address pltime=, vltime=' line (#560) - the address counterpart of opnsense_log_events_dhcp6c_prefix_updated_timestamp_seconds, for a WAN that takes its address directly by DHCPv6 rather than only a delegated prefix. Set only by an actual lease update, so it stands still between renewals by design. THE ADDRESS ITSELF IS DELIBERATELY NOT A LABEL even though it is this firewall's own: it changes on re-bind, which is one of the conditions this gauge watches for. It ships as dhcp6c.address on the log record. There is no prefix_length dimension here - a single address has none.
Unix time at which this interface's OWN WAN IPv6 address stops being PREFERRED: the time of the last address-lease line plus the pltime it carried. A DEADLINE, not a countdown, same reasoning as the prefix gauge beside it - a countdown recomputed at scrape time is indistinguishable from a stale one. Alert on it directly: ' - time() < 0'.
Unix time at which this interface's OWN WAN IPv6 address stops being VALID: the time of the last address-lease line plus the vltime it carried. Same deadline-not-countdown shape as the preferred gauge beside it. When it passes without a refresh, dhcp6c has lost the WAN address itself. THIS SERIES CAN DISAPPEAR ON PURPOSE: unlike every other gauge in this family, an explicit 'remove an address ' line (no lifetime, no interface) clears this interface's row entirely rather than leaving a frozen deadline in place - a frozen gauge would read as a healthy lease that simply stopped renewing.
--exporter.disable-log-events
opnsense_log_events_dhcp6_alloc_fail_total
Counter
reason
DHCPv6 lease allocations this firewall's kea-dhcp6 SERVER refused, by closed reason - a LAN client that asked for an IPv6 address or prefix and did not get one. reason is no_pools (not one configured pool was usable for that client, normally a client-class or configuration problem) or exhausted (pools were tried and every candidate was already taken). THIS IS EXACTLY ONE INCREMENT PER FAILED ALLOCATION, and that is the whole design: Kea emits a BURST of up to three ALLOC_ENGINE_V6_ALLOC_FAIL_* lines for one failure, all sharing one transaction id, so counting every line would report three failures for one. Only the CAUSE line is counted - alloc_engine.cc guarantees exactly one of ALLOC_ENGINE_V6_ALLOC_FAIL_NO_POOLS and the bare ALLOC_ENGINE_V6_ALLOC_FAIL fires per failure - while the scope line (SUBNET or SHARED_NETWORK) and the optional CLASSES line ship as log records and count nothing. The client's DUID, the transaction id and the subnet are NOT labels: a DUID is unbounded and identifies a client, a tid is unique per exchange and would mint a series per failure, and the subnet is an IPv6 prefix. All three ship as dhcp.duid / dhcp.tid / dhcp.alloc_fail_subnet on the log record, which is where to look to find out WHICH client. Distinct from opnsense_log_events_dhcp6c_* above, which is this firewall's own WAN client rather than the server it runs for the LAN.
--exporter.disable-log-events
opnsense_log_events_cardinality_capped_total
Counter
family
Log events counted into a family's overflow total instead of their own series, because the label tuple was new and the family already held --logs.max-metric-keys distinct tuples. Both receivers are push-based (and syslog over UDP has a spoofable source), so tuple values are sender-controlled and the budget is what stops one sender growing metric state for the life of the process. Nothing is lost: this plus the family's own series is the true event count. Non-zero and rising means the family is saturated and new tuples are no longer individually visible - raise the budget or find what is minting them.
--exporter.disable-log-events
opnsense_log_events_cardinality_keys
Gauge
family
Distinct label tuples currently tracked for each log_events family. Compare against --logs.max-metric-keys to see saturation coming before opnsense_log_events_cardinality_capped_total starts rising. Tuples are never evicted, so this only grows within a process lifetime.
--exporter.disable-log-events
opnsense_log_events_observation_dropped_total
Counter
reason
Derived log-metric observations refused by the non-blocking receiver handoff. A refused syslog observation retains its raw record so sampling cannot discard an uncounted event.
Maximum number of mbufs (the mbuf pool's ceiling, mirroring cluster_max). May read 0 on OPNsense >=26.1.11, where upstream removed this key -- that means no ceiling was reported, not a ceiling of zero, so guard any current/max ratio against a zero denominator (#543).
--exporter.disable-mbuf
opnsense_mbuf_cluster_current
Gauge
---
Current number of mbuf clusters in use
--exporter.disable-mbuf
opnsense_mbuf_cluster_cache
Gauge
---
Number of mbuf clusters in cache
--exporter.disable-mbuf
opnsense_mbuf_cluster_total
Gauge
---
Total number of mbuf clusters available
--exporter.disable-mbuf
opnsense_mbuf_cluster_max
Gauge
---
Maximum number of mbuf clusters
--exporter.disable-mbuf
opnsense_mbuf_failures_total
Counter
type
Total number of mbuf allocation failures by type
--exporter.disable-mbuf
opnsense_mbuf_sleeps_total
Counter
type
Total number of mbuf allocation sleeps by type
--exporter.disable-mbuf
opnsense_mbuf_bytes_in_use
Gauge
---
Number of bytes of memory currently in use by mbufs
--exporter.disable-mbuf
opnsense_mbuf_bytes_total
Gauge
---
Total number of bytes of memory available for mbufs
--exporter.disable-mbuf
opnsense_mbuf_bytes_in_cache
Gauge
---
Number of bytes of memory sitting in the mbuf allocator's cache -- already charged to the mbuf/cluster/jumbo pools but not currently in use, so available for immediate reuse without a new system allocation. Complements bytes_in_use (currently used) and bytes_total (the ceiling both draw from) (#579).
--exporter.disable-mbuf
opnsense_mbuf_pool_current
Gauge
pool
Current number of items in use in this secondary mbuf pool: jumbo9 (9k jumbo clusters), jumbo16 (16k jumbo clusters), or packet (the secondary zone that pre-combines an mbuf+cluster for m_getcl()). These pools are only reported on OPNsense releases whose underlying FreeBSD netstat -m emits them (26.1+); a pool missing from a scrape means the box's release predates it, not that the pool is empty -- distinguish via absence, never read a missing pool as zero (#579).
--exporter.disable-mbuf
opnsense_mbuf_pool_cache
Gauge
pool
Number of items sitting free in this secondary mbuf pool's cache, ready for immediate reuse without a new system allocation. For pool=\"packet\" this resolves netstat's packet-free field -- its own human-readable text labels the line \"(current/cache)\", the same current/cache shape as the jumbo9/jumbo16 pools, just a differently-spelled JSON key (#579).
--exporter.disable-mbuf
opnsense_mbuf_pool_total
Gauge
pool
Total number of items ever allocated to this secondary mbuf pool. NEVER reported for pool=\"packet\": that zone borrows memory from the mbuf and cluster zones rather than owning its own allocation, so upstream's netstat -m has no packet-total key and no series is emitted for it (#579).
--exporter.disable-mbuf
opnsense_mbuf_pool_max
Gauge
pool
Configured ceiling on this secondary mbuf pool. NEVER reported for pool=\"packet\", for the same reason as pool_total. jumbo16's ceiling is read from upstream's jumbo16-limit key and normalised onto this same metric even though jumbo9's equivalent key is spelled jumbo9-max -- verified against FreeBSD's usr.bin/netstat/mbuf.c: both fields come from an otherwise-identical xo_emit format string whose human-readable label reads \"(current/cache/total/max)\" in both cases, so the two JSON keys are one quantity under an inconsistent upstream name, not two different things (#579).
One series per NDP neighbour entry (value is always 1). Only emitted when --exporter.enable-ndp-details is set (high, churning cardinality from IPv6 privacy addresses). manufacturer is the OUI lookup for the MAC and is the label that identifies a device. device is the raw kernel device and interface_description the assigned name; they diverge on VLAN children and bridges, and only device joins against the interfaces metrics. type reads empty on OPNsense 26.1, which sends no type key on this endpoint.
Reachability register of the NTP peer (octal decoded to decimal)
--exporter.disable-ntp
opnsense_ntp_peer_delay_milliseconds
Gauge
server
Round-trip delay to the NTP peer in milliseconds
--exporter.disable-ntp
opnsense_ntp_peer_offset_milliseconds
Gauge
server
Clock offset relative to the NTP peer in milliseconds
--exporter.disable-ntp
opnsense_ntp_peer_jitter_milliseconds
Gauge
server
Dispersion jitter of the NTP peer in milliseconds
--exporter.disable-ntp
opnsense_ntp_peers_total
Gauge
---
Total number of NTP peers
--exporter.disable-ntp
opnsense_ntp_gps_ok
Gauge
---
Whether the last NMEA sentence from a GPS refclock reported a valid fix (1 = ok, 0 = no fix). EXPERIMENTAL: derived from OPNsense source, not validated against real GPS hardware (#224). Absent entirely when no GPS refclock is attached/reporting.
--exporter.disable-ntp
opnsense_ntp_gps_satellites
Gauge
---
Number of satellites used in the last GPS fix ($GPGGA sentences only; absent for sentences that don't carry a satellite count). EXPERIMENTAL: derived from OPNsense source, not validated against real GPS hardware (#224).
Whether this node's netbird daemon has an active connection to the management server (1 = connected, 0 = not)
--exporter.disable-netbird
opnsense_netbird_signal_connected
Gauge
---
Whether this node's netbird daemon has an active connection to the signal server (1 = connected, 0 = not)
--exporter.disable-netbird
opnsense_netbird_relays_total
Gauge
---
Number of relay servers known to this node's netbird daemon
--exporter.disable-netbird
opnsense_netbird_relays_available
Gauge
---
Number of relay servers currently reachable from this node's netbird daemon
--exporter.disable-netbird
opnsense_netbird_peers_total
Gauge
---
Number of netbird network peers known to this node
--exporter.disable-netbird
opnsense_netbird_peers_connected
Gauge
---
Number of netbird network peers this node currently has an active WireGuard connection to
--exporter.disable-netbird
opnsense_netbird_service_running
Gauge
---
Whether the netbird plugin service is running (1 = running, 0 = stopped/disabled)
--exporter.disable-netbird
opnsense_netbird_info
Gauge
cli_version, daemon_version
NetBird node version information (value is always 1; see labels). Only emitted when the daemon reports version data (absent while the daemon itself is down)
--exporter.disable-netbird
opnsense_netbird_daemon_state
Gauge
state
Current state of this node's netbird daemon (#455; always 1; exactly one series per scrape). state is drawn from netbird's closed DaemonStatus vocabulary - Idle, Connecting, Connected, NeedsLogin, LoginFailed, SessionExpired - and anything else, including a future upstream state, collapses to unknown. Idle is a normal lazy-connection state, NOT a fault; NeedsLogin/SessionExpired mean the peer needs operator re-authentication. Only emitted when the daemon reported a daemonStatus (absent while the daemon itself is down).
--exporter.disable-netbird
opnsense_netbird_peer_connected
Gauge
fqdn
Whether this node currently has an active WireGuard connection to the peer (1 = connected, 0 = not). Only emitted when --exporter.enable-netbird-details is set.
--exporter.disable-netbird
opnsense_netbird_peer_direct
Gauge
fqdn
Whether the connection to the peer uses a direct P2P path (1 = direct, 0 = relayed). Only emitted for peers with an active connection and when --exporter.enable-netbird-details is set.
--exporter.disable-netbird
opnsense_netbird_peer_received_bytes_total
Counter
fqdn
Bytes received from this peer since the netbird daemon started. Only emitted when --exporter.enable-netbird-details is set.
--exporter.disable-netbird
opnsense_netbird_peer_transmitted_bytes_total
Counter
fqdn
Bytes transmitted to this peer since the netbird daemon started. Only emitted when --exporter.enable-netbird-details is set.
Unix timestamp of the last WireGuard handshake with this peer. Only emitted for peers with an active connection and a recorded handshake, and when --exporter.enable-netbird-details is set.
--exporter.disable-netbird
opnsense_netbird_peer_latency_seconds
Gauge
fqdn
Measured round-trip latency to this peer. Only emitted for peers with an active connection and a recorded latency, and when --exporter.enable-netbird-details is set.
Whether local netflow collection is enabled (1 = enabled, 0 = disabled)
--exporter.enable-netflow
opnsense_netflow_active
Gauge
---
Whether the netflow service is active (1 = active, 0 = inactive)
--exporter.enable-netflow
opnsense_netflow_collectors_count
Gauge
---
Number of active netflow collectors
--exporter.enable-netflow
opnsense_netflow_cache_packets_total
Counter
interface
Total packets observed in netflow cache by interface
--exporter.enable-netflow
opnsense_netflow_cache_source_ip_addresses
Gauge
interface
Number of unique source IP addresses in netflow cache by interface
--exporter.enable-netflow
opnsense_netflow_cache_destination_ip_addresses
Gauge
interface
Number of unique destination IP addresses in netflow cache by interface
--exporter.enable-netflow
opnsense_netflow_capture_expected
Gauge
interface
Whether the box is configured to capture NetFlow on this interface (1 = selected, 0 = listed in the config but not selected). This is the half no counter can supply: an interface producing nothing is indistinguishable from an idle one unless you know it was supposed to be producing something. The set is closed - it comes from the firewall's own netflow config - so cardinality is bounded by the number of interfaces. An interface with records but capture_expected=0 is NOT a fault: ng_netflow names the far side of a flow from a FIB lookup, so an interface can be labelled without being captured on.
--exporter.enable-netflow
opnsense_netflow_capture_last_record_seconds
Gauge
interface
Seconds since this exporter last received a NetFlow record attributed to this interface. A RAW AGE, not a verdict: a guest VLAN can be legitimately silent for hours, so the threshold is the operator's to choose - compare against capture_active_timeout_seconds. Interfaces that have produced NO record since the exporter started are ABSENT here rather than reported as a large age, because every interface is silent at startup and a number there would make a fresh boot look like a box-wide outage; \"never seen\" and \"seen, then stopped\" are different states. IMPORTANT: a fresh age proves the interface is being NAMED, not that its own capture hook is alive - ng_netflow fills one side of each flow from a FIB lookup, so records captured elsewhere name it too. For per-hook liveness use opnsense_netflow_cache_packets_total, which is the box's own per-node view.
--exporter.enable-netflow
opnsense_netflow_capture_active_timeout_seconds
Gauge
---
The box's configured ACTIVE flow timeout: how long a long-running flow sits in the cache before ng_netflow exports it anyway. Exported so a \"this interface has gone quiet\" threshold is derived from what the firewall actually applies rather than guessed - an interface cannot be judged silent until well past this.
--exporter.enable-netflow
opnsense_netflow_capture_inactive_timeout_seconds
Gauge
---
The box's configured INACTIVE flow timeout: how long an idle flow waits before export. The floor on how quickly any interface can be observed to have stopped.
Total number of netisr hybrid dispatches by protocol
--exporter.enable-network-diagnostics
opnsense_network_diag_netisr_queued_total
Counter
protocol
Total number of netisr packets queued by protocol
--exporter.enable-network-diagnostics
opnsense_network_diag_netisr_handled_total
Counter
protocol
Total number of netisr packets handled by protocol
--exporter.enable-network-diagnostics
opnsense_network_diag_netisr_queue_drops_total
Counter
protocol
Total number of netisr queue drops by protocol
--exporter.enable-network-diagnostics
opnsense_network_diag_netisr_queue_length
Gauge
protocol
Current maximum netisr queue length across workstreams by protocol
--exporter.enable-network-diagnostics
opnsense_network_diag_netisr_queue_watermark
Gauge
protocol
High watermark of netisr queue length across workstreams by protocol
--exporter.enable-network-diagnostics
opnsense_network_diag_netisr_queue_limit
Gauge
protocol
Configured netisr queue limit by protocol
--exporter.enable-network-diagnostics
opnsense_network_diag_netisr_protocol_info
Gauge
protocol, protocol_id, policy, policy_type, flags
Static netisr protocol configuration (value is always 1). Join on protocol to interpret the other netisr series. policy_type is the one that matters: cpu and flow protocols are meant to spread across every netisr workstream, whereas source protocols are single-lane BY DESIGN and will always show one active workstream — alerting on workstream imbalance without excluding them fires falsely on igmp/rtsock/arp on every box. policy is the dispatch mode (direct/hybrid/deferred/default) and flags the raw netisr flag string.
--exporter.enable-network-diagnostics
opnsense_network_diag_netisr_active_workstreams
Gauge
protocol
Number of netisr workstreams that have carried any traffic for this protocol (handled, queued, dispatched or hybrid-dispatched greater than zero). Compare against the CPU count: a cpu-policy protocol showing 4 on a 12-core box means eight cores do no work for it at all, which is a net.isr.maxthreads/bindthreads or NIC RSS problem, not a queue-size problem. A value of 1 is normal and expected for source-policy protocols.
--exporter.enable-network-diagnostics
opnsense_network_diag_netisr_workstreams_at_limit
Gauge
protocol
Number of netisr workstreams whose high watermark has reached this protocol's configured queue limit. Non-zero means at least one lane has actually run out of queue at some point since boot. Because the watermark is a since-boot high water mark and never decays, this stays non-zero after the condition clears — it records that it happened, not that it is happening. Always 0 when the queue limit is 0 (unset).
Maximum netisr queue watermark divided by the mean watermark, computed over ACTIVE workstreams only. 1.0 is a perfectly even spread across the lanes that are working; higher means one lane absorbs disproportionately more depth. Idle workstreams are excluded on purpose — including them would dilute the mean and make an affinity problem look milder the more CPUs the box has. Emitted as 0 when the ratio is undefined: fewer than two active workstreams, or every active watermark still zero. Read alongside netisr_active_workstreams, which is what tells you whether a 0 means 'even' or 'single-lane'.
Largest per-workstream netisr queue-drop count divided by the total across all workstreams. 1.0 means every drop landed on a single lane, which points at CPU affinity — raising net.isr.maxqlen would mask the symptom rather than fix it. A value near 1/N means the drops are spread and the protocol is genuinely over its queue limit. Emitted as 0 when nothing has dropped, so gate any alert on the drop counter itself, not on this ratio alone.
--exporter.enable-network-diagnostics
opnsense_network_diag_netisr_cpu_dispatched_total
Counter
protocol, cpu, workstream
Packets dispatched directly on this netisr workstream, without being queued. Per-workstream breakdown of netisr_dispatched_total.
Packets hybrid-dispatched on this netisr workstream (handled inline because the queue was empty). Per-workstream breakdown of netisr_hybrid_dispatched_total.
--exporter.enable-network-diagnostics
opnsense_network_diag_netisr_cpu_queued_total
Counter
protocol, cpu, workstream
Packets enqueued onto this netisr workstream for deferred processing. Per-workstream breakdown of netisr_queued_total.
--exporter.enable-network-diagnostics
opnsense_network_diag_netisr_cpu_handled_total
Counter
protocol, cpu, workstream
Packets processed by this netisr workstream. Per-workstream breakdown of netisr_handled_total. A row is emitted for EVERY workstream the box reports, including ones that are entirely idle — a run of zero-valued CPUs is the finding, not missing data, and suppressing those rows would hide exactly the affinity problem this breakdown exists to show. Labelled with both workstream (the netisr stream index) and cpu (the core it is bound to); they coincide on a default configuration but are not the same thing when net.isr.bindthreads is off.
Packets dropped because this netisr workstream's queue was full. Per-workstream breakdown of netisr_queue_drops_total — this is the series that shows whether drops are concentrated on one core or spread across all of them.
--exporter.enable-network-diagnostics
opnsense_network_diag_netisr_cpu_queue_length
Gauge
protocol, cpu, workstream
Instantaneous depth of this netisr workstream's queue at scrape time. Almost always 0 on a healthy box and a poor sampling of a bursty signal — use netisr_cpu_queue_watermark for whether the queue has ever filled.
--exporter.enable-network-diagnostics
opnsense_network_diag_netisr_cpu_queue_watermark
Gauge
protocol, cpu, workstream
Highest queue depth this netisr workstream has ever reached. A since-boot high water mark: it never decays, so it records that the queue filled, not that it is full now. Equal to the protocol's queue limit means this lane has hit its ceiling and dropped.
--exporter.enable-network-diagnostics
opnsense_network_diag_sockets_active
Gauge
type
Number of active sockets by type
--exporter.enable-network-diagnostics
opnsense_network_diag_sockets_unix_total
Gauge
---
Total number of active Unix domain sockets
--exporter.enable-network-diagnostics
opnsense_network_diag_routes_total
Gauge
proto
Number of routing table entries by protocol
--exporter.enable-network-diagnostics
opnsense_network_diag_interface_routes
Gauge
proto, device, interface
Number of routing table entries whose egress interface is this one, by address family. Per-interface breakdown of routes_total, which remains the sum. device is the raw kernel device and interface the assigned description; they diverge on VLAN children, PPPoE links and unassigned devices, and only device joins against the interfaces metrics. A count collapsing to near zero on a WAN device is a link that has dropped its learned routes.
--exporter.enable-network-diagnostics
opnsense_network_diag_routes_by_flags
Gauge
proto, flags
Number of routing table entries carrying this exact netstat flag string, by address family. The flag vocabulary is closed and short, so this stays bounded regardless of table size. The letters are netstat's: U up, G gatewayed, H host route, S static, B blackhole (traffic silently discarded), 1 protocol-specific. B is the one worth watching — blackhole routes are usually deliberate but a growing count is traffic being dropped with no other signal. The flags are matched as a whole string, not per letter, so use a regex matcher to select a single flag.
--exporter.enable-network-diagnostics
opnsense_network_diag_default_route_present
Gauge
proto
Whether a default route exists for this address family (1) or not (0). Losing the default route is a total-outage condition for that family with no other signal in this exporter, so the series is emitted as 0 rather than disappearing — alert on == 0 directly, no absent() rule needed. Emitted for both ipv4 and ipv6 on every scrape; a box with no IPv6 upstream legitimately and permanently reads 0 for ipv6, so scope any alert to the families the box is expected to route.
--exporter.enable-network-diagnostics
opnsense_network_diag_default_route_info
Gauge
proto, device, interface, gateway
Identity of each installed default route (value is always 1): which interface it leaves by and which gateway it points at. There is normally exactly one series per address family; two or more means multiple default routes are installed. The gateway IS carried as a label here — unlike every other route, of which there may be hundreds and whose destinations churn — because a change of default gateway is itself the event an operator wants to see. No series is emitted for a family with no default route; read default_route_present for that.
Average response time of this upstream server in seconds
--exporter.disable-nginx
opnsense_nginx_server_zone_cache_responses_total
Counter
zone, cache_status
Cumulative responses by cache status for this server zone (only when the vts build reports cache status)
--exporter.disable-nginx
opnsense_nginx_server_zone_request_seconds_total
Counter
zone
Cumulative sum of request processing time in seconds for this server zone
--exporter.disable-nginx
opnsense_nginx_server_zone_counter_wraps_total
Counter
zone
Cumulative count of times one of this server zone's own vts counters (requests/bytes/response-code/request-time) has been detected to wrap past its accumulated value (nginx-module-vts's own overCounts wrap-detection counter). A non-zero value here means a rate() over this zone's other counters has a discontinuity at the wrap point.
Cumulative count of times one of this upstream server's own vts counters has been detected to wrap past its accumulated value (nginx-module-vts's own overCounts wrap-detection counter). See server_zone_counter_wraps_total for the sibling metric on server zones.
--exporter.disable-nginx
opnsense_nginx_cache_zone_max_bytes
Gauge
zone
Maximum size of this proxy_cache_path zone in bytes
--exporter.disable-nginx
opnsense_nginx_cache_zone_used_bytes
Gauge
zone
Currently used bytes of this proxy_cache_path zone
--exporter.disable-nginx
opnsense_nginx_cache_zone_bytes_in_total
Counter
zone
Cumulative bytes received for this cache zone
--exporter.disable-nginx
opnsense_nginx_cache_zone_bytes_out_total
Counter
zone
Cumulative bytes sent for this cache zone
--exporter.disable-nginx
opnsense_nginx_cache_zone_responses_total
Counter
zone, cache_status
Cumulative responses by cache status for this cache zone
--exporter.disable-nginx
opnsense_nginx_config_load_timestamp_seconds
Gauge
---
Unix timestamp of the last nginx config (re)load, as reported by the vts module
OpenVPN instances (1 = enabled, 0 = disabled) by role (server, client)
--exporter.disable-openvpn
opnsense_openvpn_instance_max_clients
Gauge
uuid, role, description, device_type
Configured concurrent-client cap for this OpenVPN server instance. Divide opnsense_openvpn_sessions_by_instance by this (joined on description) for a utilization ratio. Only emitted for a server instance with a cap actually configured; an unlimited/uncapped server emits no series here.
Cumulative TCP connections dropped by reason (retransmit_timeout, persist_timeout, finwait2_timeout, keepalive). These are kernel-lifetime counters that reset on reboot, like the other protocol counters. A reason is only emitted when the box reports that wire field; an older box that omits a reason omits its series rather than reporting a fabricated zero (#374).
Total times TCP entered SACK-based loss recovery. Each episode is one burst of loss the peer reported via selective ACK, however many segments it covered — so this counts loss EVENTS while tcp_sack_segment_retransmits_total counts their cost. A rising rate here with a flat retransmit rate means frequent small losses; the reverse means rare but severe ones.
Total TCP segments retransmitted during SACK recovery. A subset of tcp_retransmitted_packets_total (which also counts plain RTO-driven retransmission) — the two overlap, so never sum them.
Total bytes retransmitted during TCP SACK recovery. A subset of tcp_retransmitted_bytes_total; the two overlap, so never sum them. Compare against tcp_sent_data_bytes_total for a SACK-driven retransmission ratio — a sustained rise is a direct TCP-health signal on a box whose job is forwarding packets.
--exporter.disable-protocol
opnsense_protocol_tcp_sack_blocks_total
Counter
direction
Total TCP SACK option blocks, by direction. direction=\"received\" is the peer telling us about holes in the data WE sent (outbound loss); direction=\"sent\" is us telling the peer about holes in the data IT sent (inbound loss). The direction that rises tells you which way the lossy path runs, which no other metric on this box distinguishes.
Total times the TCP SACK scoreboard overflowed and the kernel stopped tracking individual holes in a connection, falling back to coarser recovery. Should be flat at zero; any sustained rise means loss severe enough that SACK recovery is degrading rather than helping.
Total TCP retransmissions that were themselves lost, detected during SACK recovery. This is the severe case: the path dropped the repair packet too, so recovery stalls until a timeout fires and the connection visibly hangs. No other counter on this box distinguishes a lost repair from ordinary loss. A subset of tcp_retransmitted_packets_total and of tcp_sack_segment_retransmits_total — all three overlap, so never sum them.
Total TCP segments retransmitted as part of a TSO (TCP segmentation offload) chunk during SACK recovery, i.e. repairs the NIC re-segmented rather than the kernel. Low signal on its own; it exists so the sack section is modelled completely and the split between offloaded and kernel-driven repair is visible when tuning TSO. A subset of tcp_sack_segment_retransmits_total — the two overlap, so never sum them.
Total entries added to the TCP host cache, which remembers per-peer RTT/ssthresh so a new connection to a known peer warm-starts instead of re-probing. The add rate is a proxy for distinct-peer churn, not for connection volume: a repeat peer is a cache hit and adds nothing.
Total TCP host cache insertions that overflowed a hash bucket and evicted an existing entry. Nonzero means the cache is under churn pressure and the kernel is losing cached path metrics, so affected connections restart from defaults and re-probe the path.
--exporter.disable-protocol
opnsense_protocol_tcp_hostcache_hits_total
Counter
metric
Total TCP connections that warm-started a path metric from the host cache instead of re-probing it, by which metric was reused: \"rtt\" (round-trip time), \"rttvar\" (RTT variance) and \"ssthresh\" (slow-start threshold). Despite the name these are NOT part of the statistics.tcp.hostcache section — they are top-level counters (connections-hostcache-*) and are the hit side of the cache whose insert and eviction rates tcp_hostcache_entries_added_total and tcp_hostcache_buffer_overflows_total report. A high hit rate is good: it means repeat peers skip re-probing. Hits falling while tcp_hostcache_buffer_overflows_total rises means eviction pressure is destroying the cache's value. The three are counted independently, so they do not sum to a connection count.
--exporter.disable-protocol
opnsense_protocol_tcp_timewait_events_total
Counter
event
Total TCP TIME_WAIT state events, by kind. \"responds\" = a segment answered from TIME_WAIT (usually a retransmitted FIN, normal); \"recycles\" = a TIME_WAIT entry reused early to make room, which means state pressure from short-lived connection churn; \"resets\" = an RST issued from TIME_WAIT. Only recycles and resets indicate a problem — responds is ordinary close churn.
Total TCP path-MTU-discovery blackhole detection events, by kind. A PMTUD blackhole is a path that silently drops oversized packets without returning the ICMP \"fragmentation needed\" that would let TCP shrink its MSS — the classic cause of \"some sites load, some hang\", and invisible from outside the box. \"activated\" = the kernel suspected a blackhole and dropped MSS; \"activated_min_mss\" = it fell all the way back to the minimum MSS, so the path is badly broken and throughput will suffer; \"failed\" = shrinking the MSS did not fix it. Any sustained rise warrants investigating MTU on the upstream path.
--exporter.disable-protocol
opnsense_protocol_tcp_signature_total
Counter
result
Total TCP-MD5 (RFC 2385) signature outcomes, by result. In practice TCP-MD5 authenticates BGP sessions, so on a box with no MD5-authenticated peer every result is permanently zero and that is correct, not a fault. \"good\" is the healthy denominator — its rate falling to zero is how an authenticated peer going quiet shows up. \"bad\" means a wrong or rotated key (or spoofing); \"make_failed\" is a local failure to generate a signature, i.e. a missing key in the kernel keytable; \"not_expected\" and \"not_provided\" are the two halves of an asymmetric configuration, where one side is signing and the other is not.
Number of SMART-monitored devices enumerated by the os-smart plugin
--exporter.enable-smart
opnsense_smart_device_health
Gauge
device, model, serial
SMART overall health assessment (1 = passed, 0 = failed)
--exporter.enable-smart
opnsense_smart_device_temperature_celsius
Gauge
device
Current drive temperature in degrees Celsius
--exporter.enable-smart
opnsense_smart_device_power_on_hours
Gauge
device
Total power-on hours reported by the drive
--exporter.enable-smart
opnsense_smart_attribute_value
Gauge
device, attribute_name, attribute_id
Normalised current value of a SATA SMART attribute
--exporter.enable-smart
opnsense_smart_attribute_worst
Gauge
device, attribute_name, attribute_id
Worst recorded normalised value of a SATA SMART attribute
--exporter.enable-smart
opnsense_smart_attribute_threshold
Gauge
device, attribute_name, attribute_id
Failure threshold of a SATA SMART attribute (normalised value at/below this indicates failure)
--exporter.enable-smart
opnsense_smart_attribute_raw
Gauge
device, attribute_name, attribute_id
Raw value of a SATA SMART attribute (e.g. reallocated sector count, total LBAs written)
--exporter.enable-smart
opnsense_smart_nvme_available_spare_percent
Gauge
device
NVMe remaining spare capacity as a percentage
--exporter.enable-smart
opnsense_smart_nvme_percentage_used
Gauge
device
NVMe vendor estimate of device life used as a percentage (may exceed 100)
--exporter.enable-smart
opnsense_smart_nvme_media_errors_total
Counter
device
NVMe count of unrecovered data-integrity errors
--exporter.enable-smart
opnsense_smart_nvme_unsafe_shutdowns_total
Counter
device
NVMe count of unsafe shutdowns
--exporter.enable-smart
opnsense_smart_nvme_data_units_read_total
Counter
device
NVMe data units read (1 unit = 1000 × 512 bytes)
--exporter.enable-smart
opnsense_smart_nvme_data_units_written_total
Counter
device
NVMe data units written (1 unit = 1000 × 512 bytes)
--exporter.enable-smart
opnsense_smart_device_rotation_rate_rpm
Gauge
device
Drive rotation speed in RPM as reported by the drive itself. 0 explicitly means solid-state (no spinning platter); any other value is the platter's actual RPM. Use this to pick which wear/temperature thresholds apply — an ABSENT series means the drive didn't report this field at all, which is not the same as a genuine 0 (#577).
--exporter.enable-smart
opnsense_smart_device_spare_available_percent
Gauge
device
SSD spare/reserve blocks remaining, as a percentage of the original spare pool. smartctl derives this by matching vendor-specific reallocated-sector/spare-block attributes, so it is only reported for drives it can normalize — falling toward the drive's own threshold means the wear-leveling reserve is running out (#577).
--exporter.enable-smart
opnsense_smart_device_endurance_used_percent
Gauge
device
SSD endurance used, normalized by smartctl from vendor-specific wear-leveling attributes (0-100+; values above 100 mean the drive has exceeded its rated write endurance and failure risk keeps rising). Only reported for drives smartctl can normalize this from (#577).
--exporter.enable-smart
opnsense_smart_attribute_failed
Gauge
device, attribute_name, attribute_id, when_failed
Emitted with value 1 ONLY for a SATA SMART attribute whose own when_failed marker is non-empty — i.e. that specific attribute, not just the drive's overall smart_status, has failed its threshold now or in the past. when_failed carries the raw smartctl value (\"now\" or \"past\"). A healthy attribute emits no series at all (absence means \"never failed\", not \"unknown\"), so a clean fleet adds ~0 cardinality. This is deliberately a separate metric rather than a new label on attribute_value/worst/threshold/raw: adding a label to those would change their series identity and break continuity of every existing panel/rule reading them (#577).
--exporter.enable-smart
opnsense_smart_device_info_errors
Gauge
reason
Devices whose smartInfo payload could not be fully read in the last poll, by reason (failed = nothing decoded, partial = schema disagreement)
Messages processed by this syslog-ng object since stats reset
--exporter.disable-syslog
opnsense_syslog_dropped_total
Counter
source_name, source_id, source_instance
Messages dropped by this syslog-ng object since stats reset
--exporter.disable-syslog
opnsense_syslog_written_total
Counter
source_name, source_id, source_instance
Messages written by this syslog-ng object since stats reset
--exporter.disable-syslog
opnsense_syslog_queued
Gauge
source_name, source_id, source_instance
Messages currently queued in this syslog-ng object
--exporter.disable-syslog
opnsense_syslog_truncated_messages_total
Counter
source_name, source_id, source_instance
Messages truncated by this syslog-ng object since stats reset
--exporter.disable-syslog
opnsense_syslog_truncated_bytes_total
Counter
source_name, source_id, source_instance
Bytes truncated by this syslog-ng object since stats reset
--exporter.disable-syslog
opnsense_syslog_memory_usage_bytes
Gauge
source_name, source_id, source_instance
Current memory usage of this syslog-ng object in bytes
--exporter.disable-syslog
opnsense_syslog_events_per_second
Gauge
source_name, source_id, source_instance, window
syslog-ng events per second over the labelled window (1h, 24h, since_start)
--exporter.disable-syslog
opnsense_syslog_message_size_bytes
Gauge
source_name, source_id, source_instance, stat
syslog-ng message size in bytes (stat = avg or max)
--exporter.disable-syslog
opnsense_syslog_target_state
Gauge
source_name, source_id, source_instance, state
Current lifecycle state of a syslog-ng source/target object (always 1; one series per SourceName/SourceId/SourceInstance). state is drawn from syslog-ng's closed state vocabulary -- active (currently alive and receiving stat updates), dynamic (a runtime-created object that may cease to exist), orphaned (the underlying config element was removed but its last-known counters are retained) -- and anything unrecognized collapses to unknown. A target's byte/message counters going flat while this reads orphaned (rather than active) is exactly the stall this metric exists to distinguish from an idle-but-fine target.
--exporter.disable-syslog
opnsense_syslog_service_running
Gauge
---
Whether the syslog-ng service is running (1 = running, 0 = stopped/disabled)
Unix timestamp at which the firewall booted, taken from the API's own boottime value rather than derived from uptime. Anchors the reboot dashboard annotation (#421): a query-time time()-uptime is recomputed on every evaluation and drifts between them, which moves the marker. Absent when the systemTime sub-call failed or boottime was unparseable.
Whether the Tailscale plugin service is running (1 = running, 0 = stopped/disabled)
--exporter.disable-tailscale
opnsense_tailscale_backend_running
Gauge
---
Whether the tailscaled backend state is Running (1 = Running, 0 = anything else)
--exporter.disable-tailscale
opnsense_tailscale_info
Gauge
version, relay
Tailscale node information (value is always 1; see labels)
--exporter.disable-tailscale
opnsense_tailscale_peers_total
Gauge
---
Number of tailnet peers known to this node
--exporter.disable-tailscale
opnsense_tailscale_peers_with_active_session
Gauge
---
Number of tailnet peers with an established WireGuard session from this node (derived from local last-handshake presence, not coordination-server online state)
--exporter.disable-tailscale
opnsense_tailscale_health_warnings
Gauge
---
Number of live health warning strings reported by the local tailscaled client (e.g. update available, DERP unreachable, key expiry). The warning text itself is never exported as a label.
--exporter.disable-tailscale
opnsense_tailscale_peer_session_active
Gauge
peer
Whether this node has an established WireGuard session with the peer (1 = a handshake has been recorded since tailscaled start). Node-local; deliberately not the coordination-server online flag. Only emitted when --exporter.enable-tailscale-peer-details is set.
--exporter.disable-tailscale
opnsense_tailscale_peer_direct
Gauge
peer
Whether the established session to the peer uses a direct (non-relayed) path (1 = direct, 0 = DERP-relayed). Only emitted for peers with a WireGuard session and when --exporter.enable-tailscale-peer-details is set.
--exporter.disable-tailscale
opnsense_tailscale_peer_rx_bytes_total
Counter
peer
Bytes received from this peer by this node since tailscaled start. Only emitted when --exporter.enable-tailscale-peer-details is set.
--exporter.disable-tailscale
opnsense_tailscale_peer_tx_bytes_total
Counter
peer
Bytes sent to this peer by this node since tailscaled start. Only emitted when --exporter.enable-tailscale-peer-details is set.
Unix timestamp of the last WireGuard handshake with this peer from this node. Only emitted when --exporter.enable-tailscale-peer-details is set.
--exporter.disable-tailscale
opnsense_tailscale_reauth_required
Gauge
---
Whether tailscaled is parked waiting for an interactive login (1 = it is holding an auth URL). The tunnel stays down until a human completes the login - no restart or retry clears it. Distinct from backend_running, which reads 0 for this and every other reason. The auth URL itself is a credential and is never exported, in a label or anywhere else.
--exporter.disable-tailscale
opnsense_tailscale_key_expiry_timestamp_seconds
Gauge
---
Unix timestamp at which THIS node's own Tailscale key expires. When it does the tunnel dies with no other warning, so this is the direct analogue of a certificate expiry gauge. Not emitted at all when the node key does not expire - upstream omits the field entirely then, and a 0 would read as \"expired in 1970\" and page forever on a healthy node. Self only: peer key expiry is fleet inventory covered by tailscale2otel.
Whether the Tor control port answered the last circuit and stream query (1 = up, 0 = unreachable/misconfigured)
--exporter.enable-tor
opnsense_tor_circuits
Gauge
status
Number of Tor circuits by build status (unrecognised status values are bucketed as \"other\")
--exporter.enable-tor
opnsense_tor_circuits_by_purpose
Gauge
purpose
Number of Tor circuits by purpose (unrecognised purpose values are bucketed as \"other\")
--exporter.enable-tor
opnsense_tor_streams
Gauge
status
Number of Tor streams by status (unrecognised status values are bucketed as \"other\"); never labeled by destination
--exporter.enable-tor
opnsense_tor_hidden_services
Gauge
---
Number of configured Tor hidden services
--exporter.enable-tor
opnsense_tor_hidden_service_hostname_available
Gauge
service
Whether a .onion hostname has been provisioned for a configured hidden service (1 = available, 0 = not available); the hostname itself is never exposed
Configured bandwidth limit for this pipe, normalized to bits per second. Not emitted for a pipe with no bandwidth cap configured (dnctl reports \"unlimited\", not 0 bps).
Configured added delay for this pipe in milliseconds. 0 is a real \"no added delay\" configuration, not an absence.
--exporter.disable-trafficshaper
opnsense_trafficshaper_pipe_configured_queue_size
Gauge
pipe, description, unit
Folded from this pipe's own auto-attached (template) queue.
--exporter.disable-trafficshaper
opnsense_trafficshaper_pipe_configured_weight
Gauge
pipe, description
Configured WF2Q+ scheduling weight of this pipe's own auto-attached (template) queue. A relative value with meaning only alongside sibling weights on the same scheduler.
Total number of request list entries overwritten by newer entries
--exporter.disable-unbound
opnsense_unbound_dns_request_list_exceeded_total
Counter
---
Total number of request list entries that exceeded the maximum
--exporter.disable-unbound
opnsense_unbound_dns_tcp_usage_ratio
Gauge
---
TCP connection usage ratio for the DNS resolver (0.0 to 1.0)
--exporter.disable-unbound
opnsense_unbound_dns_blocklist_enabled
Gauge
---
Whether the DNS blocklist is enabled (1 = enabled, 0 = disabled)
--exporter.disable-unbound
opnsense_unbound_dns_service_running
Gauge
---
Whether the service is running (1 = running, 0 = stopped/disabled)
--exporter.disable-unbound
opnsense_unbound_dns_recursion_time_seconds
Gauge
---
How long Unbound's recursive lookups took, bucketed - the p50/p99 companion to the recursion_time_avg_seconds and recursion_time_median_seconds gauges. Buckets are unbound's own: exponential, doubling from 1us to 2^19s. Cumulative since the resolver started, so query it through rate(). ONLY EXISTS WHEN THE RESOLVER RUNS WITH extended-statistics: yes, which is OFF by default from OPNsense 26.7 - unbound does not compute the histogram's output at all otherwise, and the exporter emits no series rather than a fabricated empty one. The _sum is reconstructed by multiplying unbound's reported mean back by this histogram's own count, which is exact to the microsecond that mean is printed at; unbound never publishes the accumulated total directly.
--exporter.disable-unbound
opnsense_unbound_dns_validation_operations_total
Counter
---
RRSIG verification operations the DNSSEC validator has attempted, counted whether they passed or failed (unbound's num.valops). This is the denominator answers_secure_total and answers_bogus_total lack: a validator doing heavy work for few secure answers is a different problem from one doing none at all, and the two are indistinguishable without it. Extended-statistics sourced, so absent when the box runs extended-statistics: no (the OPNsense 26.7 default).
--exporter.disable-unbound
opnsense_unbound_dns_infra_rtt_seconds
Gauge
ip, host
Smoothed round-trip time to an upstream server in Unbound's infra cache. Only emitted when --exporter.enable-unbound-infra is set.
--exporter.disable-unbound
opnsense_unbound_dns_infra_rto_seconds
Gauge
ip, host
Retransmission timeout for an upstream server in Unbound's infra cache. Only emitted when --exporter.enable-unbound-infra is set.
--exporter.disable-unbound
opnsense_unbound_dns_infra_host_lame
Gauge
ip, host, kind
Whether Unbound has marked an upstream server lame - not authoritative for the zone it was asked about - by lameness kind (1 = lame, 0 = fine). kind=recursion means the server answers by recursing on our behalf instead of serving the zone; kind=type_a and kind=other split lameness by query type, because a server can serve A records fine and be lame for everything else. THIS IS THE SIGNAL RTT CANNOT CARRY: Unbound stops querying a lame server, so its RTT stays healthy while resolution through it fails. Only emitted when --exporter.enable-unbound-infra is set.
--exporter.disable-unbound
opnsense_unbound_dns_infra_host_dnssec_lame
Gauge
ip, host
Whether Unbound has marked an upstream server DNSSEC-lame (1 = lame, 0 = fine): it answers the zone but will not serve the DNSSEC records needed to validate those answers. Tracked separately from infra_host_lame because Unbound tracks it separately and it fails differently - queries succeed, validation does not. Only emitted when --exporter.enable-unbound-infra is set.
--exporter.disable-unbound
opnsense_unbound_dns_infra_host_edns_broken
Gauge
ip, host
Whether Unbound has determined that EDNS queries or replies are being dropped in transit to an upstream server (1 = broken, 0 = fine), from its cached EDNS version for that host. Distinct from lameness: the server answers, but every EDNS-bearing exchange with it is eaten somewhere on the path, which silently disables DNSSEC and forces fallbacks. Usually a middlebox or MTU problem rather than the server itself. Only emitted when --exporter.enable-unbound-infra is set.
--exporter.disable-unbound
opnsense_unbound_dns_qstats_enabled
Gauge
---
Whether Unbound query-stats logging (general.stats) is on (1 = enabled, 0 = disabled). Only emitted when --exporter.enable-unbound-qstats is set.
--exporter.disable-unbound
opnsense_unbound_dns_dnsbl_blocklist_size
Gauge
---
Number of entries in the currently loaded DNSBL blocklist. Gauge: reflects whatever list is loaded right now. Only emitted when --exporter.enable-unbound-qstats is set and query-stats logging is on.
--exporter.disable-unbound
opnsense_unbound_dns_qstats_queries_7d
Gauge
result
DNSBL query-stats outcome totals over Unbound's rolling query-stats window (typically the last 7 days), by result. Gauge, not a counter: the underlying window is truncated hourly and can decrease. Only emitted when --exporter.enable-unbound-qstats is set and query-stats logging is on.
--exporter.disable-unbound
opnsense_unbound_dns_qstats_queries_total_7d
Gauge
---
Total DNS queries over Unbound's rolling query-stats window. Gauge, not a counter, for the same reason as qstats_queries_7d. Only emitted when --exporter.enable-unbound-qstats is set and query-stats logging is on.
--exporter.disable-unbound
opnsense_unbound_dns_qstats_start_time_seconds
Gauge
---
Unix timestamp the current query-stats rolling window starts from. A jump forward beyond the expected daily roll-off signals the underlying qstats database was reset. Only emitted when --exporter.enable-unbound-qstats is set and query-stats logging is on.
--exporter.disable-unbound
opnsense_unbound_dns_local_zones
Gauge
type
Number of configured Unbound local zones, by zone type. Only emitted when --exporter.enable-unbound-qstats is set.
--exporter.disable-unbound
opnsense_unbound_dns_local_data_records
Gauge
---
Total number of configured Unbound local-data resource records. Only emitted when --exporter.enable-unbound-qstats is set.
--exporter.disable-unbound
opnsense_unbound_dns_insecure_domains
Gauge
---
Number of domains configured as DNSSEC-insecure in Unbound. Only emitted when --exporter.enable-unbound-qstats is set.
--exporter.disable-unbound
opnsense_unbound_dns_qstats_top_domain_queries_7d
Gauge
domain, result
Queries for one domain over Unbound's rolling query-stats window (typically 7 days), for the busiest domains only, split by outcome: result=passed is the top-N of queries Unbound answered, result=blocked the top-N a DNSBL policy blocked - the pi-hole-style leaderboard. A domain can appear under both, since some queries for it may predate the policy that now blocks it. Gauge, not a counter: the window is truncated hourly and a qstats reset empties it, so these totals decrease. BOUNDED ON BOTH AXES and truncated by design - the API is asked for at most 512 rows per outcome, at most 512 domains are tracked per outcome, and a domain is retired 5 minutes after it last appeared. This is the busiest domains, never all of them, and summing it does not reconstruct qstats_queries_7d. Domains carry unbound's trailing root dot. Refusals past the cap surface as cardinality_capped_total; a rising refusal count means something is minting domains faster than the cap allows (random-subdomain or DNS-tunnelling traffic does exactly this) and the leaderboard below it is no longer the real top-N. Only emitted when --exporter.enable-unbound-qstats is set and query-stats logging is on.
--exporter.disable-unbound
opnsense_unbound_dns_cardinality_capped_total
Counter
family
Domains refused their own series because the leaderboard already held its full key budget when they were first seen. Non-zero and rising means the top-N is saturated and no longer reflects the real busiest domains - the usual cause is random-subdomain or DNS-tunnelling traffic churning the leaderboard, which is worth looking at in its own right. There is deliberately no companion gauge for the live key count: count() over qstats_top_domain_queries_7d derives it exactly.
Unix timestamp when the currently active boot environment (active flag containing \"N\") was created. Absent when no boot environment is marked active (e.g. unsupported filesystem).