{"id":49549091,"url":"https://github.com/alchemylink/xray-stats-exporter","last_synced_at":"2026-05-02T21:04:02.467Z","repository":{"id":349320997,"uuid":"1194324471","full_name":"AlchemyLink/xray-stats-exporter","owner":"AlchemyLink","description":"Prometheus exporter for Xray per-user and per-inbound traffic via StatsService gRPC API","archived":false,"fork":false,"pushed_at":"2026-04-20T18:23:22.000Z","size":47,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-20T19:34:09.176Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/AlchemyLink.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-28T07:38:18.000Z","updated_at":"2026-04-20T18:18:18.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/AlchemyLink/xray-stats-exporter","commit_stats":null,"previous_names":["alchemylink/xray-stats-exporter"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/AlchemyLink/xray-stats-exporter","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AlchemyLink%2Fxray-stats-exporter","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AlchemyLink%2Fxray-stats-exporter/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AlchemyLink%2Fxray-stats-exporter/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AlchemyLink%2Fxray-stats-exporter/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/AlchemyLink","download_url":"https://codeload.github.com/AlchemyLink/xray-stats-exporter/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AlchemyLink%2Fxray-stats-exporter/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32549388,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-02T19:18:06.202Z","status":"ssl_error","status_checked_at":"2026-05-02T19:16:21.335Z","response_time":132,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-05-02T21:04:01.491Z","updated_at":"2026-05-02T21:04:02.458Z","avatar_url":"https://github.com/AlchemyLink.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# xray-stats-exporter\n\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)\n[![Go Report Card](https://goreportcard.com/badge/github.com/alchemylink/xray-stats-exporter)](https://goreportcard.com/report/github.com/alchemylink/xray-stats-exporter)\n[![CI](https://github.com/AlchemyLink/xray-stats-exporter/actions/workflows/ci.yml/badge.svg)](https://github.com/AlchemyLink/xray-stats-exporter/actions/workflows/ci.yml)\n[![Status](https://img.shields.io/badge/Status-Alpha%20Testing-orange)](https://github.com/AlchemyLink/xray-stats-exporter)\n\nPrometheus exporter for [Xray-core](https://github.com/XTLS/Xray-core) — exposes per-user and per-inbound traffic metrics, geo location data, and **TSPU/DPI interference detection** via the Xray gRPC StatsService API.\n\nBuilt for self-hosted VPN operators who need to monitor user bandwidth, detect censorship events, and track throughput degradation in Grafana.\n\n\u003e [!WARNING]\n\u003e **Alpha testing.** Core metrics are stable, but TSPU detection patterns and throughput degradation logic are being tuned against real-world DPI events. API and flag names may change before v1.0.\n\n---\n\n## Table of Contents\n\n- [Metrics](#metrics)\n- [Requirements](#requirements)\n- [Installation](#installation)\n- [Flags](#flags)\n- [Systemd Service](#systemd-service)\n- [Xray Config Requirements](#xray-config-requirements)\n- [TSPU / DPI Detection](#tspu--dpi-detection)\n- [Throughput Degradation Detection](#throughput-degradation-detection)\n- [Geo Metrics](#geo-metrics)\n- [Prometheus Scrape Config](#prometheus-scrape-config)\n- [Grafana Dashboard](#grafana-dashboard)\n- [Related Projects](#related-projects)\n\n---\n\n## Metrics\n\n### Traffic (gRPC StatsService)\n\n| Metric | Type | Labels | Description |\n|--------|------|--------|-------------|\n| `xray_user_uplink_bytes_total` | counter | `email` | Cumulative bytes sent by user (client → server) |\n| `xray_user_downlink_bytes_total` | counter | `email` | Cumulative bytes received by user (server → client) |\n| `xray_inbound_uplink_bytes_total` | counter | `inbound` | Cumulative uplink bytes per inbound tag |\n| `xray_inbound_downlink_bytes_total` | counter | `inbound` | Cumulative downlink bytes per inbound tag |\n\n### Throughput (computed per scrape)\n\n| Metric | Type | Labels | Description |\n|--------|------|--------|-------------|\n| `xray_inbound_throughput_bytes_per_second` | gauge | `inbound` | Current bytes/sec per inbound (rolling, combined up+down) |\n| `xray_throughput_degradation_total` | counter | `inbound` | Scrapes where rate dropped \u003e70% below rolling 10-sample baseline |\n\n### TSPU / DPI Detection (error log tail)\n\n| Metric | Type | Labels | Description |\n|--------|------|--------|-------------|\n| `xray_handshake_failure_total` | counter | `inbound` | TLS handshake failures — classic TSPU RST-during-handshake |\n| `xray_connection_reset_total` | counter | `inbound` | TCP RST / broken pipe events — TSPU forcibly drops connections |\n| `xray_probe_detected_total` | counter | `inbound` | Unexpected data / i/o timeout — active DPI probe signatures |\n\nRequires `--error-log-path`. See [TSPU / DPI Detection](#tspu--dpi-detection) for details.\n\n### Geo Location (access log tail)\n\n| Metric | Type | Labels | Description |\n|--------|------|--------|-------------|\n| `xray_user_last_country` | gauge | `email`, `country`, `city`, `lat`, `lon` | Last seen geo location per user (gauge=1) |\n| `xray_user_connections_total` | counter | `email`, `country`, `city` | Connections per user per location |\n| `xray_inbound_connections_total` | counter | `inbound`, `country` | Connections per inbound per country |\n\nRequires `--log-path` and `--geo-city-db`. See [Geo Metrics](#geo-metrics).\n\n### Exporter Health\n\n| Metric | Type | Labels | Description |\n|--------|------|--------|-------------|\n| `xray_up` | gauge | — | 1 if Xray gRPC API is reachable, 0 otherwise |\n| `xray_scrape_duration_seconds` | gauge | — | Time taken to scrape the Xray gRPC API |\n\n---\n\n## Requirements\n\n- **Xray-core** with `StatsService` enabled in the API config\n- Xray gRPC API accessible at `127.0.0.1:10085` (configurable)\n- Per-user stats enabled in Xray (`policy.levels` with `statsUserUplink` / `statsUserDownlink`)\n- Users must have an `email` field in the inbound client config\n\n**For TSPU detection (optional):**\n- Xray `error.log` with `Warning` level or higher\n\n**For geo metrics (optional):**\n- Xray `access.log` with real client IPs (not PROXY protocol without passthrough)\n- [GeoLite2-City.mmdb](https://dev.maxmind.com/geoip/geolite2-free-geolocation-data) from MaxMind (free registration required)\n- Optionally: `GeoLite2-ASN.mmdb` for ASN labels\n\n---\n\n## Installation\n\n### Download binary (recommended)\n\n```bash\ncurl -LO https://github.com/AlchemyLink/xray-stats-exporter/releases/latest/download/xray-stats-exporter-linux-amd64\nchmod +x xray-stats-exporter-linux-amd64\nsudo mv xray-stats-exporter-linux-amd64 /usr/local/bin/xray-stats-exporter\n```\n\n### Build from source\n\n```bash\ngit clone https://github.com/AlchemyLink/xray-stats-exporter.git\ncd xray-stats-exporter\ngo build -o xray-stats-exporter .\nsudo mv xray-stats-exporter /usr/local/bin/\n```\n\n### Run (minimal)\n\n```bash\nxray-stats-exporter \\\n  --listen=127.0.0.1:9551 \\\n  --xray-endpoint=127.0.0.1:10085\n```\n\n### Run (full — geo + TSPU)\n\n```bash\nxray-stats-exporter \\\n  --listen=127.0.0.1:9551 \\\n  --xray-endpoint=127.0.0.1:10085 \\\n  --log-path=/var/log/Xray/access.log \\\n  --error-log-path=/var/log/Xray/error.log \\\n  --geo-city-db=/var/lib/xray-exporter/GeoLite2-City.mmdb \\\n  --geo-asn-db=/var/lib/xray-exporter/GeoLite2-ASN.mmdb\n```\n\n### Verify\n\n```bash\ncurl -s http://127.0.0.1:9551/metrics | grep xray_\n```\n\n---\n\n## Flags\n\n| Flag | Default | Description |\n|------|---------|-------------|\n| `--listen` | `127.0.0.1:9551` | Address and port to expose metrics on |\n| `--metrics-path` | `/metrics` | HTTP path for the metrics endpoint |\n| `--xray-endpoint` | `127.0.0.1:10085` | Xray gRPC API address |\n| `--log-path` | `\"\"` | Path to Xray `access.log` for geo metrics (empty = disabled) |\n| `--error-log-path` | `\"\"` | Path to Xray `error.log` for TSPU detection metrics (empty = disabled) |\n| `--geo-city-db` | `\"\"` | Path to `GeoLite2-City.mmdb` (empty = geo metrics disabled) |\n| `--geo-asn-db` | `\"\"` | Path to `GeoLite2-ASN.mmdb` (empty = ASN label disabled) |\n\n---\n\n## Systemd Service\n\nCreate `/etc/systemd/system/xray-stats-exporter.service`:\n\n```ini\n[Unit]\nDescription=Xray Stats Prometheus Exporter\nAfter=network.target xray.service\n\n[Service]\nUser=nobody\nGroup=nogroup\nExecStart=/usr/local/bin/xray-stats-exporter \\\n  --listen=127.0.0.1:9551 \\\n  --xray-endpoint=127.0.0.1:10085 \\\n  --log-path=/var/log/Xray/access.log \\\n  --error-log-path=/var/log/Xray/error.log \\\n  --geo-city-db=/var/lib/xray-exporter/GeoLite2-City.mmdb \\\n  --geo-asn-db=/var/lib/xray-exporter/GeoLite2-ASN.mmdb\nRestart=on-failure\nRestartSec=5s\nNoNewPrivileges=true\nProtectSystem=strict\nReadOnlyPaths=/var/log/Xray\nReadWritePaths=\n\n[Install]\nWantedBy=multi-user.target\n```\n\n```bash\nsudo systemctl daemon-reload\nsudo systemctl enable --now xray-stats-exporter\nsudo systemctl status xray-stats-exporter\n```\n\n---\n\n## Xray Config Requirements\n\nXray must have stats and API enabled. Minimal required config fragments:\n\n**`010-stats.json`** — enable stats collection:\n\n```json\n{\n  \"stats\": {},\n  \"policy\": {\n    \"levels\": {\n      \"0\": {\n        \"statsUserUplink\": true,\n        \"statsUserDownlink\": true\n      }\n    },\n    \"system\": {\n      \"statsInboundUplink\": true,\n      \"statsInboundDownlink\": true\n    }\n  }\n}\n```\n\n**`050-api.json`** — expose gRPC API on localhost:\n\n```json\n{\n  \"inbounds\": [{\n    \"listen\": \"127.0.0.1\",\n    \"port\": 10085,\n    \"protocol\": \"dokodemo-door\",\n    \"settings\": {\"address\": \"127.0.0.1\"},\n    \"tag\": \"api-inbound\"\n  }],\n  \"api\": {\n    \"tag\": \"api-inbound\",\n    \"services\": [\"StatsService\", \"HandlerService\", \"ReflectionService\"]\n  }\n}\n```\n\n**Users must have an `email` field** for per-user metrics:\n\n```json\n{\n  \"clients\": [\n    {\"id\": \"uuid-here\", \"email\": \"alice@example.com\", \"flow\": \"xtls-rprx-vision\"}\n  ]\n}\n```\n\nWithout the `email` field, the user contributes to inbound totals only — no per-user metrics are emitted.\n\n---\n\n## TSPU / DPI Detection\n\nWhen `--error-log-path` is set, the exporter tails the Xray error log in real time and classifies lines into three counter families:\n\n| Event | Metric | What it indicates |\n|-------|--------|-------------------|\n| `handshake failure` / `tls alert` | `xray_handshake_failure_total` | TSPU injects RST before TLS completes |\n| `connection reset by peer` / `broken pipe` | `xray_connection_reset_total` | TSPU forcibly terminates established connections |\n| `unknown record type` / `i/o timeout` / `context deadline exceeded` | `xray_probe_detected_total` | Active DPI probe or firewall-induced timeout |\n\nEach counter is labelled by `inbound` tag (extracted from `[tag=X]` in the log line, fallback to the proxy path component, or `\"unknown\"`).\n\n**Alerting example** — fire when 10+ connection resets hit any inbound in 5 minutes:\n\n```yaml\ngroups:\n  - name: xray-tspu\n    rules:\n      - alert: XrayTSPUBlock\n        expr: increase(xray_connection_reset_total[5m]) \u003e 10\n        for: 0m\n        labels:\n          severity: warning\n        annotations:\n          summary: \"TSPU block suspected on inbound {{ $labels.inbound }}\"\n```\n\n---\n\n## Throughput Degradation Detection\n\nThe exporter computes a rolling `bytes/sec` rate per inbound on every scrape and compares it against a 10-sample baseline. A degradation event is counted when:\n\n- Current rate \u003c 30% of baseline average (70% drop), **and**\n- Baseline average \u003e 100 KB/s (ignores idle inbounds)\n\nThis detects the DPI throttling pattern where an active inbound suddenly drops traffic (TSPU rate-limits or drops the flow while probing), distinct from natural low-usage periods.\n\n| Parameter | Value | Meaning |\n|-----------|-------|---------|\n| Window size | 10 scrapes | ~2.5 min history at 15s scrape interval |\n| Degradation threshold | 30% of baseline | Triggers on ≥70% traffic drop |\n| Minimum baseline | 100 KB/s | Ignores idle inbounds |\n\nCounter resets (Xray restart) are automatically skipped — negative delta produces no sample.\n\n---\n\n## Geo Metrics\n\nWhen `--log-path` and `--geo-city-db` are set, the exporter tails the Xray access log and resolves source IPs against GeoLite2 databases:\n\n```\n2026/03/28 10:41:22 from 1.2.3.4:56789 accepted tcp:host:443 [vless-reality-in -\u003e direct] email: alice@example.com\n```\n\n**GeoLite2 database setup:**\n\n```bash\n# Register at https://www.maxmind.com/en/geolite2/signup\n# Download from: https://download.maxmind.com/app/geoip_download\nmkdir -p /var/lib/xray-exporter\nmv GeoLite2-City.mmdb /var/lib/xray-exporter/\nmv GeoLite2-ASN.mmdb /var/lib/xray-exporter/\n```\n\nLoopback IPs (`127.0.0.x`) are skipped. Entries without an `email:` field are counted in inbound stats only.\n\n---\n\n## Prometheus Scrape Config\n\n```yaml\nscrape_configs:\n  - job_name: xray-stats\n    scrape_interval: 15s\n    static_configs:\n      - targets: ['127.0.0.1:9551']\n```\n\n---\n\n## Grafana Dashboard\n\nWorks out of the box with [Raven-server-install](https://github.com/AlchemyLink/Raven-server-install) which includes a pre-built Grafana dashboard with:\n\n- Per-user upload/download timeseries\n- Top users by traffic (bar gauge)\n- Per-inbound traffic breakdown\n- TSPU event counters (handshake failures, RST, probes) with threshold alerting\n- Throughput degradation event timeline\n\n---\n\n## Related Projects\n\n- [Raven-server-install](https://github.com/AlchemyLink/Raven-server-install) — Ansible playbooks that deploy this exporter alongside Xray + Raven-subscribe\n- [Raven-subscribe](https://github.com/AlchemyLink/Raven-subscribe) — subscription server for Xray users\n- [Xray-core](https://github.com/XTLS/Xray-core) — the VPN core\n\n---\n\n## License\n\n[MIT](LICENSE) © AlchemyLink\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falchemylink%2Fxray-stats-exporter","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Falchemylink%2Fxray-stats-exporter","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falchemylink%2Fxray-stats-exporter/lists"}