fix(metrics): add cross-platform process metrics collector for non-Linux systems

The standard Prometheus ProcessCollector uses procfs (Linux-only /proc),
so process_resident_memory_bytes and process_virtual_memory_bytes were
silently absent on FreeBSD, Darwin, and other non-Linux platforms.

- Switch to a custom prometheus.Registry to isolate GOST metrics from
  the default registry auto-registrations
- Add gopsutil/v3-based process collector (process_other.go) for
  platforms without procfs (!linux && !windows)
- Keep standard ProcessCollector (process_standard.go) on Linux/Windows
- Update metrics service and handler/metrics to serve from
  promhttp.HandlerFor(customRegistry) instead of promhttp.Handler()
- Add Registry() accessor for the custom registry

Fixes go-gost/gost#509
This commit is contained in:
ginuerzh
2026-06-21 21:55:27 +08:00
parent 3ec3362ac4
commit 55a246869e
8 changed files with 218 additions and 9 deletions
+1
View File
@@ -60,6 +60,7 @@ require (
require ( require (
github.com/andybalholm/brotli v1.0.6 // indirect github.com/andybalholm/brotli v1.0.6 // indirect
github.com/klauspost/compress v1.17.4 // indirect github.com/klauspost/compress v1.17.4 // indirect
github.com/shirou/gopsutil/v3 v3.24.5 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect github.com/sirupsen/logrus v1.9.3 // indirect
) )
+2
View File
@@ -206,6 +206,8 @@ github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6g
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/shadowsocks/go-shadowsocks2 v0.1.5 h1:PDSQv9y2S85Fl7VBeOMF9StzeXZyK1HakRm86CUbr28= github.com/shadowsocks/go-shadowsocks2 v0.1.5 h1:PDSQv9y2S85Fl7VBeOMF9StzeXZyK1HakRm86CUbr28=
github.com/shadowsocks/go-shadowsocks2 v0.1.5/go.mod h1:AGGpIoek4HRno4xzyFiAtLHkOpcoznZEkAccaI/rplM= github.com/shadowsocks/go-shadowsocks2 v0.1.5/go.mod h1:AGGpIoek4HRno4xzyFiAtLHkOpcoznZEkAccaI/rplM=
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+1 -1
View File
@@ -43,7 +43,7 @@ func (h *metricsHandler) Init(md md.Metadata) (err error) {
xmetrics.Enable(true) xmetrics.Enable(true)
h.handler = promhttp.Handler() h.handler = promhttp.HandlerFor(xmetrics.Registry(), promhttp.HandlerOpts{})
mux := http.NewServeMux() mux := http.NewServeMux()
mux.Handle(h.md.path, http.HandlerFunc(h.handleFunc)) mux.Handle(h.md.path, http.HandlerFunc(h.handleFunc))
+24 -1
View File
@@ -4,6 +4,8 @@ import (
"sync/atomic" "sync/atomic"
"github.com/go-gost/core/metrics" "github.com/go-gost/core/metrics"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/collectors"
) )
const ( const (
@@ -30,10 +32,31 @@ const (
) )
var ( var (
defaultMetrics metrics.Metrics = NewMetrics() defaultMetrics metrics.Metrics
defaultRegistry *prometheus.Registry
enabled atomic.Bool enabled atomic.Bool
) )
func init() {
defaultRegistry = prometheus.NewRegistry()
// Register Go and process collectors on our custom registry so the metrics
// endpoint is self-contained. The process collector is platform-specific:
// Linux/Windows use the standard procfs/Win32-based collector; all other
// platforms (FreeBSD, Darwin, etc.) use a gopsutil-based collector.
defaultRegistry.MustRegister(collectors.NewGoCollector())
registerProcessCollector(defaultRegistry)
defaultMetrics = NewMetrics(defaultRegistry)
}
// Registry returns the custom Prometheus registry that holds all GOST metrics
// plus the Go and process collectors. The metrics HTTP endpoint should serve
// from this registry rather than prometheus.DefaultGatherer.
func Registry() *prometheus.Registry {
return defaultRegistry
}
// Enable enables or disables metrics collection globally. When disabled, all // Enable enables or disables metrics collection globally. When disabled, all
// GetCounter, GetGauge, and GetObserver calls return noop implementations. // GetCounter, GetGauge, and GetObserver calls return noop implementations.
func Enable(b bool) { func Enable(b bool) {
+154
View File
@@ -0,0 +1,154 @@
//go:build !linux && !windows
package metrics
import (
"os"
"time"
"github.com/prometheus/client_golang/prometheus"
psutil "github.com/shirou/gopsutil/v3/process"
)
// gopsutilProcessCollector implements prometheus.Collector using gopsutil/v3
// for cross-platform process metrics on non-Linux, non-Windows systems
// (FreeBSD, Darwin, etc.) where procfs is unavailable.
type gopsutilProcessCollector struct {
pidFn func() (int, error)
cpuTotal *prometheus.Desc
openFDs *prometheus.Desc
maxFDs *prometheus.Desc
vsize *prometheus.Desc
maxVsize *prometheus.Desc
rss *prometheus.Desc
startTime *prometheus.Desc
reportErrors bool
}
func getPID() (int, error) { return os.Getpid(), nil }
func newGopsutilProcessCollector() prometheus.Collector {
return &gopsutilProcessCollector{
pidFn: getPID,
cpuTotal: prometheus.NewDesc(
"process_cpu_seconds_total",
"Total user and system CPU time spent in seconds.",
nil, nil,
),
openFDs: prometheus.NewDesc(
"process_open_fds",
"Number of open file descriptors.",
nil, nil,
),
maxFDs: prometheus.NewDesc(
"process_max_fds",
"Maximum number of open file descriptors.",
nil, nil,
),
vsize: prometheus.NewDesc(
"process_virtual_memory_bytes",
"Virtual memory size in bytes.",
nil, nil,
),
maxVsize: prometheus.NewDesc(
"process_virtual_memory_max_bytes",
"Maximum amount of virtual memory available in bytes.",
nil, nil,
),
rss: prometheus.NewDesc(
"process_resident_memory_bytes",
"Resident memory size in bytes.",
nil, nil,
),
startTime: prometheus.NewDesc(
"process_start_time_seconds",
"Start time of the process since unix epoch in seconds.",
nil, nil,
),
reportErrors: false,
}
}
func (c *gopsutilProcessCollector) Describe(ch chan<- *prometheus.Desc) {
ch <- c.cpuTotal
ch <- c.openFDs
ch <- c.maxFDs
ch <- c.vsize
ch <- c.maxVsize
ch <- c.rss
ch <- c.startTime
}
func (c *gopsutilProcessCollector) Collect(ch chan<- prometheus.Metric) {
pid, err := c.pidFn()
if err != nil {
c.reportError(ch, nil, err)
return
}
p, err := psutil.NewProcess(int32(pid))
if err != nil {
c.reportError(ch, nil, err)
return
}
// Memory metrics: RSS (resident) and VMS (virtual).
if mem, err := p.MemoryInfo(); err == nil {
ch <- prometheus.MustNewConstMetric(c.rss, prometheus.GaugeValue, float64(mem.RSS))
ch <- prometheus.MustNewConstMetric(c.vsize, prometheus.GaugeValue, float64(mem.VMS))
} else {
c.reportError(ch, c.rss, err)
}
// CPU time.
if times, err := p.Times(); err == nil {
cpuSeconds := times.User + times.System
ch <- prometheus.MustNewConstMetric(c.cpuTotal, prometheus.CounterValue, cpuSeconds)
} else {
c.reportError(ch, c.cpuTotal, err)
}
// Open file descriptors.
if fds, err := p.NumFDs(); err == nil {
ch <- prometheus.MustNewConstMetric(c.openFDs, prometheus.GaugeValue, float64(fds))
} else {
c.reportError(ch, c.openFDs, err)
}
// Max file descriptors — not reliably available via gopsutil on all platforms.
// Try RLIMIT_NOFILE via gopsutil's RlimitUsage.
if rlimits, err := p.RlimitUsage(false); err == nil && len(rlimits) > 0 {
for _, rl := range rlimits {
switch rl.Resource {
case psutil.RLIMIT_NOFILE:
ch <- prometheus.MustNewConstMetric(c.maxFDs, prometheus.GaugeValue, float64(rl.Hard))
case psutil.RLIMIT_AS:
ch <- prometheus.MustNewConstMetric(c.maxVsize, prometheus.GaugeValue, float64(rl.Hard))
}
}
}
// Process start time.
if ct, err := p.CreateTime(); err == nil {
createTime := time.Unix(ct/1000, (ct%1000)*1e6)
ch <- prometheus.MustNewConstMetric(c.startTime, prometheus.GaugeValue, float64(createTime.Unix()))
} else {
c.reportError(ch, c.startTime, err)
}
}
func (c *gopsutilProcessCollector) reportError(ch chan<- prometheus.Metric, desc *prometheus.Desc, err error) {
if !c.reportErrors {
return
}
if desc == nil {
desc = prometheus.NewInvalidDesc(err)
}
ch <- prometheus.NewInvalidMetric(desc, err)
}
// registerProcessCollector registers a gopsutil-based process metrics collector
// for platforms where procfs is unavailable.
func registerProcessCollector(reg *prometheus.Registry) {
reg.MustRegister(newGopsutilProcessCollector())
}
+14
View File
@@ -0,0 +1,14 @@
//go:build linux || windows
package metrics
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/collectors"
)
// registerProcessCollector registers the standard prometheus ProcessCollector
// which uses procfs on Linux and Win32 syscalls on Windows.
func registerProcessCollector(reg *prometheus.Registry) {
reg.MustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}))
}
+10 -5
View File
@@ -16,8 +16,13 @@ type promMetrics struct {
} }
// NewMetrics returns a Prometheus-based Metrics implementation. All metrics are // NewMetrics returns a Prometheus-based Metrics implementation. All metrics are
// automatically registered with the default Prometheus registry. // registered with the given registry. Use nil to register with the default
func NewMetrics() metrics.Metrics { // Prometheus registry.
func NewMetrics(reg *prometheus.Registry) metrics.Metrics {
if reg == nil {
reg = prometheus.DefaultRegisterer.(*prometheus.Registry)
}
host, _ := os.Hostname() host, _ := os.Hostname()
m := &promMetrics{ m := &promMetrics{
host: host, host: host,
@@ -95,13 +100,13 @@ func NewMetrics() metrics.Metrics {
}, },
} }
for k := range m.gauges { for k := range m.gauges {
prometheus.MustRegister(m.gauges[k]) reg.MustRegister(m.gauges[k])
} }
for k := range m.counters { for k := range m.counters {
prometheus.MustRegister(m.counters[k]) reg.MustRegister(m.counters[k])
} }
for k := range m.histograms { for k := range m.histograms {
prometheus.MustRegister(m.histograms[k]) reg.MustRegister(m.histograms[k])
} }
return m return m
+11 -1
View File
@@ -7,7 +7,10 @@ import (
"github.com/go-gost/core/auth" "github.com/go-gost/core/auth"
"github.com/go-gost/core/service" "github.com/go-gost/core/service"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/client_golang/prometheus/promhttp"
xmetrics "github.com/go-gost/x/metrics"
) )
const ( const (
@@ -46,6 +49,8 @@ type metricService struct {
} }
// NewService creates a metrics Service that exposes Prometheus metrics over HTTP. // NewService creates a metrics Service that exposes Prometheus metrics over HTTP.
// It serves from the GOST custom registry rather than the default one so that
// process metrics are available on all platforms.
func NewService(network, addr string, opts ...Option) (service.Service, error) { func NewService(network, addr string, opts ...Option) (service.Service, error) {
if network == "" { if network == "" {
network = "tcp" network = "tcp"
@@ -82,7 +87,12 @@ func NewService(network, addr string, opts ...Option) (service.Service, error) {
return return
} }
} }
promhttp.Handler().ServeHTTP(w, r)
reg := xmetrics.Registry()
if reg == nil {
reg = prometheus.DefaultRegisterer.(*prometheus.Registry)
}
promhttp.HandlerFor(reg, promhttp.HandlerOpts{}).ServeHTTP(w, r)
})) }))
return &metricService{ return &metricService{
s: &http.Server{ s: &http.Server{