Skip to content

Commit

Permalink
refactor: split metrics collection into TCP and UDP
Browse files Browse the repository at this point in the history
  • Loading branch information
fortuna authored Jun 5, 2023
1 parent e1eedc0 commit 09833ae
Show file tree
Hide file tree
Showing 10 changed files with 335 additions and 316 deletions.
7 changes: 3 additions & 4 deletions cmd/outline-ss-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import (
"github.com/Jigsaw-Code/outline-internal-sdk/transport/shadowsocks"
"github.com/Jigsaw-Code/outline-ss-server/ipinfo"
"github.com/Jigsaw-Code/outline-ss-server/service"
"github.com/Jigsaw-Code/outline-ss-server/service/metrics"
"github.com/op/go-logging"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
Expand Down Expand Up @@ -68,7 +67,7 @@ type ssPort struct {

type SSServer struct {
natTimeout time.Duration
m metrics.ShadowsocksMetrics
m *outlineMetrics
replayCache service.ReplayCache
ports map[int]*ssPort
}
Expand Down Expand Up @@ -179,7 +178,7 @@ func (s *SSServer) Stop() error {
}

// RunSSServer starts a shadowsocks server running, and returns the server or an error.
func RunSSServer(filename string, natTimeout time.Duration, sm metrics.ShadowsocksMetrics, replayHistory int) (*SSServer, error) {
func RunSSServer(filename string, natTimeout time.Duration, sm *outlineMetrics, replayHistory int) (*SSServer, error) {
server := &SSServer{
natTimeout: natTimeout,
m: sm,
Expand Down Expand Up @@ -280,7 +279,7 @@ func main() {
defer ip2info.Close()
}

m := metrics.NewPrometheusShadowsocksMetrics(ip2info, prometheus.DefaultRegisterer)
m := newPrometheusOutlineMetrics(ip2info, prometheus.DefaultRegisterer)
m.SetBuildInfo(version)
_, err = RunSSServer(flags.ConfigFile, flags.natTimeout, m, flags.replayHistory)
if err != nil {
Expand Down
228 changes: 228 additions & 0 deletions cmd/outline-ss-server/metrics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
// Copyright 2023 Jigsaw Operations LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
"strconv"
"time"

"github.com/Jigsaw-Code/outline-ss-server/ipinfo"
"github.com/Jigsaw-Code/outline-ss-server/service"
"github.com/Jigsaw-Code/outline-ss-server/service/metrics"
"github.com/prometheus/client_golang/prometheus"
)

type outlineMetrics struct {
ipinfo.IPInfoMap

buildInfo *prometheus.GaugeVec
accessKeys prometheus.Gauge
ports prometheus.Gauge
dataBytes *prometheus.CounterVec
dataBytesPerLocation *prometheus.CounterVec
timeToCipherMs *prometheus.HistogramVec
// TODO: Add time to first byte.

tcpProbes *prometheus.HistogramVec
tcpOpenConnections *prometheus.CounterVec
tcpClosedConnections *prometheus.CounterVec
tcpConnectionDurationMs *prometheus.HistogramVec

udpPacketsFromClientPerLocation *prometheus.CounterVec
udpAddedNatEntries prometheus.Counter
udpRemovedNatEntries prometheus.Counter
}

var _ service.TCPMetrics = (*outlineMetrics)(nil)
var _ service.UDPMetrics = (*outlineMetrics)(nil)

// 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
// `registerer` must not be.
func newPrometheusOutlineMetrics(ip2info ipinfo.IPInfoMap, registerer prometheus.Registerer) *outlineMetrics {
m := &outlineMetrics{
IPInfoMap: ip2info,
buildInfo: prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: "shadowsocks",
Name: "build_info",
Help: "Information on the outline-ss-server build",
}, []string{"version"}),
accessKeys: prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: "shadowsocks",
Name: "keys",
Help: "Count of access keys",
}),
ports: prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: "shadowsocks",
Name: "ports",
Help: "Count of open Shadowsocks ports",
}),
tcpProbes: prometheus.NewHistogramVec(prometheus.HistogramOpts{
Namespace: "shadowsocks",
Name: "tcp_probes",
Buckets: []float64{0, 49, 50, 51, 73, 91},
Help: "Histogram of number of bytes from client to proxy, for detecting possible probes",
}, []string{"port", "status", "error"}),
tcpOpenConnections: prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: "shadowsocks",
Subsystem: "tcp",
Name: "connections_opened",
Help: "Count of open TCP connections",
}, []string{"location"}),
tcpClosedConnections: prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: "shadowsocks",
Subsystem: "tcp",
Name: "connections_closed",
Help: "Count of closed TCP connections",
}, []string{"location", "status", "access_key"}),
tcpConnectionDurationMs: prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Namespace: "shadowsocks",
Subsystem: "tcp",
Name: "connection_duration_ms",
Help: "TCP connection duration distributions.",
Buckets: []float64{
100,
float64(time.Second.Milliseconds()),
float64(time.Minute.Milliseconds()),
float64(time.Hour.Milliseconds()),
float64(24 * time.Hour.Milliseconds()), // Day
float64(7 * 24 * time.Hour.Milliseconds()), // Week
},
}, []string{"status"}),
dataBytes: prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: "shadowsocks",
Name: "data_bytes",
Help: "Bytes transferred by the proxy, per access key",
}, []string{"dir", "proto", "access_key"}),
dataBytesPerLocation: prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: "shadowsocks",
Name: "data_bytes_per_location",
Help: "Bytes transferred by the proxy, per location",
}, []string{"dir", "proto", "location"}),
timeToCipherMs: prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Namespace: "shadowsocks",
Name: "time_to_cipher_ms",
Help: "Time needed to find the cipher",
Buckets: []float64{0.1, 1, 10, 100, 1000},
}, []string{"proto", "found_key"}),
udpPacketsFromClientPerLocation: prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: "shadowsocks",
Subsystem: "udp",
Name: "packets_from_client_per_location",
Help: "Packets received from the client, per location and status",
}, []string{"location", "status"}),
udpAddedNatEntries: prometheus.NewCounter(
prometheus.CounterOpts{
Namespace: "shadowsocks",
Subsystem: "udp",
Name: "nat_entries_added",
Help: "Entries added to the UDP NAT table",
}),
udpRemovedNatEntries: prometheus.NewCounter(
prometheus.CounterOpts{
Namespace: "shadowsocks",
Subsystem: "udp",
Name: "nat_entries_removed",
Help: "Entries removed from the UDP NAT table",
}),
}

