package metrics import ( "log/slog" "net" "sync" "time" "golang.getoutline.org/tunnel-server/ipinfo" outline_prometheus "golang.getoutline.org/tunnel-server/prometheus" "golang.getoutline.org/tunnel-server/service" svcmetrics "golang.getoutline.org/tunnel-server/service/metrics" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/collectors" ) // ServerMetrics tracks server-level Prometheus metrics. type ServerMetrics struct { buildInfo *prometheus.GaugeVec accessKeys prometheus.Gauge ports prometheus.Gauge addedNatEntries prometheus.Counter removedNatEntries prometheus.Counter } var _ prometheus.Collector = (*ServerMetrics)(nil) var _ service.NATMetrics = (*ServerMetrics)(nil) func NewServerMetrics() *ServerMetrics { return &ServerMetrics{ buildInfo: prometheus.NewGaugeVec(prometheus.GaugeOpts{ Name: "build_info", Help: "Information on the shroud build", }, []string{"version"}), accessKeys: prometheus.NewGauge(prometheus.GaugeOpts{ Name: "keys", Help: "Count of access keys", }), ports: prometheus.NewGauge(prometheus.GaugeOpts{ Name: "ports", Help: "Count of open ports", }), addedNatEntries: prometheus.NewCounter(prometheus.CounterOpts{ Subsystem: "udp", Name: "nat_entries_added", Help: "Entries added to the UDP NAT table", }), removedNatEntries: prometheus.NewCounter(prometheus.CounterOpts{ Subsystem: "udp", Name: "nat_entries_removed", Help: "Entries removed from the UDP NAT table", }), } } func (m *ServerMetrics) Describe(ch chan<- *prometheus.Desc) { m.buildInfo.Describe(ch) m.accessKeys.Describe(ch) m.ports.Describe(ch) m.addedNatEntries.Describe(ch) m.removedNatEntries.Describe(ch) } func (m *ServerMetrics) Collect(ch chan<- prometheus.Metric) { m.buildInfo.Collect(ch) m.accessKeys.Collect(ch) m.ports.Collect(ch) m.addedNatEntries.Collect(ch) m.removedNatEntries.Collect(ch) } func (m *ServerMetrics) SetVersion(version string) { m.buildInfo.WithLabelValues(version).Set(1) } func (m *ServerMetrics) SetNumAccessKeys(numKeys int, ports int) { m.accessKeys.Set(float64(numKeys)) m.ports.Set(float64(ports)) } func (m *ServerMetrics) AddNATEntry() { m.addedNatEntries.Inc() } func (m *ServerMetrics) RemoveNATEntry() { m.removedNatEntries.Inc() } // TransferTracker wraps ServiceMetrics to also track per-key byte transfer // for the REST API's /metrics/transfer endpoint. type TransferTracker struct { service.ServiceMetrics mu sync.Mutex transfer map[string]int64 // key ID -> cumulative outbound bytes } func NewTransferTracker(inner service.ServiceMetrics) *TransferTracker { return &TransferTracker{ ServiceMetrics: inner, transfer: make(map[string]int64), } } func (t *TransferTracker) AddOpenTCPConnection(conn net.Conn) service.TCPConnMetrics { inner := t.ServiceMetrics.AddOpenTCPConnection(conn) return &trackingTCPConnMetrics{TCPConnMetrics: inner, tracker: t} } // GetTransferByKey returns cumulative byte transfer per key and resets counters. func (t *TransferTracker) GetTransferByKey() map[string]int64 { t.mu.Lock() defer t.mu.Unlock() result := t.transfer t.transfer = make(map[string]int64) return result } func (t *TransferTracker) addBytes(keyID string, bytes int64) { t.mu.Lock() defer t.mu.Unlock() t.transfer[keyID] += bytes } type trackingTCPConnMetrics struct { service.TCPConnMetrics tracker *TransferTracker accessKey string } func (m *trackingTCPConnMetrics) AddAuthentication(accessKey string) { m.accessKey = accessKey m.TCPConnMetrics.AddAuthentication(accessKey) } func (m *trackingTCPConnMetrics) AddClose(status string, data svcmetrics.ProxyMetrics, duration time.Duration) { if m.accessKey != "" { m.tracker.addBytes(m.accessKey, data.ProxyTarget+data.TargetProxy) } m.TCPConnMetrics.AddClose(status, data, duration) } // SetupRegistry creates a Prometheus registry with all metric collectors. func SetupRegistry( ip2info ipinfo.IPInfoMap, serverMetrics *ServerMetrics, version string, nodeCollectors []string, logger *slog.Logger, ) (*prometheus.Registry, *TransferTracker, *ServerMetrics, error) { registry := prometheus.NewRegistry() // Go runtime metrics. registry.MustRegister(collectors.NewGoCollector()) registry.MustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{})) // Shadowsocks service metrics. serviceMetrics, err := outline_prometheus.NewServiceMetrics(ip2info) if err != nil { return nil, nil, nil, err } serverMetrics.SetVersion(version) r := prometheus.WrapRegistererWithPrefix("shadowsocks_", registry) r.MustRegister(serverMetrics, serviceMetrics) transferTracker := NewTransferTracker(serviceMetrics) // Node exporter collectors. if len(nodeCollectors) > 0 { if err := registerNodeCollectors(registry, logger, nodeCollectors); err != nil { logger.Warn("Failed to register node_exporter collectors, continuing without them.", "err", err) } } return registry, transferTracker, serverMetrics, nil }