From 55a246869eea3b8da458f4c42627a9c3768d93bc Mon Sep 17 00:00:00 2001 From: ginuerzh Date: Sun, 21 Jun 2026 21:55:27 +0800 Subject: [PATCH] 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 --- go.mod | 1 + go.sum | 2 + handler/metrics/handler.go | 2 +- metrics/metrics.go | 27 ++++++- metrics/process_other.go | 154 ++++++++++++++++++++++++++++++++++++ metrics/process_standard.go | 14 ++++ metrics/prom.go | 15 ++-- metrics/service/service.go | 12 ++- 8 files changed, 218 insertions(+), 9 deletions(-) create mode 100644 metrics/process_other.go create mode 100644 metrics/process_standard.go diff --git a/go.mod b/go.mod index b456c48a..054ee151 100644 --- a/go.mod +++ b/go.mod @@ -60,6 +60,7 @@ require ( require ( github.com/andybalholm/brotli v1.0.6 // 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 ) diff --git a/go.sum b/go.sum index c5caa741..45c6a358 100644 --- a/go.sum +++ b/go.sum @@ -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/shadowsocks/go-shadowsocks2 v0.1.5 h1:PDSQv9y2S85Fl7VBeOMF9StzeXZyK1HakRm86CUbr28= 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.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= diff --git a/handler/metrics/handler.go b/handler/metrics/handler.go index 793951e9..13e6f010 100644 --- a/handler/metrics/handler.go +++ b/handler/metrics/handler.go @@ -43,7 +43,7 @@ func (h *metricsHandler) Init(md md.Metadata) (err error) { xmetrics.Enable(true) - h.handler = promhttp.Handler() + h.handler = promhttp.HandlerFor(xmetrics.Registry(), promhttp.HandlerOpts{}) mux := http.NewServeMux() mux.Handle(h.md.path, http.HandlerFunc(h.handleFunc)) diff --git a/metrics/metrics.go b/metrics/metrics.go index 1f3b802d..bef0b39a 100644 --- a/metrics/metrics.go +++ b/metrics/metrics.go @@ -4,6 +4,8 @@ import ( "sync/atomic" "github.com/go-gost/core/metrics" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/collectors" ) const ( @@ -30,10 +32,31 @@ const ( ) var ( - defaultMetrics metrics.Metrics = NewMetrics() - enabled atomic.Bool + defaultMetrics metrics.Metrics + defaultRegistry *prometheus.Registry + 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 // GetCounter, GetGauge, and GetObserver calls return noop implementations. func Enable(b bool) { diff --git a/metrics/process_other.go b/metrics/process_other.go new file mode 100644 index 00000000..347438fe --- /dev/null +++ b/metrics/process_other.go @@ -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()) +} diff --git a/metrics/process_standard.go b/metrics/process_standard.go new file mode 100644 index 00000000..09ab886c --- /dev/null +++ b/metrics/process_standard.go @@ -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{})) +} diff --git a/metrics/prom.go b/metrics/prom.go index 44e5d149..f30fbf6c 100644 --- a/metrics/prom.go +++ b/metrics/prom.go @@ -16,8 +16,13 @@ type promMetrics struct { } // NewMetrics returns a Prometheus-based Metrics implementation. All metrics are -// automatically registered with the default Prometheus registry. -func NewMetrics() metrics.Metrics { +// registered with the given registry. Use nil to register with the default +// Prometheus registry. +func NewMetrics(reg *prometheus.Registry) metrics.Metrics { + if reg == nil { + reg = prometheus.DefaultRegisterer.(*prometheus.Registry) + } + host, _ := os.Hostname() m := &promMetrics{ host: host, @@ -95,13 +100,13 @@ func NewMetrics() metrics.Metrics { }, } for k := range m.gauges { - prometheus.MustRegister(m.gauges[k]) + reg.MustRegister(m.gauges[k]) } for k := range m.counters { - prometheus.MustRegister(m.counters[k]) + reg.MustRegister(m.counters[k]) } for k := range m.histograms { - prometheus.MustRegister(m.histograms[k]) + reg.MustRegister(m.histograms[k]) } return m diff --git a/metrics/service/service.go b/metrics/service/service.go index 72dc1d52..e964172f 100644 --- a/metrics/service/service.go +++ b/metrics/service/service.go @@ -7,7 +7,10 @@ import ( "github.com/go-gost/core/auth" "github.com/go-gost/core/service" + "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" + + xmetrics "github.com/go-gost/x/metrics" ) const ( @@ -46,6 +49,8 @@ type metricService struct { } // 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) { if network == "" { network = "tcp" @@ -82,7 +87,12 @@ func NewService(network, addr string, opts ...Option) (service.Service, error) { 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{ s: &http.Server{