// 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,
m.dataBytes, m.dataBytesPerLocation, m.timeToCipherMs, m.udpPacketsFromClientPerLocation, m.udpAddedNatEntries, m.udpRemovedNatEntries)
return m
}

func (m *outlineMetrics) SetBuildInfo(version string) {
m.buildInfo.WithLabelValues(version).Set(1)
}

func (m *outlineMetrics) SetNumAccessKeys(numKeys int, ports int) {
m.accessKeys.Set(float64(numKeys))
m.ports.Set(float64(ports))
}

func (m *outlineMetrics) AddOpenTCPConnection(clientInfo ipinfo.IPInfo) {
m.tcpOpenConnections.WithLabelValues(clientInfo.CountryCode.String()).Inc()
}

// addIfNonZero helps avoid the creation of series that are always zero.
func addIfNonZero(value int64, counterVec *prometheus.CounterVec, lvs ...string) {
if value > 0 {
counterVec.WithLabelValues(lvs...).Add(float64(value))
}
}

func (m *outlineMetrics) AddClosedTCPConnection(clientInfo ipinfo.IPInfo, accessKey, status string, data metrics.ProxyMetrics, duration time.Duration) {
m.tcpClosedConnections.WithLabelValues(clientInfo.CountryCode.String(), status, accessKey).Inc()
m.tcpConnectionDurationMs.WithLabelValues(status).Observe(duration.Seconds() * 1000)
addIfNonZero(data.ClientProxy, m.dataBytes, "c>p", "tcp", accessKey)
addIfNonZero(data.ClientProxy, m.dataBytesPerLocation, "c>p", "tcp", clientInfo.CountryCode.String())
addIfNonZero(data.ProxyTarget, m.dataBytes, "p>t", "tcp", accessKey)
addIfNonZero(data.ProxyTarget, m.dataBytesPerLocation, "p>t", "tcp", clientInfo.CountryCode.String())
addIfNonZero(data.TargetProxy, m.dataBytes, "p<t", "tcp", accessKey)
addIfNonZero(data.TargetProxy, m.dataBytesPerLocation, "p<t", "tcp", clientInfo.CountryCode.String())
addIfNonZero(data.ProxyClient, m.dataBytes, "c<p", "tcp", accessKey)
addIfNonZero(data.ProxyClient, m.dataBytesPerLocation, "c<p", "tcp", clientInfo.CountryCode.String())
}

