Skip to content

Docker Compose Reference

Generated by make docs. Do not hand-edit — edits here are overwritten on the next run.

Every flag the exporter knows about, grouped the same way as Configuration, rendered as a single Compose file with each setting commented out at its default. For a short working example, see Docker & Compose; this page exists so a new setting cannot ship without a place in an example config.

Flags with no environment variable (--config.check is the only one today) cannot live in environment: at all — they are documented as comments and belong in command: instead.

docker-compose.yml
services:
  opnsense-exporter:
    image: ghcr.io/rknightion/opnsense-exporter:latest
    container_name: opnsense-exporter
    restart: always

    # Flag-only settings: no environment variable exists, so these can only be
    # passed as command-line arguments. Uncomment into command: to use one, not
    # into environment: below.
    # command:
    #   Validate the effective configuration and exit, without binding any port,
    #   starting the poll scheduler, contacting OPNsense, or exporting telemetry. Exits
    #   0 when the configuration is usable and 1 otherwise. Referenced files (API
    #   key/secret, TLS keypairs) are read; network reachability is deliberately not
    #   checked (that is what /-/ready is for). Has no env var by design: an ambient one
    #   would turn every start into a no-op.
    #   - --config.check
    #   Addresses on which to expose metrics and web interface. Repeatable for multiple
    #   addresses. Examples: `:9100` or `[::1]:9100` for http, `vsock://:9100` for vsock
    #   - --web.listen-address
    #   Path to configuration file that can enable TLS or authentication. See:
    #   https://github.com/prometheus/exporter-toolkit/blob/master/docs/web-configuration.md
    #   - --web.config.file
    #   Only log messages with the given severity or above. One of: [debug, info, warn,
    #   error]
    #   - --log.level
    #   Output format of log messages. One of: [logfmt, json]
    #   - --log.format
    #   Use systemd socket activation listeners instead of port listeners (Linux only).
    #   - --web.systemd-socket

    environment:
      # ==========================================================================
      # OPNsense connection
      # How to reach the firewall API. Address and protocol are required; everything
      # else tunes the client.
      # ==========================================================================
      # Hostname or IP address of OPNsense API
      OPNSENSE_EXPORTER_OPS_API: "opnsense.example.com"
      # API key to use to connect to OPNsense API. This flag/ENV or the OPS_API_KEY_FILE
      # may be set.
      # OPNSENSE_EXPORTER_OPS_API_KEY: ""
      # API secret to use to connect to OPNsense API. This flag/ENV or the
      # OPS_API_SECRET_FILE may be set.
      # OPNSENSE_EXPORTER_OPS_API_SECRET: ""
      # Disable TLS certificate verification
      # OPNSENSE_EXPORTER_OPS_INSECURE: "false"
      # Maximum number of background OPNsense API requests in flight across all
      # scheduled collector polls, including nested sub-requests. Bounds the
      # simultaneous PHP/configd load on the firewall: lower it (e.g. 4-8) to protect a
      # low-power appliance at the cost of queued or longer polls; raise it to let more
      # independent polls progress concurrently on capable hardware. It does not affect
      # /metrics replay. Must be >= 1.
      # OPNSENSE_EXPORTER_OPS_MAX_CONCURRENT_REQUESTS: "16"
      # Number of attempts for a failed OPNsense API request (transport errors /
      # retryable 5xx). Worst-case block time is --opnsense.timeout x this value.
      # OPNSENSE_EXPORTER_OPS_MAX_RETRIES: "3"
      # Protocol to use to connect to OPNsense API. One of: [http, https]
      OPNSENSE_EXPORTER_OPS_PROTOCOL: "https"
      # Per-request HTTP timeout for calls to the OPNsense API. Combined with
      # --opnsense.max-retries this bounds one endpoint attempt sequence inside a
      # background collector poll (timeout x retries). Keep that product below
      # --exporter.max-scrape-duration so the poll deadline, rather than a request
      # retry, remains the outer bound. Prometheus scrape_timeout applies only to
      # replaying /metrics.
      # OPNSENSE_EXPORTER_OPS_TIMEOUT: "15s"

      # ==========================================================================
      # Opt-in collectors
      # Off by default, each for a stated reason: extra per-poll API cost, high
      # cardinality, or a live call to a third party.
      # ==========================================================================
      # Enable per-table pf evaluation/packet/byte counters for firewall aliases (~10
      # series per alias table)
      # OPNSENSE_EXPORTER_ENABLE_ALIAS_DETAILS: "false"
      # Enable every opt-in collector switch (--exporter.enable-*) that is not
      # explicitly set on the command line or via its own env var. A collector whose
      # PLUGIN the startup availability probe finds absent is left off, so this enables
      # what the box can actually serve; anything gated on cost or cardinality rather
      # than a plugin is enabled regardless. If the firewall cannot be reached at
      # startup the probe falls open and everything is enabled. NOTE: because
      # availability is resolved once at startup, a plugin installed LATER does not
      # self-activate under this flag until the next restart. Never enables the
      # syslog/Zenarmor/NetFlow receivers - those open network sockets and are out of
      # scope. Each collector this switches on is logged individually with the reason it
      # defaults to off; an explicit --exporter.enable-<x>=false always wins over this
      # blanket switch.
      # OPNSENSE_EXPORTER_ENABLE_ALL_AVAILABLE: "false"
      # Enable per-entry ARP metrics (ip/mac/hostname labels - high, churning
      # cardinality). Off by default; the low-cardinality entries_total aggregate is
      # always emitted.
      # OPNSENSE_EXPORTER_ENABLE_ARP_DETAILS: "false"
      # Enable per-lease detail metrics for ISC DHCPv4 (high cardinality on large
      # networks)
      # OPNSENSE_EXPORTER_ENABLE_DHCPV4_DETAILS: "false"
      # Enable per-lease detail metrics for ISC DHCPv6 (high cardinality on large
      # networks)
      # OPNSENSE_EXPORTER_ENABLE_DHCPV6_DETAILS: "false"
      # Enable per-lease detail metrics for Dnsmasq DHCP (high cardinality on large
      # networks)
      # OPNSENSE_EXPORTER_ENABLE_DNSMASQ_DETAILS: "false"
      # Enable the NAT rule inventory count metric (opnsense_firewall_nat_rules), broken
      # down by type (source_nat, d_nat, one_to_one, npt) and enabled state. Off by
      # default: each scheduled poll does four extra GETs, one per NAT rule type. Rules
      # created before an admin migrated to the MVC-managed NAT backend are not counted;
      # NAT rule pf hit/byte statistics do not exist upstream.
      # OPNSENSE_EXPORTER_ENABLE_FIREWALL_NAT_COUNTS: "false"
      # Enable per-rule detail metrics for firewall rules (high cardinality on large
      # rulesets)
      # OPNSENSE_EXPORTER_ENABLE_FIREWALL_RULES_DETAILS: "false"
      # Enable per-package firmware detail metrics (pending package updates and
      # installed plugin inventory; adds one extra API call per scheduled poll)
      # OPNSENSE_EXPORTER_ENABLE_FIRMWARE_PACKAGE_DETAILS: "false"
      # Enable FRR routing-state volume gauges (zebra RIB / OSPF route table / LSDB
      # counts by protocol, route type, area and LSA type - never per-prefix or per-LSA
      # series). Off by default: the underlying bootgrid endpoints have no success-body
      # caching and their payload size scales with route-table size (up to 6 extra vtysh
      # execs per scheduled poll).
      # OPNSENSE_EXPORTER_ENABLE_FRR_ROUTES: "false"
      # Enable the HA sync status collector (performs a live XML-RPC call to the CARP
      # peer on every scheduled poll). Disabled by default.
      # OPNSENSE_EXPORTER_ENABLE_HASYNC: "false"
      # Enable the Suricata recent-alerts gauge (opnsense_ids_recent_alerts by action).
      # Off by default: each scheduled poll triggers a reverse read of eve.json on the
      # box. Window set by --exporter.ids-alert-lookback.
      # OPNSENSE_EXPORTER_ENABLE_IDS_ALERTS: "false"
      # Enable per-lease IPsec mode-cfg detail metrics (opnsense_ipsec_lease_online with
      # an unbounded road-warrior user label). Off by default; the per-pool lease
      # aggregates stay always-on.
      # OPNSENSE_EXPORTER_ENABLE_IPSEC_LEASE_DETAILS: "false"
      # Enable per-lease detail metrics for Kea DHCP (high cardinality on large
      # networks)
      # OPNSENSE_EXPORTER_ENABLE_KEA_DETAILS: "false"
      # Enable per-entry NDP metrics (ip/mac labels - high, churning cardinality from
      # IPv6 privacy-address rotation). Off by default; the low-cardinality
      # entries_total aggregate is always emitted.
      # OPNSENSE_EXPORTER_ENABLE_NDP_DETAILS: "false"
      # Enable per-peer detail metrics for NetBird (per-peer cardinality; peer FQDN
      # labels)
      # OPNSENSE_EXPORTER_ENABLE_NETBIRD_DETAILS: "false"
      # Enable the netflow collector (enabled status, service status, cache stats).
      # Disabled by default.
      # OPNSENSE_EXPORTER_ENABLE_NETFLOW: "false"
      # Enable the network diagnostics collector (netisr, sockets, routes). Disabled by
      # default.
      # OPNSENSE_EXPORTER_ENABLE_NETWORK_DIAGNOSTICS: "false"
      # Enable per-session detail metrics for OpenVPN (exposes usernames and per-client
      # tunnel addresses)
      # OPNSENSE_EXPORTER_ENABLE_OPENVPN_DETAILS: "false"
      # Enable the SMART disk health collector. Off by default: each scheduled poll does
      # a per-disk POST fanout that runs `smartctl -a` on the firewall (extra
      # API/latency cost, and wakes spun-down disks). Silent when the os-smart plugin is
      # absent.
      # OPNSENSE_EXPORTER_ENABLE_SMART: "false"
      # Enable per-peer detail metrics for Tailscale (per-peer cardinality; peer
      # hostname labels)
      # OPNSENSE_EXPORTER_ENABLE_TAILSCALE_PEER_DETAILS: "false"
      # Enable the Tor circuit/stream telemetry collector (control-port GETINFO via the
      # os-tor plugin). Off by default: each scheduled poll does two extra configd execs
      # to query the control port, and requires the plugin's control port + password to
      # be configured. Silent when the os-tor plugin is absent.
      # OPNSENSE_EXPORTER_ENABLE_TOR: "false"
      # Enable per-upstream infra cache RTT metrics from Unbound (cardinality scales
      # with the resolver's infra cache; one series pair per upstream ip/host)
      # OPNSENSE_EXPORTER_ENABLE_UNBOUND_INFRA: "false"
      # Enable Unbound DNSBL query-stats totals and blocklist size metrics, plus
      # local-zone/data/insecure-domain counts. Off by default: the query-stats totals
      # call is backed by an expensive configd+python+pandas+DuckDB query (~1s per
      # scheduled poll) - skipped entirely while query-stats logging (general.stats) is
      # off on the box, but still paid for on every scheduled poll once it is on.
      # OPNSENSE_EXPORTER_ENABLE_UNBOUND_QSTATS: "false"
      # Enable the vnstat persistent traffic accounting collector (day/month/total bytes
      # per interface, survives reboots). Off by default: each scheduled poll does one
      # interface_list call plus one get_json_data call per interface vnstat tracks.
      # Silent when the os-vnstat plugin is absent.
      # OPNSENSE_EXPORTER_ENABLE_VNSTAT: "false"

      # ==========================================================================
      # Default-on collectors
      # On unless disabled. Each is silent when the subsystem or plugin it reads is
      # absent, so leaving one on costs nothing on a box without it.
      # ==========================================================================
      # Disable the scraping of ACME client certificate renewal status and expiry
      # metrics (silent when the os-acme-client plugin is absent)
      # OPNSENSE_EXPORTER_DISABLE_ACME: "false"
      # Disable the scraping of system activity metrics (CPU percentages, thread counts)
      # OPNSENSE_EXPORTER_DISABLE_ACTIVITY: "false"
      # Disable the scraping of firewall alias table sizes
      # OPNSENSE_EXPORTER_DISABLE_ALIAS: "false"
      # Disable the scraping of APC UPS (apcupsd) metrics (silent when the os-apcupsd
      # plugin is absent)
      # OPNSENSE_EXPORTER_DISABLE_APCUPSD: "false"
      # Disable the scraping of the ARP table
      # OPNSENSE_EXPORTER_DISABLE_ARP_TABLE: "false"
      # Disable the scraping of local-auth security-posture metrics (user/group/API-key
      # counts, aggregates only - no per-user data)
      # OPNSENSE_EXPORTER_DISABLE_AUTH: "false"
      # Disable the scraping of config backup freshness metrics (last backup
      # timestamp/count/size)
      # OPNSENSE_EXPORTER_DISABLE_BACKUP: "false"
      # Disable the scraping of BPF listener statistics
      # OPNSENSE_EXPORTER_DISABLE_BPF: "false"
      # Disable the scraping of captive portal zone/session metrics (silent when no
      # zones are configured)
      # OPNSENSE_EXPORTER_DISABLE_CAPTIVEPORTAL: "false"
      # Disable the scraping of CARP/VIP status metrics
      # OPNSENSE_EXPORTER_DISABLE_CARP: "false"
      # Disable the scraping of certificate expiry metrics
      # OPNSENSE_EXPORTER_DISABLE_CERTIFICATES: "false"
      # Disable the scraping of chrony NTP tracking/source metrics (silent when the
      # os-chrony plugin is absent)
      # OPNSENSE_EXPORTER_DISABLE_CHRONY: "false"
      # Disable the scraping of ClamAV engine version and signature database freshness
      # metrics (silent when the os-clamav plugin is absent)
      # OPNSENSE_EXPORTER_DISABLE_CLAMAV: "false"
      # Disable the scraping of the cron table
      # OPNSENSE_EXPORTER_DISABLE_CRON_TABLE: "false"
      # Disable the scraping of CrowdSec alert/decision/bouncer/machine counts (silent
      # when the os-crowdsec plugin is absent)
      # OPNSENSE_EXPORTER_DISABLE_CROWDSEC: "false"
      # Disable the scraping of ISC DHCPv4 leases (silent when the legacy ISC DHCP
      # backend is absent)
      # OPNSENSE_EXPORTER_DISABLE_DHCPV4: "false"
      # Disable the scraping of ISC DHCPv6 leases and delegated prefixes (silent when
      # the legacy ISC DHCP backend is absent)
      # OPNSENSE_EXPORTER_DISABLE_DHCPV6: "false"
      # Disable the scraping of Dnsmasq DHCP leases
      # OPNSENSE_EXPORTER_DISABLE_DNSMASQ: "false"
      # Disable the scraping of DynDNS (ddclient) account update status metrics (silent
      # when the os-ddclient plugin is absent)
      # OPNSENSE_EXPORTER_DISABLE_DYNDNS: "false"
      # Disable the feature-availability collector (opnsense_feature_available; #517).
      # It periodically probes the plugin-gated endpoints backing the opt-in
      # SMART/Tor/Vnstat collectors and logs a one-shot line naming the flag to enable
      # any that answer successfully but are not yet enabled.
      # OPNSENSE_EXPORTER_DISABLE_FEATURE_AVAILABILITY: "false"
      # Disable the scraping of the firewall (pf) metrics
      # OPNSENSE_EXPORTER_DISABLE_FIREWALL: "false"
      # Disable the scraping of firewall rule statistics
      # OPNSENSE_EXPORTER_DISABLE_FIREWALL_RULES: "false"
      # Disable the scraping of the firmware metrics
      # OPNSENSE_EXPORTER_DISABLE_FIRMWARE: "false"
      # Disable the flow collector (Prometheus byte/packet volume counters rolled up
      # from flow records, on bounded dimensions). Silent until a flow source - today
      # the Zenarmor receiver - is enabled and feeding it.
      # OPNSENSE_EXPORTER_DISABLE_FLOW: "false"
      # Disable the scraping of FRR routing metrics (BGP/OSPF/BFD; silent when the
      # os-frr plugin is absent)
      # OPNSENSE_EXPORTER_DISABLE_FRR: "false"
      # Disable the scraping of gateway status metrics (RTT, packet loss, gateway state)
      # OPNSENSE_EXPORTER_DISABLE_GATEWAYS: "false"
      # Disable the scraping of HAProxy statistics (silent when the os-haproxy plugin is
      # absent)
      # OPNSENSE_EXPORTER_DISABLE_HAPROXY: "false"
      # Disable the scraping of hardware identity/PSU metrics (DMI system info via
      # os-dmidecode; Deciso DEC-series PSU status via os-dec-hw). Silent when neither
      # plugin is installed.
      # OPNSENSE_EXPORTER_DISABLE_HARDWARE: "false"
      # Disable the scraping of the discovered-host inventory (Interfaces > Host
      # discovery / hostwatch): interface+source host counts, low-cardinality. A core
      # OPNsense feature (not a plugin); reads absent/silent on releases predating it.
      # OPNSENSE_EXPORTER_DISABLE_HOSTDISCOVERY: "false"
      # Disable the scraping of Suricata IDS/IPS metrics (service status, IPS mode, eve
      # log and ruleset inventory, installed-rule count; silent structures when IDS is
      # unconfigured)
      # OPNSENSE_EXPORTER_DISABLE_IDS: "false"
      # Disable the interfaces collector (per-interface traffic/link metrics)
      # OPNSENSE_EXPORTER_DISABLE_INTERFACES: "false"
      # Disable the scraping of IPSec service
      # OPNSENSE_EXPORTER_DISABLE_IPSEC: "false"
      # Disable the scraping of Kea DHCP lease metrics
      # OPNSENSE_EXPORTER_DISABLE_KEA: "false"
      # Disable the scraping of LLDP neighbor table metrics (silent when the os-lldpd
      # plugin is absent)
      # OPNSENSE_EXPORTER_DISABLE_LLDPD: "false"
      # Disable the log_events collector (Prometheus counters derived from received
      # syslog lines: firewall/haproxy/sshd/dhcp/audit/ids event totals). Silent until
      # the syslog receiver is enabled and feeding it.
      # OPNSENSE_EXPORTER_DISABLE_LOG_EVENTS: "false"
      # Disable the scraping of mbuf statistics
      # OPNSENSE_EXPORTER_DISABLE_MBUF: "false"
      # Disable the scraping of Monit service check status (silent when Monit is not
      # running)
      # OPNSENSE_EXPORTER_DISABLE_MONIT: "false"
      # Disable the scraping of the NDP (IPv6 neighbor discovery) table
      # OPNSENSE_EXPORTER_DISABLE_NDP: "false"
      # Disable the scraping of NetBird management/signal connectivity, relay and peer
      # metrics (silent when the os-netbird plugin is absent)
      # OPNSENSE_EXPORTER_DISABLE_NETBIRD: "false"
      # Disable the scraping of nginx VTS statistics (silent when the os-nginx plugin is
      # absent)
      # OPNSENSE_EXPORTER_DISABLE_NGINX: "false"
      # Disable the scraping of NTP peer metrics
      # OPNSENSE_EXPORTER_DISABLE_NTP: "false"
      # Disable the scraping of NUT UPS metrics (silent when the os-nut plugin is
      # absent)
      # OPNSENSE_EXPORTER_DISABLE_NUT: "false"
      # Disable the scraping of OpenVPN service
      # OPNSENSE_EXPORTER_DISABLE_OPENVPN: "false"
      # Disable the scraping of PF statistics (state table, counters, memory limits,
      # timeouts)
      # OPNSENSE_EXPORTER_DISABLE_PF_STATS: "false"
      # Disable the protocol-statistics collector (TCP/UDP/IP/ICMP/ARP/CARP/pfsync
      # counters)
      # OPNSENSE_EXPORTER_DISABLE_PROTOCOL: "false"
      # Disable the scraping of Q-Feeds threat intelligence statistics (silent when the
      # os-q-feeds-connector plugin is absent)
      # OPNSENSE_EXPORTER_DISABLE_QFEEDS: "false"
      # Disable the scraping of relayd virtual server/table/host health (silent when the
      # os-relayd plugin is absent)
      # OPNSENSE_EXPORTER_DISABLE_RELAYD: "false"
      # Disable the services collector (per-service running state)
      # OPNSENSE_EXPORTER_DISABLE_SERVICES: "false"
      # Disable the scraping of the siproxd active SIP registration count (silent when
      # the os-siproxd plugin is absent)
      # OPNSENSE_EXPORTER_DISABLE_SIPROXD: "false"
      # Disable the scraping of ZFS boot-environment inventory metrics (silent/zero on
      # non-ZFS filesystems such as UFS)
      # OPNSENSE_EXPORTER_DISABLE_SNAPSHOTS: "false"
      # Disable the scraping of syslog-ng statistics
      # OPNSENSE_EXPORTER_DISABLE_SYSLOG: "false"
      # Disable the scraping of system resource metrics (memory, uptime, disk, swap)
      # OPNSENSE_EXPORTER_DISABLE_SYSTEM: "false"
      # Disable the scraping of Tailscale node-local metrics (silent when the
      # os-tailscale plugin is absent; complementary to tailscale2otel)
      # OPNSENSE_EXPORTER_DISABLE_TAILSCALE: "false"
      # Disable the scraping of temperature metrics
      # OPNSENSE_EXPORTER_DISABLE_TEMPERATURE: "false"
      # Disable the scraping of traffic shaper pipe/queue/rule statistics (silent when
      # the shaper is unconfigured)
      # OPNSENSE_EXPORTER_DISABLE_TRAFFICSHAPER: "false"
      # Disable the scraping of Unbound service
      # OPNSENSE_EXPORTER_DISABLE_UNBOUND: "false"
      # Disable the scraping of Wireguard service
      # OPNSENSE_EXPORTER_DISABLE_WIREGUARD: "false"

      # ==========================================================================
      # Operator console
      # The web UI served at /. Reads a passive snapshot of the last scrape; it never
      # gathers on its own.
      # ==========================================================================
      # Hide the /config page.
      # OPNSENSE_EXPORTER_WEB_UI_DISABLE_CONFIG: "false"
      # Hide the /devices page (exposes MAC/hostname).
      # OPNSENSE_EXPORTER_WEB_UI_DISABLE_DEVICES: "false"
      # Serve the operator console at / (else the minimal landing page).
      # OPNSENSE_EXPORTER_WEB_UI_ENABLED: "true"
      # Live-poll interval for the console's dynamic pages.
      # OPNSENSE_EXPORTER_WEB_UI_REFRESH_INTERVAL: "5s"

      # ==========================================================================
      # HTTP server and logging
      # The metrics listener and process logging.
      # ==========================================================================
      # --log.format has no env var - see the flag-only settings note above command:.
      # --log.level has no env var - see the flag-only settings note above command:.
      # --web.config.file has no env var - see the flag-only settings note above command:.
      # Exclude metrics about the exporter itself (process_*, go_*).
      # OPNSENSE_EXPORTER_DISABLE_EXPORTER_METRICS: ""
      # --web.listen-address has no env var - see the flag-only settings note above command:.
      # --web.systemd-socket has no env var - see the flag-only settings note above command:.
      # Path under which to expose metrics.
      # OPNSENSE_EXPORTER_WEB_TELEMETRY_PATH: "/metrics"

      # ==========================================================================
      # Exporter behaviour
      # Poll scheduling, caching, instance identity and the cardinality budget.
      # ==========================================================================
      # Interval at which the exporter polls the OPNsense health endpoint (#386). This
      # is the circuit-breaker cadence: the health poll sets and clears the process-wide
      # 'firewall unreachable' flag, so it bounds how quickly collectors resume after
      # the box recovers. Independent of --collector.poll-interval since #386, which
      # previously controlled it by accident. Clamped to [5s, 15m].
      # OPNSENSE_EXPORTER_COLLECTOR_HEALTH_POLL_INTERVAL: "60s"
      # Default interval at which each collector polls the OPNsense API into the
      # in-memory snapshot that /metrics and the OTLP bridge replay (#336). A collector
      # may declare its own faster/slower tier; every interval is clamped to [5s, 15m].
      # OPNSENSE_EXPORTER_COLLECTOR_POLL_INTERVAL: "60s"
      # Override a specific collector's poll interval as <collector>=<duration>
      # (repeatable; clamped to [5s, 15m]). Wins over the collector's built-in tier.
      # Example: --collector.poll-interval-override=gateways=10s
      # --collector.poll-interval-override=smart=1h.
      # OPNSENSE_EXPORTER_COLLECTOR_POLL_INTERVAL_OVERRIDE: ""
      # --config.check has no env var - see the flag-only settings note above command:.
      # How long to cache responses from slow-moving API endpoints (system/CPU identity,
      # certificate inventory, Unbound DNS blocklist policy config) and to remember that
      # a plugin-gated endpoint is absent (its 404). This data changes only on an admin
      # action - a config edit, a certificate renewal, a plugin install - so re-fetching
      # it on every poll only costs firewall CPU. Set it above the collector poll
      # interval or it can never serve a hit. The cost is staleness: a newly installed
      # plugin, or a cert change, can take up to this long to show up. Set to 0 to fetch
      # everything on every poll. Live data (counters, rates, service run-state) is
      # never cached regardless of this setting.
      # OPNSENSE_EXPORTER_CACHE_TTL: "1h0m0s"
      # How long to cache firmware API responses (status and, when enabled, package
      # details). The firmware data OPNsense serves is the stored result of the box's
      # own update check, which it refreshes roughly daily, so re-fetching it on every
      # poll only costs firewall CPU. Set to 0 to fetch on every poll.
      # OPNSENSE_EXPORTER_FIRMWARE_CACHE_TTL: "12h0m0s"
      # Lookback window over which opnsense_ids_recent_alerts counts Suricata eve alerts
      # (a gauge). Only used when --exporter.enable-ids-alerts is set. Counts are a
      # floor when more than 500 alerts fall inside the window.
      # OPNSENSE_EXPORTER_IDS_ALERT_LOOKBACK: "15m"
      # Label to use to identify the instance in every metric. If you have multiple
      # instances of the exporter, you can differentiate them by using different value
      # in this flag, that represents the instance of the target OPNsense. If left
      # empty, it defaults to the configured OPNsense address (deterministic). Set
      # --exporter.instance-use-hostname to derive it from the OPNsense hostname
      # instead.
      # OPNSENSE_EXPORTER_INSTANCE_LABEL: ""
      # When --exporter.instance-label is empty, derive the instance label from the
      # OPNsense hostname reported by the API instead of the configured address. This
      # lookup is deterministic: it blocks at startup and, if the hostname cannot be
      # obtained, the exporter refuses to start (rather than silently falling back to
      # the address, which would make the label depend on startup timing and flip
      # between restarts).
      # OPNSENSE_EXPORTER_INSTANCE_USE_HOSTNAME: "false"
      # Upper bound on a single collector poll (#336). Since serving /metrics now
      # replays an in-memory snapshot rather than calling the API, this bounds each
      # background poll so a stalled/blackholed endpoint frees its poll-concurrency slot
      # instead of holding it open. Serving itself is never blocked by it.
      # OPNSENSE_EXPORTER_MAX_SCRAPE_DURATION: "50s"
      # Soft budget for the total number of Prometheus series produced by the COLLECTOR
      # registry (the same set /metrics and the OTLP bridge serve, and what metricsnap
      # replays to the web UI's /cardinality report) — this is NOT the exporter
      # process's full series count: process_*/go_* self-metrics and the
      # opnsense_exporter_otlp_* delivery-health family live on a separate self registry
      # and are never counted here, so this number will read lower than what your
      # Prometheus tenant ultimately stores for this job. Nothing is ever dropped,
      # capped or refused when it is exceeded (#494) — exceeding it only logs a
      # rate-limited warning (once on the transition into the over-budget state, then at
      # most hourly while it persists, and once more on the transition back under
      # budget) and is reported on /cardinality alongside the existing per-metric
      # warn/crit thresholds, which are a different, unrelated dimension. Set to 0 to
      # disable the check entirely.
      # OPNSENSE_EXPORTER_SERIES_BUDGET: "100000"

      # ==========================================================================
      # OTLP metrics export
      # Push metrics to an OTLP endpoint instead of, or alongside, being scraped.
      # ==========================================================================
      # Enable pushing metrics to an OTLP endpoint (in addition to the /metrics pull
      # endpoint). Off by default.
      # OPNSENSE_EXPORTER_OTLP_ENABLED: "false"
      # OTLP endpoint URL. When empty, the standard OTEL_EXPORTER_OTLP_ENDPOINT env var
      # is used.
      # OPNSENSE_EXPORTER_OTLP_ENDPOINT: ""
      # Interval between OTLP metric exports (independent of Prometheus scrapes).
      # OPNSENSE_EXPORTER_OTLP_EXPORT_INTERVAL: "60s"
      # Optional second OTLP export lane for fast-tier collectors only (#390). Zero (the
      # default) keeps the single-stream behaviour exactly. When set, fast-tier
      # collectors (gateways, interfaces, protocol, pf_stats, activity, netflow, carp
      # — or whatever --collector.poll-interval-override makes fast) export at this
      # interval while everything else stays on --otlp.export-interval. Must be shorter
      # than --otlp.export-interval. Fast-tier series are a small fraction of the total,
      # so 15s here costs far less than setting --otlp.export-interval=15s for
      # everything.
      # OPNSENSE_EXPORTER_OTLP_FAST_EXPORT_INTERVAL: "0s"
      # Grafana Cloud OTLP gateway base URL (required when using the Grafana Cloud
      # shortcut).
      # OPNSENSE_EXPORTER_OTLP_GRAFANA_CLOUD_ENDPOINT: ""
      # Grafana Cloud OTLP instance ID. With --otlp.grafana-cloud-token, synthesizes
      # basic-auth. This flag/ENV or
      # OPNSENSE_EXPORTER_OTLP_GRAFANA_CLOUD_INSTANCE_ID_FILE may be set.
      # OPNSENSE_EXPORTER_OTLP_GRAFANA_CLOUD_INSTANCE_ID: ""
      # Grafana Cloud Access Policy token. This flag/ENV or
      # OPNSENSE_EXPORTER_OTLP_GRAFANA_CLOUD_TOKEN_FILE may be set.
      # OPNSENSE_EXPORTER_OTLP_GRAFANA_CLOUD_TOKEN: ""
      # OTLP headers as comma-separated key=value pairs (e.g.
      # X-Scope-OrgID=1,Authorization=Bearer x). When set, replaces
      # OTEL_EXPORTER_OTLP_HEADERS entirely; when empty, that env var is used.
      # OPNSENSE_EXPORTER_OTLP_HEADERS: ""
      # Disable TLS for the OTLP connection (plaintext).
      # OPNSENSE_EXPORTER_OTLP_INSECURE: "false"
      # OTLP transport protocol: grpc or http/protobuf. Defaults to http/protobuf; an
      # empty value is rejected.
      # OPNSENSE_EXPORTER_OTLP_PROTOCOL: "http/protobuf"
      # service.name resource attribute for exported metrics.
      # OPNSENSE_EXPORTER_OTLP_SERVICE_NAME: "opnsense-exporter"
      # Path to a CA certificate file used to verify the OTLP server.
      # OPNSENSE_EXPORTER_OTLP_TLS_CA_FILE: ""
      # Path to a client certificate file for OTLP mutual TLS (requires
      # --otlp.tls-key-file).
      # OPNSENSE_EXPORTER_OTLP_TLS_CERT_FILE: ""
      # Path to a client key file for OTLP mutual TLS (requires --otlp.tls-cert-file).
      # OPNSENSE_EXPORTER_OTLP_TLS_KEY_FILE: ""

      # ==========================================================================
      # Continuous profiling (Pyroscope)
      # Off until a server address is set. All profile types are pushed by default.
      # ==========================================================================
      # Pyroscope application name profiles are reported under.
      # OPNSENSE_EXPORTER_PYROSCOPE_APPLICATION_NAME: "opnsense-exporter"
      # HTTP basic auth password for Pyroscope (Grafana Cloud Access Policy token). This
      # flag/ENV or PYROSCOPE_AUTH_PASSWORD_FILE may be set.
      # OPNSENSE_EXPORTER_PYROSCOPE_AUTH_PASSWORD: ""
      # HTTP basic auth user for Pyroscope (Grafana Cloud stack/instance ID). This
      # flag/ENV or PYROSCOPE_AUTH_USER_FILE may be set.
      # OPNSENSE_EXPORTER_PYROSCOPE_AUTH_USER: ""
      # Disable mutex/block contention profiling. On by default; disabling drops the two
      # contention profiles and their process-global sampling rates. CPU, memory,
      # goroutine (and goroutine-leak, when built with the experiment) profiling are
      # unaffected.
      # OPNSENSE_EXPORTER_PYROSCOPE_DISABLE_MUTEX_BLOCK: "false"
      # Grafana Cloud Pyroscope endpoint URL. When empty, continuous profiling is
      # disabled.
      # OPNSENSE_EXPORTER_PYROSCOPE_SERVER_ADDRESS: ""
      # Pyroscope tenant ID (only needed for multi-tenancy; unused for Grafana Cloud).
      # OPNSENSE_EXPORTER_PYROSCOPE_TENANT_ID: ""

      # ==========================================================================
      # Grafana annotations
      # The exporter's only outbound write: firewall change events written into
      # Grafana's annotation store.
      # ==========================================================================
      # Write OPNsense change events (reboots, configuration changes, interface counter
      # resets, upgrades, certificate renewals, feed updates) into Grafana's annotation
      # store so they overlay any dashboard. Off by default: this is the exporter's only
      # outbound write.
      # OPNSENSE_EXPORTER_ANNOTATIONS_ENABLED: "false"
      # Extra tag to add to every written annotation (repeatable), e.g. env:prod. Every
      # annotation already carries opnsense-exporter, the event kind and
      # instance:<name>.
      # OPNSENSE_EXPORTER_ANNOTATIONS_EXTRA_TAGS: ""
      # Grafana base URL to write annotations to, e.g. https://mystack.grafana.net.
      # OPNSENSE_EXPORTER_ANNOTATIONS_GRAFANA_URL: ""
      # How often the watched event metrics are checked for changes. This bounds how
      # late an annotation is WRITTEN, never where it is PLACED — each annotation
      # carries the event's own timestamp.
      # OPNSENSE_EXPORTER_ANNOTATIONS_INTERVAL: "60s"
      # How old an event may be and still be worth annotating, and how far back the
      # startup reconciliation looks for annotations this exporter already wrote. Keeps
      # a fresh deployment from annotating a reboot that happened months ago. Read this
      # together with --annotations.max-per-cycle: a fresh deployment finds every event
      # inside this window at once, and that first-run backlog drains at most
      # max-per-cycle annotations per --annotations.interval (default 20/60s), so a 24h
      # lookback on a busy firewall takes several minutes to catch up. Shorten this if
      # you want a fresh deployment to start clean rather than backfill a day.
      # OPNSENSE_EXPORTER_ANNOTATIONS_LOOKBACK: "24h"
      # Maximum annotation posts ATTEMPTED per check, successful or not. A guard against
      # one bad reading writing hundreds of annotations, not a rate limit to tune. It
      # also paces the first-run backlog --annotations.lookback produces: the excess is
      # not marked seen, so it is re-proposed on the next check and a deployment with a
      # 24h lookback drains at this many per --annotations.interval until it is caught
      # up. Events are only lost if they age out of the lookback before the backlog
      # reaches them. Raising it drains faster but makes a rate limit
      # (opnsense_exporter_annotations_rate_limited_total) more likely, since a Grafana
      # org shares one annotation limit across every writer.
      # OPNSENSE_EXPORTER_ANNOTATIONS_MAX_PER_CYCLE: "20"
      # Timeout for each Grafana annotation API request.
      # OPNSENSE_EXPORTER_ANNOTATIONS_TIMEOUT: "10s"
      # Grafana service-account token used to write annotations. It needs the annotation
      # write permission and nothing else. This flag/ENV or
      # OPNSENSE_EXPORTER_ANNOTATIONS_TOKEN_FILE may be set.
      # OPNSENSE_EXPORTER_ANNOTATIONS_TOKEN: ""

      # ==========================================================================
      # Log shipping: syslog receiver
      # Push-based. The firewall sends syslog here; the exporter parses, enriches and
      # ships it. UNAUTHENTICATED unless TLS client certs are configured.
      # ==========================================================================
      # Comma-separated CIDR allowlist of hosts permitted to send syslog (e.g.
      # 10.0.0.254/32). Empty accepts any sender. Syslog is unauthenticated, so set this
      # on a shared network.
      # OPNSENSE_EXPORTER_LOGS_SYSLOG_ALLOWED_PEERS: ""
      # Dump syslog lines this receiver cannot parse (unknown program, no matching
      # parser, or an unparseable envelope) to --logs.debug-capture.dir for inspection.
      # Requires --logs.debug-capture.dir. Additive - these lines still ship as generic
      # records.
      # OPNSENSE_EXPORTER_LOGS_SYSLOG_DEBUG_CAPTURE: "false"
      # Enable the syslog receiver: listens for logs pushed by OPNsense (RFC5424 or
      # RFC3164, UDP and/or TCP) and ships them enriched with rule descriptions,
      # interface names and hostnames. Off by default. Requires --logs.enabled.
      # Configure a matching target on the firewall under System > Settings > Logging >
      # Targets.
      # OPNSENSE_EXPORTER_LOGS_SYSLOG_ENABLED: "false"
      # Enrich received syslog records from the OPNsense API: firewall rule descriptions
      # (including auto-generated system rules), friendly interface names, DHCP
      # hostnames, MAC addresses, local/remote scope and well-known service names.
      # OPNSENSE_EXPORTER_LOGS_SYSLOG_ENRICH: "true"
      # Comma-separated syslog programs to DROP (e.g. radvd,cron). Empty ships
      # everything. Dropped records are counted in
      # opnsense_exporter_logs_rejected_total{reason="filtered"} - never silently
      # discarded.
      # OPNSENSE_EXPORTER_LOGS_SYSLOG_EXCLUDE_PROGRAMS: ""
      # Comma-separated syslog programs to ship, dropping everything else. Empty ships
      # everything. Mutually exclusive with --logs.syslog.exclude-programs.
      # OPNSENSE_EXPORTER_LOGS_SYSLOG_INCLUDE_PROGRAMS: ""
      # TCP listen address for the syslog receiver. Empty disables the TCP listener.
      # Prefer TCP for firewall logs: UDP datagram loss is silent and unrecoverable.
      # OPNSENSE_EXPORTER_LOGS_SYSLOG_LISTEN_TCP: ":5514"
      # TLS listen address for the syslog receiver (RFC5424 over TLS, OPNsense
      # tls4/tls6). Empty disables the TLS listener. Requires
      # --logs.syslog.tls-cert-file and --logs.syslog.tls-key-file.
      # OPNSENSE_EXPORTER_LOGS_SYSLOG_LISTEN_TLS: ""
      # UDP listen address for the syslog receiver. Empty disables the UDP listener.
      # Port 5514 (not 514) because 514 is privileged and the container runs non-root.
      # OPNSENSE_EXPORTER_LOGS_SYSLOG_LISTEN_UDP: ":5514"
      # Maximum concurrent connections to the syslog receiver, applied PER TRANSPORT:
      # plain TCP and TLS each get this budget from a separate pool. They are separate
      # so a plaintext flood cannot starve authenticated mTLS senders out of the
      # capacity they need. Bounds goroutine growth on an unauthenticated ingress; with
      # both transports enabled the worst-case connection count is twice this value.
      # OPNSENSE_EXPORTER_LOGS_SYSLOG_MAX_CONNS: "64"
      # Drop records less severe than this (emerg, alert, crit, err, warning, notice,
      # info, debug). E.g. notice drops info and debug. Empty ships every severity.
      # OPNSENSE_EXPORTER_LOGS_SYSLOG_MIN_SEVERITY: ""
      # Sample (drop) high-volume raw log lines AFTER their metrics have been derived:
      # keep firewall block/reject lines and drop passes, keep HAProxy state changes and
      # errors and drop the per-connection noise. Low-volume programs (sshd, dhcp,
      # audit, ids) are kept in full. Off by default. Requires the log_events collector
      # (exporter.disable-log-events must not be set) so every dropped line is counted
      # first.
      # OPNSENSE_EXPORTER_LOGS_SYSLOG_SAMPLE: "false"
      # When sampling is on, stamp a sampled="true" attribute on every shipped line so
      # consumers know the log stream is incomplete and must use the derived counters
      # for totals. On by default; only takes effect when --logs.syslog.sample is set.
      # OPNSENSE_EXPORTER_LOGS_SYSLOG_SAMPLED_ATTRIBUTE: "true"
      # PEM server certificate for the TLS syslog listener.
      # OPNSENSE_EXPORTER_LOGS_SYSLOG_TLS_CERT_FILE: ""
      # PEM CA bundle to verify sender client certificates on the TLS syslog listener.
      # When set, a sender MUST present a certificate signed by this CA - the only real
      # sender authentication syslog offers. Empty accepts any TLS client (encryption
      # only).
      # OPNSENSE_EXPORTER_LOGS_SYSLOG_TLS_CLIENT_CA_FILE: ""
      # PEM private key for the TLS syslog listener.
      # OPNSENSE_EXPORTER_LOGS_SYSLOG_TLS_KEY_FILE: ""

      # ==========================================================================
      # Log shipping: Zenarmor receiver
      # The exporter poses as an Elasticsearch node so Zenarmor streams its reporting
      # data here. High volume: trim at source with Zenarmor's own indexes setting.
      # ==========================================================================
      # Comma-separated CIDR allowlist of hosts permitted to stream (e.g.
      # 10.0.0.254/32). Empty accepts any sender. The receiver is unauthenticated unless
      # --logs.zenarmor.auth-user is set, so set this on a shared network.
      # OPNSENSE_EXPORTER_LOGS_ZENARMOR_ALLOWED_PEERS: ""
      # Password for --logs.zenarmor.auth-user.
      # OPNSENSE_EXPORTER_LOGS_ZENARMOR_AUTH_PASSWORD: ""
      # Require HTTP basic auth on the Zenarmor receiver, with this username. Set the
      # same credentials in Zenarmor's streaming settings. Empty disables auth.
      # OPNSENSE_EXPORTER_LOGS_ZENARMOR_AUTH_USER: ""
      # Dump Zenarmor signals this receiver does not model (unhandled Elasticsearch
      # endpoints, unknown families, documents that would not parse) to
      # --logs.debug-capture.dir for inspection. Requires --logs.debug-capture.dir.
      # While on, the unhandled-endpoint warning is suppressed - the capture file
      # carries the same signal.
      # OPNSENSE_EXPORTER_LOGS_ZENARMOR_DEBUG_CAPTURE: "false"
      # Drop records describing the exporter's own Elasticsearch ingest connection -
      # Zenarmor inspects the link the receiver listens on, so it reports the very
      # connection delivering its records (roughly 15% of all volume, and most of the
      # http family). Matched on the streaming peer's address plus the receiver's listen
      # port, never the destination address, which a containerised exporter cannot know.
      # Set false to keep them; drops are counted as
      # logs_rejected_total{reason="self_traffic"}.
      # OPNSENSE_EXPORTER_LOGS_ZENARMOR_DROP_SELF_TRAFFIC: "true"
      # Enable the Zenarmor receiver: poses as an Elasticsearch node so Zenarmor can
      # stream its reporting data (connections, DNS, TLS, HTTP, threat alerts) to the
      # exporter, which ships it enriched over OTLP. Off by default. Requires
      # --logs.enabled. Configure the firewall under Configuration/Zenarmor > Settings >
      # Streaming Data > 'Stream Reporting Data to External Elasticsearch' - NOT the
      # initial wizard's 'Remote Elasticsearch Database', which replaces local reporting
      # irreversibly.
      # OPNSENSE_EXPORTER_LOGS_ZENARMOR_ENABLED: "false"
      # Enrich received Zenarmor records from the OPNsense API: friendly interface
      # names, local/remote scope and well-known service names. Zenarmor resolves
      # hostnames, MACs and device identity itself, so this adds only what it does not
      # already know.
      # OPNSENSE_EXPORTER_LOGS_ZENARMOR_ENRICH: "true"
      # Drop Zenarmor records whose FIELD matches REGEX, as FIELD=~REGEX (e.g.
      # 'server_name=~.*\.grafana\.net'). Repeatable; default off. The field name is
      # validated at startup against the receiver's attribute vocabulary - a typo is a
      # startup error, never a silent no-op. Derived counters are observed BEFORE the
      # drop, so opnsense_log_events_zenarmor_total stays complete; drops are counted as
      # logs_rejected_total{reason="excluded"} and logs_zenarmor_excluded_total{rule}.
      # EXCLUSION IS LOSSY: the derived counters carry no server_name, query or
      # device_name, so an excluded record's forensic detail is gone for good. Prefer a
      # query-time filter unless volume genuinely forces this. Set via env as one rule
      # per LINE.
      # OPNSENSE_EXPORTER_LOGS_ZENARMOR_EXCLUDE: ""
      # Comma-separated Zenarmor families to ship (conn, dns, tls, http, alert, sip).
      # Empty ships all of them. Prefer restricting this at the Zenarmor end instead -
      # data cut at source never crosses the wire. Zenarmor streams ~2.5-3.3M
      # records/day (~4-6 GB/day of JSON), of which conn is ~61%.
      # OPNSENSE_EXPORTER_LOGS_ZENARMOR_FAMILIES: ""
      # Listen address for the Zenarmor receiver. Point Zenarmor's streaming URI at it.
      # OPNSENSE_EXPORTER_LOGS_ZENARMOR_LISTEN_HTTP: ":9200"
      # Maximum bulk requests processed concurrently by the Zenarmor receiver. The
      # per-request body limit bounds one request; without this, N simultaneous requests
      # each buffer that full allowance. Excess requests are refused with 503 before a
      # body is read. 0 disables the limit.
      # OPNSENSE_EXPORTER_LOGS_ZENARMOR_MAX_CONCURRENT_REQUESTS: "8"
      # PEM server certificate for the Zenarmor receiver. Set with
      # --logs.zenarmor.tls-key-file to serve HTTPS, and use an https:// URI in
      # Zenarmor's streaming settings.
      # OPNSENSE_EXPORTER_LOGS_ZENARMOR_TLS_CERT_FILE: ""
      # PEM private key for --logs.zenarmor.tls-cert-file.
      # OPNSENSE_EXPORTER_LOGS_ZENARMOR_TLS_KEY_FILE: ""
      # How Zenarmor delivers its reporting data: 'elasticsearch' (default) runs the
      # built-in Elasticsearch receiver on --logs.zenarmor.listen-http; 'syslog' ingests
      # it through the shared syslog receiver (requires --logs.syslog.enabled and a
      # business-tier Zenarmor licence). families/exclude/enrich/drop-self-traffic apply
      # to either transport.
      # OPNSENSE_EXPORTER_LOGS_ZENARMOR_TRANSPORT: "elasticsearch"

      # ==========================================================================
      # Log shipping: polled lanes
      # Lanes that poll the firewall API rather than receiving a push.
      # ==========================================================================
      # Enable the crowdsec log source: ships CrowdSec alert and decision records to
      # Loki (there is no native syslog path for these - the plugin registers no syslog
      # scope; alerts live only in the LAPI). Requires --logs.enabled. Polls at a 60s
      # floor regardless of --logs.poll-interval. Silent when the os-crowdsec plugin is
      # absent. Off by default.
      # OPNSENSE_EXPORTER_LOGS_CROWDSEC_ENABLED: "false"
      # Enable the IDS (Suricata EVE alert) log source: ships full Suricata alert
      # records polled via ids/service/query_alerts. Off by default. Requires
      # --logs.enabled. If the box already forwards EVE JSON via syslog
      # (ids.general.syslog_eve), prefer that native path instead of also enabling this
      # source - do not ship the same alerts twice.
      # OPNSENSE_EXPORTER_LOGS_IDS_ENABLED: "false"
      # Enable the opt-in Unbound per-query DNS log source (pi-hole-style query log to
      # Loki: domain, client, action, resolution source, blocklist and dnssec_status per
      # query). Off by default; requires --logs.enabled. CAVEAT: without a per-client
      # filter, Unbound's query-log backend (DuckDB) only ever exposes the newest 1000
      # rows across the WHOLE resolver - on a firewall sustaining more than roughly 1000
      # queries between polls, older rows silently fall out of that window before this
      # exporter ever sees them. This is accepted, honestly-counted sampling loss, not a
      # bug: it is tracked via
      # opnsense_exporter_logs_possible_gap_total{source="unbound"}, never silently
      # dropped. Homelab/SMB query volumes are fine; a busy enterprise resolver should
      # not enable this. Also requires Unbound reporting/statistics enabled on the
      # firewall. Poll floor 15s regardless of --logs.poll-interval.
      # OPNSENSE_EXPORTER_LOGS_UNBOUND_ENABLED: "false"

      # ==========================================================================
      # Log shipping: debug capture
      # Writes UNMODELLED receiver signals to disk so unknown payloads can be modelled
      # without a packet capture. An investigation tool for a window, not a running
      # mode.
      # ==========================================================================
      # Directory to dump UNMODELLED receiver signals into for inspection, as NDJSON
      # under <dir>/<receiver>/ (files are 0600 and carry real network data - addresses,
      # DNS queries, TLS SNI, HTTP hosts). Off unless set. Enable capture per receiver
      # with --logs.zenarmor.debug-capture / --logs.syslog.debug-capture. Point a
      # writable bind mount here; only signals the exporter cannot model are written,
      # never the full stream.
      # OPNSENSE_EXPORTER_LOGS_DEBUG_CAPTURE_DIR: ""
      # Total size cap for --logs.debug-capture.dir (e.g. 256MiB, 1GB). Capture STOPS
      # when the dir reaches this, keeping the oldest samples; it never deletes to make
      # room, so a debug capture can never fill the disk. Counts bytes left by previous
      # runs.
      # OPNSENSE_EXPORTER_LOGS_DEBUG_CAPTURE_MAX_BYTES: "256MiB"

      # ==========================================================================
      # Log shipping: pipeline
      # The shared sink, buffer and delivery settings every log lane feeds.
      # ==========================================================================
      # Maximum number of records the emitter hands to the sink per batch. The sink pays
      # a fixed per-resource-partition round-trip, and distinct partitions plateau with
      # batch duration, so a larger batch amortises that fixed cost almost linearly
      # rather than costing proportionally more.
      # OPNSENSE_EXPORTER_LOGS_BATCH_MAX: "5000"
      # Aggregate byte budget for the in-memory backpressure queue. The record-count cap
      # (--logs.buffer-size) alone does not bound memory: a receiver preserves each
      # record's raw body, so a few large records can outweigh thousands of small ones.
      # On overflow the oldest record is dropped and counted, exactly as for the count
      # cap. 0 disables the byte budget.
      # OPNSENSE_EXPORTER_LOGS_BUFFER_MAX_BYTES: "134217728"
      # Capacity of the in-memory backpressure queue between pollers and the sink. On
      # overflow the oldest record is dropped and counted (logs_dropped_total). At the
      # measured ~475 bytes/record retained size, 65536 records is ~31MB, comfortably
      # under the 128MiB --logs.buffer-max-bytes default, so the two bounds read against
      # one number instead of this record cap silently binding first at a fraction of
      # the byte budget.
      # OPNSENSE_EXPORTER_LOGS_BUFFER_SIZE: "65536"
      # Enable the opt-in log/event shipping pipeline (polls OPNsense event APIs and
      # ships to Loki via OTLP). Off by default. Independent of --otlp.enabled (which
      # gates metrics).
      # OPNSENSE_EXPORTER_LOGS_ENABLED: "false"
      # Maximum distinct label tuples retained per derived log_events metric family.
      # Receivers are push-based and syslog over UDP has a spoofable source, so tuple
      # values are sender-controlled: without this bound a sender can grow
      # process-lifetime metric state without limit. Tuples beyond the cap fold into a
      # counted overflow series rather than being dropped silently. 0 disables the cap.
      # OPNSENSE_EXPORTER_LOGS_MAX_METRIC_KEYS: "5000"
      # Maximum estimated retained size for a single record - its body, source and
      # attributes plus a fixed overhead allowance, measured the same way as
      # --logs.buffer-max-bytes so the two read against one number. A record larger than
      # this is rejected at ingest and counted rather than queued, so one oversized
      # record cannot occupy the whole queue budget or become a batch the sink
      # permanently refuses. 0 disables the per-record cap.
      # OPNSENSE_EXPORTER_LOGS_MAX_RECORD_BYTES: "1048576"
      # Base interval between event polls per source (floor 5s). Sources may raise their
      # own floor.
      # OPNSENSE_EXPORTER_LOGS_POLL_INTERVAL: "10s"
      # Maximum number of resource partitions within one batch that the sink exports
      # concurrently. Each partition is a separate synchronous wire request, so a batch
      # of N partitions previously cost N sequential round-trips. 1 restores the old
      # fully-sequential behaviour. Values below 1 are normalised to 1.
      # OPNSENSE_EXPORTER_LOGS_SHIP_CONCURRENCY: "8"
      # Maximum delivery attempts for one batch before it is dropped and counted
      # (logs_dropped_total{reason="ship_failed_permanent"}). Retries are exponentially
      # backed off. Without this bound a batch the sink permanently refuses is retried
      # forever by the single emitter goroutine, wedging all subsequent delivery. 0
      # restores unlimited retries.
      # OPNSENSE_EXPORTER_LOGS_SHIP_MAX_ATTEMPTS: "10"
      # Log shipping sink: otlp (OTLP logs, reuses the --otlp.* transport) or stdout
      # (one JSON line per event).
      # OPNSENSE_EXPORTER_LOGS_SINK: "otlp"
      # Optional path to persist per-source cursors across restarts (atomic JSON). Empty
      # = in-memory only (resume from now on restart).
      # OPNSENSE_EXPORTER_LOGS_STATE_FILE: ""

      # ==========================================================================
      # Flow: NetFlow receiver
      # Opens an UNAUTHENTICATED UDP socket. NetFlow has no authentication of any kind,
      # so restrict it with an allowlist or by firewalling the port.
      # ==========================================================================
      # CIDR allowlist of exporters permitted to send flow records, repeatable. Empty
      # means accept from anyone, which is a deliberate decision to trust the network
      # rather than a default to drift into: anything that can reach the port can inject
      # flow records.
      # OPNSENSE_EXPORTER_FLOW_NETFLOW_ALLOWED_PEERS: ""
      # Dump raw NetFlow datagrams to --logs.debug-capture.dir. "unidentified" writes
      # only datagrams carrying something the decoder could not interpret (an unmodelled
      # template element, an options template, an unknown flowset, or a datagram that
      # would not decode at all) - cheap, and the mode worth leaving on. "all" writes
      # every datagram, for regenerating a replay fixture or measuring the export;
      # deliberately heavy, bounded only by --logs.debug-capture.max-bytes. Requires
      # --flow.netflow.enabled and the shared dir.
      # OPNSENSE_EXPORTER_FLOW_NETFLOW_DEBUG_CAPTURE: "off"
      # Enable the NetFlow v5/v9 receiver. Opens an UNAUTHENTICATED UDP socket: NetFlow
      # has no authentication of any kind, so restrict it with
      # --flow.netflow.allowed-peers or by firewalling the port. Requires
      # --flow.enabled.
      # OPNSENSE_EXPORTER_FLOW_NETFLOW_ENABLED: "false"
      # Override the derived NetFlow ifIndex-to-device map, as comma-separated
      # index=device pairs (e.g. "1=ixl0,5=igb0,13=ixl0_vlan50"). Entries listed here
      # beat the derived map; indices not listed still use it, so pin every index that
      # carries traffic. Read yours off the box with: ifinfo | awk '$1 == "Interface" {
      # n++; print n, $2 }' - that is the whole enumeration. ngctl list | grep netflow
      # shows only the interfaces netflow captures, and an egress index can legitimately
      # name one it does not. A pin is a STATIC assertion against a POSITIONAL index:
      # adding or removing any interface renumbers every position above it, so a pin
      # that was right when written silently goes stale and then actively mislabels,
      # because it still wins. Re-read the enumeration after any interface change and
      # watch opnsense_flow_ifindex_conflicts, whose reason="derived_differs" is that
      # divergence; settle which side is right with ngctl show netflow_<device>:, where
      # the ifaceN hook name is the index ng_netflow actually stamps on the records.
      # OPNSENSE_EXPORTER_FLOW_NETFLOW_IFINDEX_MAP: ""
      # Address the NetFlow receiver binds, host:port. Bound eagerly at startup, so a
      # port already in use is a startup error rather than a receiver that is silently
      # never there.
      # OPNSENSE_EXPORTER_FLOW_NETFLOW_LISTEN: ":2055"

      # ==========================================================================
      # Flow: rollups and correlation
      # Volume counters derived from flow records, and the correlator that joins NetFlow
      # fragments with Zenarmor conn documents.
      # ==========================================================================
      # Correlate NetFlow fragments and Zenarmor conn documents into one merged flow
      # record per connection-window. A pass-through when only one source is present.
      # Off emits NetFlow records raw and per-fragment.
      # OPNSENSE_EXPORTER_FLOW_CORRELATE: "true"
      # Hard cap on live correlator entries. At the cap the oldest is force-emitted
      # (never dropped) and counted. The NetFlow ingress is unauthenticated, so this
      # bounds memory against a flood. 0 is unbounded (unwise with the listener on).
      # OPNSENSE_EXPORTER_FLOW_CORRELATE_MAX_ENTRIES: "50000"
      # How long the correlator holds a connection-window before emitting. Also the
      # maximum a flow log is delayed. NetFlow export lag runs to ~30m for long flows
      # (#346), so a flow whose records straddle the window emits a partial per window
      # rather than one joined record.
      # OPNSENSE_EXPORTER_FLOW_CORRELATE_WINDOW: "3m"
      # Entries in the DNS answer cache that gives a flow to a bare IP its dst.domain,
      # fed by the Zenarmor dns family. Over the cap it stops inserting rather than
      # evicting hot entries. 0 disables domain enrichment.
      # OPNSENSE_EXPORTER_FLOW_DNS_CACHE_SIZE: "50000"
      # Enable flow rollups: bounded byte and packet volume counters derived from flow
      # records. Costs nothing where no flow source is configured - the metrics are
      # simply silent, like log_events without the syslog receiver. Set
      # --exporter.disable-flow to remove the collector entirely.
      # OPNSENSE_EXPORTER_FLOW_ENABLED: "true"
      # Add a `country` label to the flow volume metrics. OFF by default and it should
      # usually stay off: country is a ~250-value dimension multiplied against every
      # existing flow series, so turning it on can multiply opnsense_flow_bytes_total's
      # cardinality roughly 250-fold. --flow.top-n and --flow.max-keys still bound the
      # result, which means the practical effect on a busy box is that real series start
      # folding into __other__ rather than that the family grows without limit. ASN and
      # city NEVER become labels at any setting. Geo on flow LOGS needs no flag - it is
      # unconditional whenever --geoip.enabled is set.
      # OPNSENSE_EXPORTER_FLOW_GEOIP_METRIC_DIMS: "false"
      # Flow log emission: "per_flow" ships one OTLP log record per correlated flow on
      # the shared log pipeline; "off" ships none while still deriving all metrics.
      # Zenarmor conn documents ship on their own lane regardless.
      # OPNSENSE_EXPORTER_FLOW_LOG_MODE: "per_flow"
      # Maximum distinct label combinations the flow accumulator tracks in memory. A
      # separate bound from --flow.top-n: this caps memory between scrapes, that caps
      # emitted series. Combinations first seen at the cap fold into __other__ and are
      # counted by opnsense_flow_rollup_capped_total. 0 is unbounded.
      # OPNSENSE_EXPORTER_FLOW_MAX_KEYS: "2500"
      # Cap on flow log records shipped per minute; excess is TRUNCATED (never sampled)
      # and counted. A flood guard on the unauthenticated NetFlow ingress. 0 is
      # unlimited. Metrics are never truncated.
      # OPNSENSE_EXPORTER_FLOW_MAX_LOGS_PER_WINDOW: "0"
      # Maximum flow series emitted per scrape. Everything beyond folds into a single
      # __other__ series per source, so the family still sums exactly at any limit. 0
      # emits every tracked combination.
      # OPNSENSE_EXPORTER_FLOW_TOP_N: "1000"
      # Emit opnsense_flow_top_talker_bytes_total: bytes per internal host and
      # direction, top-N with an __other__ remainder. OFF by default because the host
      # label is high cardinality; the top-N bounds it but a host label is still one
      # series per host.
      # OPNSENSE_EXPORTER_FLOW_TOP_TALKERS: "false"
      # Derive flow records from the Zenarmor receiver's conn documents. Adds no new log
      # records to Loki: the conn document ships exactly as before and this only feeds
      # the metric rollup. Requires --logs.zenarmor.enabled to produce anything.
      # OPNSENSE_EXPORTER_FLOW_ZENARMOR: "true"

      # ==========================================================================
      # GeoIP enrichment
      # Local MaxMind lookups that give every flow record a country, continent and ASN,
      # whether or not Zenarmor saw the connection. Purely local - no lookup touches the
      # network. Fail-open: a missing database means the attributes are absent, never a
      # failed start.
      # ==========================================================================
      # Path to a MaxMind GeoLite2-ASN / GeoIP2-ISP database. This is the one enrichment
      # no amount of Zenarmor coverage supplies: Zenarmor ships no ASN database on any
      # box. Defaults to the downloaded copy when --geoip.download.enabled is set.
      # OPNSENSE_EXPORTER_GEOIP_ASN_DATABASE: ""
      # Path to a MaxMind Country OR City database (GeoLite2-Country, GeoLite2-City,
      # GeoIP2-City). A City database is a strict superset, so one path accepts either
      # and the city/region attributes are simply absent with a Country file. Defaults
      # to the downloaded copy when --geoip.download.enabled is set. A missing file is
      # not an error - enrichment is fail-open and the attributes are just absent.
      # OPNSENSE_EXPORTER_GEOIP_COUNTRY_DATABASE: ""
      # MaxMind account ID for the database download API (the Basic-auth username).
      # OPNSENSE_EXPORTER_GEOIP_DOWNLOAD_ACCOUNT_ID: ""
      # Directory downloaded databases are installed into, as <dir>/<edition>.mmdb. Must
      # be writable and should be persistent - a volume that is lost on restart costs a
      # full download every start, against MaxMind's daily limit.
      # OPNSENSE_EXPORTER_GEOIP_DOWNLOAD_DIR: "/var/lib/opnsense-exporter/geoip"
      # Comma-separated MaxMind edition IDs to download. Default is Country + ASN (~9 MB
      # and ~12 MB resident). Swap GeoLite2-Country for GeoLite2-City (~60 MB resident)
      # to get city and region attributes without Zenarmor - the same
      # --geoip.country-database path accepts either edition.
      # OPNSENSE_EXPORTER_GEOIP_DOWNLOAD_EDITIONS: "GeoLite2-Country,GeoLite2-ASN"
      # Download MaxMind databases directly, so no geoipupdate cron or sidecar is
      # needed. Requires --geoip.download.account-id and a license key. Conditional
      # requests mean an unchanged database costs a 304 and no download quota. Off by
      # default: operator-managed files are the supported baseline and this adds an
      # outbound network dependency.
      # OPNSENSE_EXPORTER_GEOIP_DOWNLOAD_ENABLED: "false"
      # How often to ask MaxMind for a newer build. GeoLite2 rebuilds twice a week, so
      # daily is ample; an unchanged database answers 304 and costs no quota. The first
      # download runs at startup regardless, so a fresh container is not blind for a
      # whole interval. 0 downloads only at startup.
      # OPNSENSE_EXPORTER_GEOIP_DOWNLOAD_INTERVAL: "24h"
      # MaxMind license key. This flag/ENV or
      # OPNSENSE_EXPORTER_GEOIP_DOWNLOAD_LICENSE_KEY_FILE may be set; the file form is
      # preferred for a container secret.
      # OPNSENSE_EXPORTER_GEOIP_DOWNLOAD_LICENSE_KEY: ""
      # End-to-end timeout for one edition's download. A timeout leaves the installed
      # database untouched and is retried on the next interval.
      # OPNSENSE_EXPORTER_GEOIP_DOWNLOAD_TIMEOUT: "5m"
      # Enable local GeoIP enrichment of flow records from MaxMind .mmdb files on disk.
      # Adds country/continent/city/ASN attributes to flow LOGS for external addresses,
      # so geo no longer depends on whether Zenarmor happened to see the connection.
      # Purely local: no lookup ever touches the network. Off by default because it
      # needs a database the exporter does not ship.
      # OPNSENSE_EXPORTER_GEOIP_ENABLED: "false"
      # How often to re-stat the database paths and hot-swap a changed file. This is
      # what makes the operator-managed path work - a geoipupdate cron, a sidecar or a
      # re-mounted volume can rewrite the files under a running exporter. Separate from
      # --geoip.download.interval, which asks MaxMind whether a newer build exists. 0
      # disables reloading.
      # OPNSENSE_EXPORTER_GEOIP_RELOAD_INTERVAL: "15m"

    ports:
      - "8080:8080" # web.listen-address - the /metrics and operator-console port.

      # The receivers below listen on their own ports and are all off by default.
      # Publish a port only once its receiver is enabled.
      # Syslog UDP. Publish BOTH the UDP and TCP lines below - the commonest reason a
      # syslog target appears to send nothing is that only one transport got published.
      # (--logs.syslog.enabled)
      # - "5514:5514/udp"
      # Syslog TCP. See the UDP line above. (--logs.syslog.enabled)
      # - "5514:5514/tcp"
      # Zenarmor's Elasticsearch-shaped stream. 9200 is only the Elasticsearch
      # convention; nothing requires it, it just has to match the URI configured in
      # Zenarmor. (--logs.zenarmor.enabled)
      # - "9200:9200"
      # NetFlow v5/v9. UNAUTHENTICATED - restrict with flow.netflow.allowed-peers or a
      # firewall rule before publishing this on a shared network.
      # (--flow.netflow.enabled)
      # - "2055:2055/udp"