Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add TunnelTime metric #171

Merged
merged 43 commits into from
Mar 27, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
70f1f3e
Fix log message.
sbruens Feb 1, 2024
46dcef4
Fix Prometheus instructions.
sbruens Feb 28, 2024
50a39bf
Add new proposed metrics.
sbruens Feb 28, 2024
8efddb8
Pass the IP to `Add(Open|Closed)TCPConnection` instead of the clientI…
sbruens Feb 29, 2024
2e1c4d9
Add `ipinfo` helper function to get IP info using a `net.IP`.
sbruens Mar 14, 2024
54b18c5
Add an `activeIPKeyTracker` struct and use it to measure `IPKeyTime` …
sbruens Mar 14, 2024
8f200a5
Fix casing of `GetIpInfoFromIP`.
sbruens Mar 14, 2024
db1be10
Fix existing TCP and UDP tests.
sbruens Mar 14, 2024
e8d0fad
Fix more tests.
sbruens Mar 14, 2024
00c1045
Merge remote-tracking branch 'origin' into sbruens/ip-key-metrics
sbruens Mar 15, 2024
7f3d9d0
Exercise each scenario in its own test.
sbruens Mar 15, 2024
fb7bc2e
Add tests for metrics.
sbruens Mar 15, 2024
db15a2f
Use in `IPInfo` map to test `shadowsocks_ip_key_connectivity_seconds_…
sbruens Mar 15, 2024
f49c40d
Merge branch 'master' into sbruens/ip-key-metrics
sbruens Mar 15, 2024
0520fd2
Remove `key_connectivity_seconds` for the time being.
sbruens Mar 15, 2024
423565f
Merge branch 'master' into sbruens/ip-key-metrics
sbruens Mar 16, 2024
6613942
Fix bad merge.
sbruens Mar 16, 2024
f5e3cf5
Put the `IPKey` metrics behind a flag for now.
sbruens Mar 18, 2024
959de84
Revert "Put the `IPKey` metrics behind a flag for now."
sbruens Mar 20, 2024
5c75727
Address review comments.
sbruens Mar 20, 2024
4d0a20a
Remove `ipinfo.IPInfo` from `RemoveUDPNatEntry`, as it doesn't need it.
sbruens Mar 20, 2024
73fb6ab
Undo changes to ipinfo.
sbruens Mar 20, 2024
2e982f2
Undo changes to main.
sbruens Mar 20, 2024
09fd19e
Make `setNow` private.
sbruens Mar 20, 2024
25c4393
Make `activeClients` a map of pointers.
sbruens Mar 21, 2024
1816156
Add locks to the active clients to avoid concurrency issues.
sbruens Mar 21, 2024
a3d7a57
Add a lock to the tracker.
sbruens Mar 21, 2024
b14464d
Use `netip.Addr` as IP type in the `IPKey` struct.
sbruens Mar 21, 2024
2d1252b
Replace `MustParseAddr` with a new `toIPAddr` helper function.
sbruens Mar 21, 2024
e60e53a
Remove the `clientInfo` param from `startConnection`.
sbruens Mar 22, 2024
cfa143d
Make `startConnection` and `stopConnection` use `IPKey` as param.
sbruens Mar 22, 2024
c56bcfd
Consolidate `toIPAddr()` and `toIPKey()`.
sbruens Mar 22, 2024
bfd381c
Add comment to mutex.
sbruens Mar 22, 2024
55ba178
Remove lock from `activeClient` struct.
sbruens Mar 22, 2024
e428f8d
Make stubbable `time.Now` private.
sbruens Mar 22, 2024
a5731fc
Pass around the `IPKey` instead of storing it on the `activeClient`.
sbruens Mar 22, 2024
4112238
Initialize TunnelTime for new `IPKey`s with default value of 0.
sbruens Mar 22, 2024
7cb66b3
Refactor to implement a custom `Collector` to track TunnelTime.
sbruens Mar 22, 2024
ced36bd
Update comments and log.
sbruens Mar 22, 2024
0ee630d
Handle error case from `SplitHostPort()`.
sbruens Mar 25, 2024
3960510
Address review comments.
sbruens Mar 25, 2024
3eb0114
Refactor the tunnel time collector to use 2 `CounterVec`s underneath …
sbruens Mar 25, 2024
714252f
Apply suggestions from code review
sbruens Mar 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,7 @@ To fetch and update MMDB files from [DB-IP](https://db-ip.com), you can do somet

## Full Working Example: Try It!

Fetch dependencies for this demo:
```
GO111MODULE=off go get github.com/prometheus/prometheus/cmd/...
```
If that doesn't work, download the [prometheus](https://prometheus.io/download/) binary directly.
Download the [Prometheus](https://prometheus.io/download/) binary.


### Run the server
Expand All @@ -60,7 +56,7 @@ In production, you may want to specify `-ip_country_db` to get per-country metri
### Run the Prometheus scraper for metrics collection
On Terminal 2, start prometheus scraper for metrics collection:
```
$(go env GOPATH)/bin/prometheus --config.file=cmd/outline-ss-server/prometheus_example.yml
prometheus --config.file=cmd/outline-ss-server/prometheus_example.yml
```

### Run the SOCKS-to-Shadowsocks client
Expand Down
22 changes: 12 additions & 10 deletions cmd/outline-ss-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,21 +227,23 @@ func readConfig(filename string) (*Config, error) {

func main() {
var flags struct {
ConfigFile string
MetricsAddr string
IPCountryDB string
IPASNDB string
natTimeout time.Duration
replayHistory int
Verbose bool
Version bool
ConfigFile string
MetricsAddr string
IPCountryDB string
IPASNDB string
natTimeout time.Duration
replayHistory int
EnableIPKeyConnectivityMetrics bool
Verbose bool
Version bool
}
flag.StringVar(&flags.ConfigFile, "config", "", "Configuration filename")
flag.StringVar(&flags.MetricsAddr, "metrics", "", "Address for the Prometheus metrics")
flag.StringVar(&flags.IPCountryDB, "ip_country_db", "", "Path to the ip-to-country mmdb file")
flag.StringVar(&flags.IPASNDB, "ip_asn_db", "", "Path to the ip-to-ASN mmdb file")
flag.DurationVar(&flags.natTimeout, "udptimeout", defaultNatTimeout, "UDP tunnel timeout")
flag.IntVar(&flags.replayHistory, "replay_history", 0, "Replay buffer size (# of handshakes)")
flag.BoolVar(&flags.EnableIPKeyConnectivityMetrics, "enable_ip_key_connectivity_metrics", false, "Enables the collection and reporting of IPKey connectivity metrics")
sbruens marked this conversation as resolved.
Show resolved Hide resolved
sbruens marked this conversation as resolved.
Show resolved Hide resolved
flag.BoolVar(&flags.Verbose, "verbose", false, "Enables verbose logging output")
flag.BoolVar(&flags.Version, "version", false, "The version of the server")

Expand Down Expand Up @@ -280,11 +282,11 @@ func main() {
}
ip2info, err := ipinfo.NewMMDBIPInfoMap(flags.IPCountryDB, flags.IPASNDB)
if err != nil {
logger.Fatalf("Could create IP info map: %v. Aborting", err)
logger.Fatalf("Failed to create IP info map: %v. Aborting", err)
}
defer ip2info.Close()

m := newPrometheusOutlineMetrics(ip2info, prometheus.DefaultRegisterer)
m := newPrometheusOutlineMetrics(ip2info, prometheus.DefaultRegisterer, flags.EnableIPKeyConnectivityMetrics)
m.SetBuildInfo(version)
_, err = RunSSServer(flags.ConfigFile, flags.natTimeout, m, flags.replayHistory)
if err != nil {
Expand Down
171 changes: 163 additions & 8 deletions cmd/outline-ss-server/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package main

import (
"fmt"
"net"
"strconv"
"time"

Expand All @@ -25,8 +26,16 @@ import (
"github.com/prometheus/client_golang/prometheus"
)

const (
// How often to report the active IP key TunnelTime.
activeIPKeyTrackerReportingInterval = 5 * time.Second
)
sbruens marked this conversation as resolved.
Show resolved Hide resolved

var since = time.Since
sbruens marked this conversation as resolved.
Show resolved Hide resolved

type outlineMetrics struct {
ipinfo.IPInfoMap
*activeIPKeyTracker

buildInfo *prometheus.GaugeVec
accessKeys prometheus.Gauge
Expand All @@ -36,6 +45,9 @@ type outlineMetrics struct {
timeToCipherMs *prometheus.HistogramVec
// TODO: Add time to first byte.

IPKeyTimePerKey *prometheus.CounterVec
IPKeyTimePerLocation *prometheus.CounterVec

tcpProbes *prometheus.HistogramVec
tcpOpenConnections *prometheus.CounterVec
tcpClosedConnections *prometheus.CounterVec
Expand All @@ -49,11 +61,100 @@ type outlineMetrics struct {
var _ service.TCPMetrics = (*outlineMetrics)(nil)
var _ service.UDPMetrics = (*outlineMetrics)(nil)

type activeClient struct {
sbruens marked this conversation as resolved.
Show resolved Hide resolved
IPKey IPKey
sbruens marked this conversation as resolved.
Show resolved Hide resolved
connectionCount int
startTime time.Time
}

func (c *activeClient) IsActive() bool {
sbruens marked this conversation as resolved.
Show resolved Hide resolved
return c.connectionCount > 0
}

type IPKey struct {
ip string
sbruens marked this conversation as resolved.
Show resolved Hide resolved
accessKey string
}

type activeIPKeyTracker struct {
activeClients map[IPKey]activeClient
metricsCallback func(IPKey, time.Duration)
sbruens marked this conversation as resolved.
Show resolved Hide resolved
}

// Reports time connected for all active clients, called at a regular interval.
func (t *activeIPKeyTracker) reportAll() {
sbruens marked this conversation as resolved.
Show resolved Hide resolved
if len(t.activeClients) == 0 {
logger.Debugf("No active clients. No IPKey activity to report.")
return
}
for _, c := range t.activeClients {
sbruens marked this conversation as resolved.
Show resolved Hide resolved
t.reportDuration(c)
}
}

// Reports time connected for a given active client.
func (t *activeIPKeyTracker) reportDuration(c activeClient) {
sbruens marked this conversation as resolved.
Show resolved Hide resolved
connDuration := since(c.startTime)
logger.Debugf("Reporting activity for key `%v`, duration: %v", c.IPKey.accessKey, connDuration)
t.metricsCallback(c.IPKey, connDuration)

// Reset the start time now that it's been reported.
c.startTime = time.Now()
t.activeClients[c.IPKey] = c
sbruens marked this conversation as resolved.
Show resolved Hide resolved
}

// Registers a new active connection for a client [net.Addr] and access key.
func (t *activeIPKeyTracker) startConnection(addr net.Addr, accessKey string) {
sbruens marked this conversation as resolved.
Show resolved Hide resolved
hostname, _, _ := net.SplitHostPort(addr.String())
ipKey := IPKey{ip: hostname, accessKey: accessKey}

c, exists := t.activeClients[ipKey]
if !exists {
c = activeClient{ipKey, 0, time.Now()}
}
c.connectionCount++
t.activeClients[ipKey] = c
}

// Removes an active connection for a client [net.Addr] and access key.
func (t *activeIPKeyTracker) stopConnection(addr net.Addr, accessKey string) {
sbruens marked this conversation as resolved.
Show resolved Hide resolved
hostname, _, _ := net.SplitHostPort(addr.String())
ipKey := IPKey{ip: hostname, accessKey: accessKey}

c := t.activeClients[ipKey]
sbruens marked this conversation as resolved.
Show resolved Hide resolved
c.connectionCount--
sbruens marked this conversation as resolved.
Show resolved Hide resolved
if !c.IsActive() {
t.reportDuration(c)
delete(t.activeClients, ipKey)
return
}
t.activeClients[ipKey] = c
}

func newActiveIPKeyTracker(callback func(IPKey, time.Duration)) *activeIPKeyTracker {
t := &activeIPKeyTracker{activeClients: make(map[IPKey]activeClient), metricsCallback: callback}
ticker := time.NewTicker(activeIPKeyTrackerReportingInterval)
done := make(chan struct{})
go func() {
sbruens marked this conversation as resolved.
Show resolved Hide resolved
for {
select {
case <-ticker.C:
sbruens marked this conversation as resolved.
Show resolved Hide resolved
t.reportAll()
case <-done:
sbruens marked this conversation as resolved.
Show resolved Hide resolved
logger.Debugf("done channel %p closed", done)
ticker.Stop()
return
}
}
}()
return t
}

// newPrometheusOutlineMetrics constructs a metrics object that uses
// `ipCountryDB` to convert IP addresses to countries, and reports all
// metrics to Prometheus via `registerer`. `ipCountryDB` may be nil, but
// `ip2info` to convert IP addresses to countries, and reports all
// metrics to Prometheus via `registerer`. `ip2info` may be nil, but
// `registerer` must not be.
func newPrometheusOutlineMetrics(ip2info ipinfo.IPInfoMap, registerer prometheus.Registerer) *outlineMetrics {
func newPrometheusOutlineMetrics(ip2info ipinfo.IPInfoMap, registerer prometheus.Registerer, enableIPKeyConnectivity bool) *outlineMetrics {
sbruens marked this conversation as resolved.
Show resolved Hide resolved
m := &outlineMetrics{
IPInfoMap: ip2info,
buildInfo: prometheus.NewGaugeVec(prometheus.GaugeOpts{
Expand Down Expand Up @@ -104,6 +205,16 @@ func newPrometheusOutlineMetrics(ip2info ipinfo.IPInfoMap, registerer prometheus
float64(7 * 24 * time.Hour.Milliseconds()), // Week
},
}, []string{"status"}),
IPKeyTimePerKey: prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: "shadowsocks",
Name: "ip_key_connectivity_seconds",
Help: "Time at least 1 connection was open for a (IP, access key) pair, per key",
}, []string{"access_key"}),
IPKeyTimePerLocation: prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: "shadowsocks",
Name: "ip_key_connectivity_seconds_per_location",
Help: "Time at least 1 connection was open for a (IP, access key) pair, per location",
}, []string{"location", "asn"}),
dataBytes: prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: "shadowsocks",
Expand Down Expand Up @@ -145,10 +256,15 @@ func newPrometheusOutlineMetrics(ip2info ipinfo.IPInfoMap, registerer prometheus
Help: "Entries removed from the UDP NAT table",
}),
}
if enableIPKeyConnectivity {
m.activeIPKeyTracker = newActiveIPKeyTracker(m.reportIPKeyActivity)
}
logger.Debugf("tracker: %v", m.activeIPKeyTracker)
sbruens marked this conversation as resolved.
Show resolved Hide resolved

// TODO: Is it possible to pass where to register the collectors?
registerer.MustRegister(m.buildInfo, m.accessKeys, m.ports, m.tcpProbes, m.tcpOpenConnections, m.tcpClosedConnections, m.tcpConnectionDurationMs,
sbruens marked this conversation as resolved.
Show resolved Hide resolved
m.dataBytes, m.dataBytesPerLocation, m.timeToCipherMs, m.udpPacketsFromClientPerLocation, m.udpAddedNatEntries, m.udpRemovedNatEntries)
m.dataBytes, m.dataBytesPerLocation, m.timeToCipherMs, m.udpPacketsFromClientPerLocation, m.udpAddedNatEntries, m.udpRemovedNatEntries,
m.IPKeyTimePerKey, m.IPKeyTimePerLocation)
return m
}

Expand All @@ -161,10 +277,32 @@ func (m *outlineMetrics) SetNumAccessKeys(numKeys int, ports int) {
m.ports.Set(float64(ports))
}

func (m *outlineMetrics) AddOpenTCPConnection(clientInfo ipinfo.IPInfo) {
func (m *outlineMetrics) AddOpenTCPConnection(addr net.Addr) {
clientInfo, err := ipinfo.GetIPInfoFromAddr(m.IPInfoMap, addr)
sbruens marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
logger.Warningf("Failed client info lookup: %v", err)
}
logger.Debugf("Got info \"%#v\" for IP %v", clientInfo, addr.String())
sbruens marked this conversation as resolved.
Show resolved Hide resolved
m.tcpOpenConnections.WithLabelValues(clientInfo.CountryCode.String(), asnLabel(clientInfo.ASN)).Inc()
}

// Reports total time connected, by access key and by country.
func (m *outlineMetrics) reportIPKeyActivity(ipKey IPKey, duration time.Duration) {
sbruens marked this conversation as resolved.
Show resolved Hide resolved
m.IPKeyTimePerKey.WithLabelValues(ipKey.accessKey).Add(duration.Seconds())
ip := net.ParseIP(ipKey.ip)
clientInfo, err := ipinfo.GetIPInfoFromIP(m.IPInfoMap, ip)
sbruens marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
logger.Warningf("Failed client info lookup: %v", err)
}
m.IPKeyTimePerLocation.WithLabelValues(clientInfo.CountryCode.String(), asnLabel(clientInfo.ASN)).Add(duration.Seconds())
}

func (m *outlineMetrics) AddAuthenticatedTCPConnection(addr net.Addr, accessKey string) {
if m.activeIPKeyTracker != nil {
m.activeIPKeyTracker.startConnection(addr, accessKey)
}
}

// addIfNonZero helps avoid the creation of series that are always zero.
func addIfNonZero(value int64, counterVec *prometheus.CounterVec, lvs ...string) {
if value > 0 {
Expand All @@ -179,7 +317,12 @@ func asnLabel(asn int) string {
return fmt.Sprint(asn)
}

func (m *outlineMetrics) AddClosedTCPConnection(clientInfo ipinfo.IPInfo, accessKey, status string, data metrics.ProxyMetrics, duration time.Duration) {
func (m *outlineMetrics) AddClosedTCPConnection(addr net.Addr, accessKey, status string, data metrics.ProxyMetrics, duration time.Duration) {
clientInfo, err := ipinfo.GetIPInfoFromAddr(m.IPInfoMap, addr)
sbruens marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
logger.Warningf("Failed client info lookup: %v", err)
}
logger.Debugf("Got info \"%#v\" for IP %v", clientInfo, addr.String())
m.tcpClosedConnections.WithLabelValues(clientInfo.CountryCode.String(), asnLabel(clientInfo.ASN), status, accessKey).Inc()
m.tcpConnectionDurationMs.WithLabelValues(status).Observe(duration.Seconds() * 1000)
addIfNonZero(data.ClientProxy, m.dataBytes, "c>p", "tcp", accessKey)
Expand All @@ -190,6 +333,10 @@ func (m *outlineMetrics) AddClosedTCPConnection(clientInfo ipinfo.IPInfo, access
addIfNonZero(data.TargetProxy, m.dataBytesPerLocation, "p<t", "tcp", clientInfo.CountryCode.String(), asnLabel(clientInfo.ASN))
addIfNonZero(data.ProxyClient, m.dataBytes, "c<p", "tcp", accessKey)
addIfNonZero(data.ProxyClient, m.dataBytesPerLocation, "c<p", "tcp", clientInfo.CountryCode.String(), asnLabel(clientInfo.ASN))

if m.activeIPKeyTracker != nil {
m.activeIPKeyTracker.stopConnection(addr, accessKey)
}
}

func (m *outlineMetrics) AddUDPPacketFromClient(clientInfo ipinfo.IPInfo, accessKey, status string, clientProxyBytes, proxyTargetBytes int) {
Expand All @@ -207,12 +354,20 @@ func (m *outlineMetrics) AddUDPPacketFromTarget(clientInfo ipinfo.IPInfo, access
addIfNonZero(int64(proxyClientBytes), m.dataBytesPerLocation, "c<p", "udp", clientInfo.CountryCode.String(), asnLabel(clientInfo.ASN))
}

func (m *outlineMetrics) AddUDPNatEntry() {
func (m *outlineMetrics) AddUDPNatEntry(addr net.Addr, accessKey string) {
m.udpAddedNatEntries.Inc()

if m.activeIPKeyTracker != nil {
m.activeIPKeyTracker.startConnection(addr, accessKey)
}
}

func (m *outlineMetrics) RemoveUDPNatEntry() {
func (m *outlineMetrics) RemoveUDPNatEntry(addr net.Addr, accessKey string) {
m.udpRemovedNatEntries.Inc()

if m.activeIPKeyTracker != nil {
m.activeIPKeyTracker.stopConnection(addr, accessKey)
}
}

func (m *outlineMetrics) AddTCPProbe(status, drainResult string, port int, clientProxyBytes int64) {
Expand Down
Loading
Loading