func (m *outlineMetrics) AddUDPPacketFromClient(clientInfo ipinfo.IPInfo, accessKey, status string, clientProxyBytes, proxyTargetBytes int) {
m.udpPacketsFromClientPerLocation.WithLabelValues(clientInfo.CountryCode.String(), status).Inc()
addIfNonZero(int64(clientProxyBytes), m.dataBytes, "c>p", "udp", accessKey)
addIfNonZero(int64(clientProxyBytes), m.dataBytesPerLocation, "c>p", "udp", clientInfo.CountryCode.String())
addIfNonZero(int64(proxyTargetBytes), m.dataBytes, "p>t", "udp", accessKey)
addIfNonZero(int64(proxyTargetBytes), m.dataBytesPerLocation, "p>t", "udp", clientInfo.CountryCode.String())
}

func (m *outlineMetrics) AddUDPPacketFromTarget(clientInfo ipinfo.IPInfo, accessKey, status string, targetProxyBytes, proxyClientBytes int) {
addIfNonZero(int64(targetProxyBytes), m.dataBytes, "p<t", "udp", accessKey)
addIfNonZero(int64(targetProxyBytes), m.dataBytesPerLocation, "p<t", "udp", clientInfo.CountryCode.String())
addIfNonZero(int64(proxyClientBytes), m.dataBytes, "c<p", "udp", accessKey)
addIfNonZero(int64(proxyClientBytes), m.dataBytesPerLocation, "c<p", "udp", clientInfo.CountryCode.String())
}

func (m *outlineMetrics) AddUDPNatEntry() {
m.udpAddedNatEntries.Inc()
}

func (m *outlineMetrics) RemoveUDPNatEntry() {
m.udpRemovedNatEntries.Inc()
}

func (m *outlineMetrics) AddTCPProbe(status, drainResult string, port int, clientProxyBytes int64) {
m.tcpProbes.WithLabelValues(strconv.Itoa(port), status, drainResult).Observe(float64(clientProxyBytes))
}

func (m *outlineMetrics) AddTCPCipherSearch(accessKeyFound bool, timeToCipher time.Duration) {
foundStr := "false"
if accessKeyFound {
foundStr = "true"
}
m.timeToCipherMs.WithLabelValues("tcp", foundStr).Observe(timeToCipher.Seconds() * 1000)
}

func (m *outlineMetrics) AddUDPCipherSearch(accessKeyFound bool, timeToCipher time.Duration) {
foundStr := "false"
if accessKeyFound {
foundStr = "true"
}
m.timeToCipherMs.WithLabelValues("udp", foundStr).Observe(timeToCipher.Seconds() * 1000)
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,31 @@
package metrics
// Copyright 2023 Jigsaw Operations LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
"testing"
"time"

"github.com/Jigsaw-Code/outline-ss-server/ipinfo"
"github.com/Jigsaw-Code/outline-ss-server/service/metrics"
"github.com/prometheus/client_golang/prometheus"
)

