A lightweight Prometheus exporter for PasarGuard that exposes per-user traffic counters and online status as Prometheus metrics.
It queries the PasarGuard Panel API for authentication, user lists, node discovery, and online status, and each Node REST API for per-user upload/download byte counters via Xray stats.
# HELP down_bytes_total Bytes sent to the peer
# TYPE down_bytes_total counter
down_bytes_total{email="alice"} 982372615
# HELP up_bytes_total Bytes received from the peer
# TYPE up_bytes_total counter
up_bytes_total{email="alice"} 52819433
# HELP is_online Is the peer online
# TYPE is_online gauge
is_online{email="alice"} 1
# HELP pasarguard_up Whether the last scrape was successful
# TYPE pasarguard_up gauge
pasarguard_up 1
# HELP pasarguard_scrape_duration_seconds Duration of the last scrape in seconds
# TYPE pasarguard_scrape_duration_seconds gauge
pasarguard_scrape_duration_seconds 0.342
The email label contains the human-readable PasarGuard username (not a numeric ID).
All configuration is through environment variables. No config files.
The exporter authenticates with the Panel using either a Panel API key (recommended) or a
username/password pair. API keys can be created in the Panel under Admin → API Keys — see the
PasarGuard API keys docs. They're revocable
independently of the admin password, can be given an expiration date, and can optionally be scoped
to read-only users/nodes permissions instead of inheriting the full rights of the owning admin.
| Variable | Required | Default | Description |
|---|---|---|---|
PANEL_URL |
Yes | — | PasarGuard Panel base URL (e.g. https://panel.example.com) |
PANEL_API_KEY |
Yes* | — | Panel API key (pg_key_...), sent as X-Api-Key on every request |
PANEL_API_KEY_FILE |
Yes* | — | Path to file containing the Panel API key (alternative to PANEL_API_KEY) |
PANEL_USERNAME |
Yes* | — | Panel admin username |
PANEL_PASSWORD |
Yes* | — | Panel admin password |
PANEL_PASSWORD_FILE |
Yes* | — | Path to file containing the Panel admin password (alternative to PANEL_PASSWORD) |
LISTEN_ADDR |
No | :9115 |
Address and port the exporter listens on |
ONLINE_THRESHOLD |
No | 2m |
Duration since last online_at to consider a user online (Go duration format) |
SCRAPE_TIMEOUT |
No | 30s |
Maximum time for a single scrape to complete (Go duration format) |
PANEL_BASIC_AUTH_USERNAME |
No | — | HTTP Basic Auth username for Panel requests (e.g. reverse proxy auth) |
PANEL_BASIC_AUTH_PASSWORD |
No | — | HTTP Basic Auth password for Panel requests |
PANEL_TLS_CERT_FILE |
No | — | Path to client certificate PEM file for mTLS with the Panel |
PANEL_TLS_KEY_FILE |
No | — | Path to client private key PEM file for mTLS with the Panel |
PANEL_TLS_CA_FILE |
No | — | Path to CA certificate PEM file to verify the Panel's server certificate |
NODE_TLS_CERT_FILE |
No | — | Path to client certificate PEM file for mTLS with Nodes |
NODE_TLS_KEY_FILE |
No | — | Path to client private key PEM file for mTLS with Nodes |
* Either an API key (PANEL_API_KEY/PANEL_API_KEY_FILE) or a username/password pair
(PANEL_USERNAME + PANEL_PASSWORD/PANEL_PASSWORD_FILE) is required. If an API key is set, it takes
precedence and the username/password fields are ignored. Within each pair, the non-_FILE variable
takes precedence if both are set, and trailing newlines are stripped from file contents.
The admin account backing your credentials (the Panel account itself, or the account an API key is
scoped to / inherits from) must have sudo admin privileges, or equivalent custom users/nodes
read permissions with scope "all" (required to access node API keys and stats for every user, not just
the credential owner's own).
If your Panel is behind a reverse proxy that requires HTTP Basic Auth, set both PANEL_BASIC_AUTH_USERNAME and PANEL_BASIC_AUTH_PASSWORD. The credentials are sent as an Authorization: Basic ... header on every request to the Panel API, alongside the JWT Bearer token.
To use client certificates for authentication with the Panel or Node APIs, set the corresponding *_TLS_CERT_FILE and *_TLS_KEY_FILE pairs. Panel and Node use independent cert pairs.
PANEL_TLS_CA_FILE allows specifying a custom CA to verify the Panel's server certificate (useful for self-signed certs). Node server CAs are provided per-node via the Panel API's server_ca field.
docker build -t pasarguard-exporter .
# Using an API key (recommended)
docker run -d \
--name pasarguard-exporter \
-p 9115:9115 \
-e PANEL_URL=https://panel.example.com \
-e PANEL_API_KEY=pg_key_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \
pasarguard-exporter
# Or using username/password
docker run -d \
--name pasarguard-exporter \
-p 9115:9115 \
-e PANEL_URL=https://panel.example.com \
-e PANEL_USERNAME=admin \
-e PANEL_PASSWORD=secret \
pasarguard-exporterThe image is ~18 MB (multi-stage build with distroless).
Requires Go 1.23+.
go build -o pasarguard-exporter ./cmd/exporter/
export PANEL_URL=https://panel.example.com
export PANEL_API_KEY=pg_key_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
# Or: export PANEL_USERNAME=admin; export PANEL_PASSWORD=secret
./pasarguard-exporterMetrics are served at http://localhost:9115/metrics.
Add a scrape job to your prometheus.yml:
scrape_configs:
- job_name: pasarguard
static_configs:
- targets: ['localhost:9115']
scrape_interval: 30s
scrape_timeout: 30sA pre-built Grafana dashboard is included at grafana/dashboard.json. Import it via Dashboards → Import → Upload JSON file in Grafana.
The dashboard includes:
- Online Users — current count and over-time graph
- Traffic rates — per-user download/upload rates (bytes/sec)
- Total traffic — cumulative download, upload, and combined totals
- Traffic per user — horizontal bar chart with per-user breakdown
- User status table — online/offline status, download, and upload per user
A User dropdown allows filtering all panels by one or more users.
On each Prometheus scrape (GET /metrics):
- Authenticate with the Panel API — a static API key (recommended, no refresh needed) or username/password (JWT, auto-refreshes on 401).
- Fetch all users from the Panel (paginated, 100 per page). The user list provides usernames and
online_attimestamps. - Discover nodes from the Panel. Each node entry includes the
api_keyandserver_caneeded to query it. - Query each connected node for per-user Xray traffic stats via gRPC (
NodeService.GetStats). Unreachable nodes are skipped — they don't fail the entire scrape. - Accumulate counters across scrapes. Because the Panel periodically resets Xray counters (
reset=true), raw values can drop to zero at any time. The exporter reads withreset=falseand maintains in-memory accumulators to produce monotonically increasing Prometheus counters. - Emit metrics for every user from the Panel's user list, with traffic totals summed across all nodes.
Xray's internal counters are reset to zero whenever the Panel reads them. The exporter handles this:
- If the raw value grew since last seen: add the delta to the accumulator.
- If the raw value dropped (Panel reset detected): treat the new value as fresh traffic since the reset and add it.
This ensures up_bytes_total and down_bytes_total are always monotonically increasing, which is what Prometheus expects from counters.
Note: Accumulators are in-memory. Restarting the exporter resets counters to the current Xray values. This causes a one-time counter decrease visible in Prometheus as a counter reset — Prometheus handles this natively with its
rate()/increase()functions.
A user is considered online (is_online{email="..."} 1) if their online_at timestamp from the Panel is within the ONLINE_THRESHOLD window (default: 2 minutes). Otherwise the value is 0.
The exporter failed to complete a scrape. Check logs for the root cause — it will be one of the errors below.
The exporter couldn't reach the Panel API or the API rejected the request.
unexpected status 401 or unexpected status 422
If using PANEL_API_KEY: the key is invalid, mistyped, revoked, or expired. Mint a new one in the Panel
under Admin → API Keys and update PANEL_API_KEY/PANEL_API_KEY_FILE — API keys don't auto-refresh
since there's nothing to refresh.
If using PANEL_USERNAME/PANEL_PASSWORD: bad credentials. Verify they (or PANEL_PASSWORD_FILE) are
correct. The exporter auto-refreshes the JWT on 401, so if this persists the credentials themselves are
wrong.
unexpected status 403: {"detail":"You're not allowed"}
The credentials lack permissions. The nodes endpoint requires sudo admin privileges (or, for a
custom-scoped API key, users.read and nodes.read permissions with scope "all"). Verify your account:
# API key
curl -s https://your-panel/api/admin \
-H "X-Api-Key: $PANEL_API_KEY" | jq .is_sudo
# Username/password
TOKEN=$(curl -s -X POST https://your-panel/api/admin/token \
-d "username=YOUR_USER&password=YOUR_PASS&grant_type=password" \
-H "Content-Type: application/x-www-form-urlencoded" | jq -r .access_token)
curl -s https://your-panel/api/admin \
-H "Authorization: Bearer $TOKEN" | jq .is_sudoIf is_sudo is false, promote the account, use a sudo admin account, or (for API keys) grant the key
users.read/nodes.read permissions with scope "all".
connection refused / no such host / TLS handshake errors
Network issue. Verify PANEL_URL is reachable from where the exporter runs. If the Panel uses a self-signed certificate, set PANEL_TLS_CA_FILE.
A specific node couldn't be queried. The exporter skips unreachable nodes and still emits metrics from reachable ones — this is a warning, not a fatal error.
The exporter connects to each node's gRPC port (the port field from the Panel, not api_port). Common causes:
- Node is offline or its gRPC port is firewalled
- TLS certificate mismatch (the node's
server_cafrom the Panel doesn't match) - Wrong API key (usually means the Panel's node config is stale — re-sync the node in the Panel)
- If using mTLS for nodes, verify
NODE_TLS_CERT_FILE/NODE_TLS_KEY_FILEare correct
- Nodes may have no active users yet
- Verify at least one node has
"status": "connected"in the Panel — the exporter skips nodes with other statuses - Check logs for
failed to get stats from nodewarnings — if all nodes fail, counters stay at zero
Expected behavior. Accumulators are in-memory. After a restart, counters reset to current Xray values. Prometheus handles this natively — rate() and increase() functions account for counter resets.
# Build
go build ./cmd/exporter/
# Vet
go vet ./...
# Docker
docker build -t pasarguard-exporter .