This commit is contained in:
Parham Alvani 2026-07-18 10:07:43 +09:00 committed by GitHub
commit c745f0850b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 170 additions and 1 deletions

View File

@ -1,3 +1,7 @@
!!! quote "Changes in sing-box 1.14.0"
:material-plus: [enable_metrics](#enable_metrics)
!!! quote "Changes in sing-box 1.10.0"
:material-plus: [access_control_allow_origin](#access_control_allow_origin)
@ -25,6 +29,7 @@
"default_mode": "",
"access_control_allow_origin": [],
"access_control_allow_private_network": false,
"enable_metrics": false,
// Deprecated
@ -119,6 +124,27 @@ Allow access from private network.
To access the Clash API on a private network from a public website, `access_control_allow_private_network` must be enabled.
#### enable_metrics
!!! question "Since sing-box 1.14.0"
Enable the Prometheus metrics endpoint.
When enabled, sing-box exposes metrics in the Prometheus text format at
`http://{{external-controller}}/metrics`, sharing the same authentication as the
rest of the Clash API (send `Authorization: Bearer ${secret}` when `secret` is set).
The following metrics are exported, in addition to the standard Go runtime and
process collectors:
| Metric | Type | Labels | Description |
|-------------------------------------------|---------|------------|----------------------------------------------------------|
| `singbox_uplink_bytes_total` | counter | | Total number of bytes uploaded through all connections. |
| `singbox_downlink_bytes_total` | counter | | Total number of bytes downloaded through all connections.|
| `singbox_active_connections` | gauge | | Number of currently active connections. |
| `singbox_active_connections_by_outbound` | gauge | `outbound` | Active connections grouped by outbound tag. |
| `singbox_outbound_delay_milliseconds` | gauge | `outbound` | Latest URL test delay of an outbound, in milliseconds. |
#### store_mode
!!! failure "Deprecated in sing-box 1.8.0"

View File

@ -0,0 +1,109 @@
package clashapi
import (
"net/http"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/trafficcontrol"
"github.com/sagernet/sing-box/common/urltest"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/collectors"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
const metricsNamespace = "singbox"
// metricsCollector implements prometheus.Collector and exposes sing-box traffic,
// connection and outbound health statistics. All values are read live on each
// scrape from the shared traffic manager, outbound manager and URL test history,
// so the collector holds no state of its own and needs no background goroutine.
type metricsCollector struct {
trafficManager *trafficcontrol.Manager
urlTestHistory *urltest.HistoryStorage
outbound adapter.OutboundManager
uplinkBytes *prometheus.Desc
downlinkBytes *prometheus.Desc
activeConnections *prometheus.Desc
connectionsByOutbound *prometheus.Desc
outboundDelay *prometheus.Desc
}
func newMetricsCollector(trafficManager *trafficcontrol.Manager, urlTestHistory *urltest.HistoryStorage, outbound adapter.OutboundManager) *metricsCollector {
return &metricsCollector{
trafficManager: trafficManager,
urlTestHistory: urlTestHistory,
outbound: outbound,
uplinkBytes: prometheus.NewDesc(
prometheus.BuildFQName(metricsNamespace, "", "uplink_bytes_total"),
"Total number of bytes uploaded through all connections.",
nil, nil,
),
downlinkBytes: prometheus.NewDesc(
prometheus.BuildFQName(metricsNamespace, "", "downlink_bytes_total"),
"Total number of bytes downloaded through all connections.",
nil, nil,
),
activeConnections: prometheus.NewDesc(
prometheus.BuildFQName(metricsNamespace, "", "active_connections"),
"Number of currently active connections.",
nil, nil,
),
connectionsByOutbound: prometheus.NewDesc(
prometheus.BuildFQName(metricsNamespace, "", "active_connections_by_outbound"),
"Number of currently active connections grouped by outbound tag.",
[]string{"outbound"}, nil,
),
outboundDelay: prometheus.NewDesc(
prometheus.BuildFQName(metricsNamespace, "", "outbound_delay_milliseconds"),
"Latest URL test delay of an outbound, in milliseconds.",
[]string{"outbound"}, nil,
),
}
}
func (c *metricsCollector) Describe(ch chan<- *prometheus.Desc) {
ch <- c.uplinkBytes
ch <- c.downlinkBytes
ch <- c.activeConnections
ch <- c.connectionsByOutbound
ch <- c.outboundDelay
}
func (c *metricsCollector) Collect(ch chan<- prometheus.Metric) {
uplinkTotal, downlinkTotal := c.trafficManager.Total()
ch <- prometheus.MustNewConstMetric(c.uplinkBytes, prometheus.CounterValue, float64(uplinkTotal))
ch <- prometheus.MustNewConstMetric(c.downlinkBytes, prometheus.CounterValue, float64(downlinkTotal))
ch <- prometheus.MustNewConstMetric(c.activeConnections, prometheus.GaugeValue, float64(c.trafficManager.ConnectionsLen()))
connectionsByOutbound := make(map[string]int)
for _, metadata := range c.trafficManager.Connections() {
connectionsByOutbound[metadata.Outbound]++
}
for outbound, count := range connectionsByOutbound {
ch <- prometheus.MustNewConstMetric(c.connectionsByOutbound, prometheus.GaugeValue, float64(count), outbound)
}
for _, outbound := range c.outbound.Outbounds() {
history := c.urlTestHistory.LoadURLTestHistory(outbound.Tag())
if history == nil {
continue
}
ch <- prometheus.MustNewConstMetric(c.outboundDelay, prometheus.GaugeValue, float64(history.Delay), outbound.Tag())
}
}
// newMetricsHandler builds an HTTP handler serving the Prometheus text format.
// It uses a dedicated registry so sing-box metrics never collide with the
// default global registry, and includes the standard Go runtime and process
// collectors alongside the sing-box collector.
func newMetricsHandler(trafficManager *trafficcontrol.Manager, urlTestHistory *urltest.HistoryStorage, outbound adapter.OutboundManager) http.Handler {
registry := prometheus.NewRegistry()
registry.MustRegister(
collectors.NewGoCollector(),
collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}),
newMetricsCollector(trafficManager, urlTestHistory, outbound),
)
return promhttp.HandlerFor(registry, promhttp.HandlerOpts{})
}

View File

@ -136,6 +136,10 @@ func NewServer(ctx context.Context, logFactory log.ObservableFactory, options op
r.Mount("/cache", cacheRouter(ctx))
r.Mount("/dns", dnsRouter(s.dnsRouter))
if options.EnableMetrics {
r.Handle("/metrics", newMetricsHandler(trafficManager, urlTestHistory, s.outbound))
}
s.setupMetaAPI(r)
})
if options.ExternalUI != "" {

8
go.mod
View File

@ -31,6 +31,7 @@ require (
github.com/openai/openai-go/v3 v3.26.0
github.com/oschwald/maxminddb-golang v1.13.1
github.com/pkg/sftp v1.13.10
github.com/prometheus/client_golang v1.23.2
github.com/sagernet/asc-go v0.0.0-20241217030726-d563060fe4e1
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a
github.com/sagernet/cors v1.2.1
@ -88,7 +89,9 @@ require (
github.com/anchore/go-lzo v0.1.0 // indirect
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 // indirect
github.com/coreos/go-oidc/v3 v3.17.0 // indirect
github.com/database64128/netx-go v0.1.1 // indirect
@ -125,6 +128,7 @@ require (
github.com/libp2p/go-netroute v0.2.1 // indirect
github.com/mdlayher/socket v0.5.1 // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/philhofer/fwd v1.2.0 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/pion/dtls/v3 v3.1.5 // indirect
@ -133,6 +137,9 @@ require (
github.com/pires/go-proxyproto v0.8.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus-community/pro-bing v0.4.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/safchain/ethtool v0.3.0 // indirect
github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260712142643-1e5048bd5587 // indirect
@ -182,6 +189,7 @@ require (
github.com/zeebo/blake3 v0.2.4 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap/exp v0.3.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect
golang.org/x/oauth2 v0.34.0 // indirect
golang.org/x/term v0.40.0 // indirect

23
go.sum
View File

@ -24,6 +24,8 @@ github.com/anthropics/anthropic-sdk-go v1.26.0 h1:oUTzFaUpAevfuELAP1sjL6CQJ9HHAf
github.com/anthropics/anthropic-sdk-go v1.26.0/go.mod h1:qUKmaW+uuPB64iy1l+4kOSvaLqPXnHTTBKH6RVZ7q5Q=
github.com/anytls/sing-anytls v0.0.11 h1:w8e9Uj1oP3m4zxkyZDewPk0EcQbvVxb7Nn+rapEx4fc=
github.com/anytls/sing-anytls v0.0.11/go.mod h1:7rjN6IukwysmdusYsrV51Fgu1uW6vsrdd6ctjnEAln8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/caddyserver/certmagic v0.25.3-0.20260421143802-60d9d8b415d6 h1:LYSB6VgWzKtNrcxElw3c97BP40Oc7bizKxA9K1Vi/5k=
github.com/caddyserver/certmagic v0.25.3-0.20260421143802-60d9d8b415d6/go.mod h1:llW/CvsNmza8S6hmsuggsZeiX+uS27dkqY27wDIuBWg=
github.com/caddyserver/zerossl v0.1.5 h1:dkvOjBAEEtY6LIGAHei7sw2UgqSD6TrWweXpV7lvEvE=
@ -135,6 +137,10 @@ github.com/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0=
github.com/koron/go-ssdp v0.0.4/go.mod h1:oDXq+E5IL5q0U8uSBcoAXzTzInwy5lEgC91HoKtbmZk=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/letsencrypt/challtestsrv v1.4.2 h1:0ON3ldMhZyWlfVNYYpFuWRTmZNnyfiL9Hh5YzC3JVwU=
@ -167,6 +173,8 @@ github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/openai/openai-go/v3 v3.26.0 h1:bRt6H/ozMNt/dDkN4gobnLqaEGrRGBzmbVs0xxJEnQE=
@ -192,8 +200,18 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus-community/pro-bing v0.4.0 h1:YMbv+i08gQz97OZZBwLyvmmQEEzyfyrrjEaAchdy3R4=
github.com/prometheus-community/pro-bing v0.4.0/go.mod h1:b7wRYZtCcPmt4Sz319BykUU241rWLe1VFXyiyWK/dH4=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0=
github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs=
@ -387,6 +405,8 @@ go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek=
go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
@ -511,8 +531,9 @@ google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@ -29,6 +29,7 @@ type ClashAPIOptions struct {
ModeList []string `json:"-"`
AccessControlAllowOrigin badoption.Listable[string] `json:"access_control_allow_origin,omitempty"`
AccessControlAllowPrivateNetwork bool `json:"access_control_allow_private_network,omitempty"`
EnableMetrics bool `json:"enable_metrics,omitempty"`
// Deprecated: migrated to global cache file
CacheFile string `json:"cache_file,omitempty"`