func TestMethodsDontPanic(t *testing.T) {
ssMetrics := NewPrometheusShadowsocksMetrics(nil, prometheus.NewPedanticRegistry())
proxyMetrics := ProxyMetrics{
ssMetrics := newPrometheusOutlineMetrics(nil, prometheus.NewPedanticRegistry())
proxyMetrics := metrics.ProxyMetrics{
ClientProxy: 1,
ProxyTarget: 2,
TargetProxy: 3,
Expand All @@ -30,19 +45,19 @@ func TestMethodsDontPanic(t *testing.T) {
}

func BenchmarkOpenTCP(b *testing.B) {
ssMetrics := NewPrometheusShadowsocksMetrics(nil, prometheus.NewRegistry())
ssMetrics := newPrometheusOutlineMetrics(nil, prometheus.NewRegistry())
b.ResetTimer()
for i := 0; i < b.N; i++ {
ssMetrics.AddOpenTCPConnection(ipinfo.IPInfo{CountryCode: "ZZ"})
}
}

func BenchmarkCloseTCP(b *testing.B) {
ssMetrics := NewPrometheusShadowsocksMetrics(nil, prometheus.NewRegistry())
ssMetrics := newPrometheusOutlineMetrics(nil, prometheus.NewRegistry())
clientInfo := ipinfo.IPInfo{CountryCode: "ZZ"}
accessKey := "key 1"
status := "OK"
data := ProxyMetrics{}
data := metrics.ProxyMetrics{}
timeToCipher := time.Microsecond
duration := time.Minute
b.ResetTimer()
Expand All @@ -53,19 +68,19 @@ func BenchmarkCloseTCP(b *testing.B) {
}

func BenchmarkProbe(b *testing.B) {
ssMetrics := NewPrometheusShadowsocksMetrics(nil, prometheus.NewRegistry())
ssMetrics := newPrometheusOutlineMetrics(nil, prometheus.NewRegistry())
status := "ERR_REPLAY"
drainResult := "other"
port := 12345
data := ProxyMetrics{}
data := metrics.ProxyMetrics{}
b.ResetTimer()
for i := 0; i < b.N; i++ {
ssMetrics.AddTCPProbe(status, drainResult, port, data.ClientProxy)
}
}

func BenchmarkClientUDP(b *testing.B) {
ssMetrics := NewPrometheusShadowsocksMetrics(nil, prometheus.NewRegistry())
ssMetrics := newPrometheusOutlineMetrics(nil, prometheus.NewRegistry())
clientInfo := ipinfo.IPInfo{CountryCode: "ZZ"}
accessKey := "key 1"
status := "OK"
Expand All @@ -79,7 +94,7 @@ func BenchmarkClientUDP(b *testing.B) {
}

func BenchmarkTargetUDP(b *testing.B) {
ssMetrics := NewPrometheusShadowsocksMetrics(nil, prometheus.NewRegistry())
ssMetrics := newPrometheusOutlineMetrics(nil, prometheus.NewRegistry())
clientInfo := ipinfo.IPInfo{CountryCode: "ZZ"}
accessKey := "key 1"
status := "OK"
Expand All @@ -91,7 +106,7 @@ func BenchmarkTargetUDP(b *testing.B) {
}

func BenchmarkNAT(b *testing.B) {
ssMetrics := NewPrometheusShadowsocksMetrics(nil, prometheus.NewRegistry())
ssMetrics := newPrometheusOutlineMetrics(nil, prometheus.NewRegistry())
b.ResetTimer()
for i := 0; i < b.N; i++ {
ssMetrics.AddUDPNatEntry()
Expand Down
3 changes: 1 addition & 2 deletions cmd/outline-ss-server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,11 @@ import (
"testing"
"time"

"github.com/Jigsaw-Code/outline-ss-server/service/metrics"
"github.com/prometheus/client_golang/prometheus"
)

func TestRunSSServer(t *testing.T) {
m := metrics.NewPrometheusShadowsocksMetrics(nil, prometheus.DefaultRegisterer)
m := newPrometheusOutlineMetrics(nil, prometheus.DefaultRegisterer)
server, err := RunSSServer("config_example.yml", 30*time.Second, m, 10000)
if err != nil {
t.Fatalf("RunSSServer() error = %v", err)
Expand Down
Loading

0 comments on commit 09833ae

Please sign in to comment.