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:
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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))
|
||||
|
||||
+24
-1
@@ -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()
|
||||
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) {
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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{
|
||||
|
||||
Reference in New Issue
Block a user