ceremonyclient/go-libp2p/p2p/protocol/holepunch/metrics.go
2024-10-12 11:55:17 -07:00

188 lines
5.7 KiB
Go

package holepunch
import (
"github.com/libp2p/go-libp2p/core/network"
"github.com/libp2p/go-libp2p/p2p/metricshelper"
ma "github.com/multiformats/go-multiaddr"
"github.com/prometheus/client_golang/prometheus"
)
const metricNamespace = "libp2p_holepunch"
var (
directDialsTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: metricNamespace,
Name: "direct_dials_total",
Help: "Direct Dials Total",
},
[]string{"outcome"},
)
hpAddressOutcomesTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: metricNamespace,
Name: "address_outcomes_total",
Help: "Hole Punch outcomes by Transport",
},
[]string{"side", "num_attempts", "ipv", "transport", "outcome"},
)
hpOutcomesTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: metricNamespace,
Name: "outcomes_total",
Help: "Hole Punch outcomes overall",
},
[]string{"side", "num_attempts", "outcome"},
)
collectors = []prometheus.Collector{
directDialsTotal,
hpAddressOutcomesTotal,
hpOutcomesTotal,
}
)
type MetricsTracer interface {
HolePunchFinished(side string, attemptNum int, theirAddrs []ma.Multiaddr, ourAddr []ma.Multiaddr, directConn network.ConnMultiaddrs)
DirectDialFinished(success bool)
}
type metricsTracer struct{}
var _ MetricsTracer = &metricsTracer{}
type metricsTracerSetting struct {
reg prometheus.Registerer
}
type MetricsTracerOption func(*metricsTracerSetting)
func WithRegisterer(reg prometheus.Registerer) MetricsTracerOption {
return func(s *metricsTracerSetting) {
if reg != nil {
s.reg = reg
}
}
}
func NewMetricsTracer(opts ...MetricsTracerOption) MetricsTracer {
setting := &metricsTracerSetting{reg: prometheus.DefaultRegisterer}
for _, opt := range opts {
opt(setting)
}
metricshelper.RegisterCollectors(setting.reg, collectors...)
// initialise metrics's labels so that the first data point is handled correctly
for _, side := range []string{"initiator", "receiver"} {
for _, numAttempts := range []string{"1", "2", "3", "4"} {
for _, outcome := range []string{"success", "failed", "cancelled", "no_suitable_address"} {
for _, ipv := range []string{"ip4", "ip6"} {
for _, transport := range []string{"quic", "quic-v1", "tcp", "webtransport"} {
hpAddressOutcomesTotal.WithLabelValues(side, numAttempts, ipv, transport, outcome)
}
}
if outcome == "cancelled" {
// not a valid outcome for the overall holepunch metric
continue
}
hpOutcomesTotal.WithLabelValues(side, numAttempts, outcome)
}
}
}
return &metricsTracer{}
}
// HolePunchFinished tracks metrics completion of a holepunch. Metrics are tracked on
// a holepunch attempt level and on individual addresses involved in a holepunch.
//
// outcome for an address is computed as:
//
// - success:
// A direct connection was established with the peer using this address
// - cancelled:
// A direct connection was established with the peer but not using this address
// - failed:
// No direct connection was made to the peer and the peer reported an address
// with the same transport as this address
// - no_suitable_address:
// The peer reported no address with the same transport as this address
func (mt *metricsTracer) HolePunchFinished(side string, numAttempts int,
remoteAddrs []ma.Multiaddr, localAddrs []ma.Multiaddr, directConn network.ConnMultiaddrs) {
tags := metricshelper.GetStringSlice()
*tags = append(*tags, side, getNumAttemptString(numAttempts))
var dipv, dtransport string
if directConn != nil {
dipv, _ = metricshelper.GetIPVersion(directConn.LocalMultiaddr())
dtransport = metricshelper.GetTransport(directConn.LocalMultiaddr())
}
matchingAddressCount := 0
// calculate holepunch outcome for all the addresses involved
for _, la := range localAddrs {
lipv, _ := metricshelper.GetIPVersion(la)
ltransport := metricshelper.GetTransport(la)
matchingAddress := false
for _, ra := range remoteAddrs {
ripv, _ := metricshelper.GetIPVersion(ra)
rtransport := metricshelper.GetTransport(ra)
if ripv == lipv && rtransport == ltransport {
// the peer reported an address with the same transport
matchingAddress = true
matchingAddressCount++
*tags = append(*tags, ripv, rtransport)
if directConn != nil && dipv == ripv && dtransport == rtransport {
// the connection was made using this address
*tags = append(*tags, "success")
} else if directConn != nil {
// connection was made but not using this address
*tags = append(*tags, "cancelled")
} else {
// no connection was made
*tags = append(*tags, "failed")
}
hpAddressOutcomesTotal.WithLabelValues(*tags...).Inc()
*tags = (*tags)[:2] // 2 because we want to keep (side, numAttempts)
break
}
}
if !matchingAddress {
*tags = append(*tags, lipv, ltransport, "no_suitable_address")
hpAddressOutcomesTotal.WithLabelValues(*tags...).Inc()
*tags = (*tags)[:2] // 2 because we want to keep (side, numAttempts)
}
}
outcome := "failed"
if directConn != nil {
outcome = "success"
} else if matchingAddressCount == 0 {
// there were no matching addresses, this attempt was going to fail
outcome = "no_suitable_address"
}
*tags = append(*tags, outcome)
hpOutcomesTotal.WithLabelValues(*tags...).Inc()
metricshelper.PutStringSlice(tags)
}
func getNumAttemptString(numAttempt int) string {
var attemptStr = [...]string{"0", "1", "2", "3", "4", "5"}
if numAttempt > 5 {
return "> 5"
}
return attemptStr[numAttempt]
}
func (mt *metricsTracer) DirectDialFinished(success bool) {
tags := metricshelper.GetStringSlice()
if success {
*tags = append(*tags, "success")
} else {
*tags = append(*tags, "failed")
}
directDialsTotal.WithLabelValues(*tags...).Inc()
metricshelper.PutStringSlice(tags)
}