forked from quic-go/quic-go
* add Prometheus metrics for sent long and short header packets * add Prometheus metrics for received long and short header packets * add Grafana panels for sent and received packets
258 lines
7.7 KiB
Go
258 lines
7.7 KiB
Go
package metrics
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"time"
|
|
|
|
"github.com/quic-go/quic-go"
|
|
"github.com/quic-go/quic-go/internal/qerr"
|
|
"github.com/quic-go/quic-go/logging"
|
|
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
)
|
|
|
|
var (
|
|
connStarted = prometheus.NewCounterVec(
|
|
prometheus.CounterOpts{
|
|
Namespace: metricNamespace,
|
|
Name: "connections_started_total",
|
|
Help: "Connections Started",
|
|
},
|
|
[]string{"dir"},
|
|
)
|
|
connClosed = prometheus.NewCounterVec(
|
|
prometheus.CounterOpts{
|
|
Namespace: metricNamespace,
|
|
Name: "connections_closed_total",
|
|
Help: "Connections Closed",
|
|
},
|
|
[]string{"dir", "reason"},
|
|
)
|
|
connDuration = prometheus.NewHistogramVec(
|
|
prometheus.HistogramOpts{
|
|
Namespace: metricNamespace,
|
|
Name: "connection_duration_seconds",
|
|
Help: "Duration of a Connection",
|
|
Buckets: prometheus.ExponentialBuckets(1.0/16, 2, 25), // up to 24 days
|
|
},
|
|
[]string{"dir"},
|
|
)
|
|
connHandshakeDuration = prometheus.NewHistogramVec(
|
|
prometheus.HistogramOpts{
|
|
Namespace: metricNamespace,
|
|
Name: "handshake_duration_seconds",
|
|
Help: "Duration of the QUIC Handshake",
|
|
Buckets: prometheus.ExponentialBuckets(0.001, 1.3, 35),
|
|
},
|
|
[]string{"dir"},
|
|
)
|
|
packetsSent = prometheus.NewCounterVec(
|
|
prometheus.CounterOpts{
|
|
Namespace: metricNamespace,
|
|
Name: "packets_sent_total",
|
|
Help: "Packets Sent",
|
|
},
|
|
[]string{"type"},
|
|
)
|
|
packetsReceived = prometheus.NewCounterVec(
|
|
prometheus.CounterOpts{
|
|
Namespace: metricNamespace,
|
|
Name: "packets_received_total",
|
|
Help: "Packets Received",
|
|
},
|
|
[]string{"type"},
|
|
)
|
|
)
|
|
|
|
// DefaultConnectionTracer returns a callback that creates a metrics ConnectionTracer.
|
|
// The ConnectionTracer returned can be set on the quic.Config for a new connection.
|
|
func DefaultConnectionTracer(_ context.Context, p logging.Perspective, _ logging.ConnectionID) *logging.ConnectionTracer {
|
|
switch p {
|
|
case logging.PerspectiveClient:
|
|
return NewClientConnectionTracerWithRegisterer(prometheus.DefaultRegisterer)
|
|
case logging.PerspectiveServer:
|
|
return NewServerConnectionTracerWithRegisterer(prometheus.DefaultRegisterer)
|
|
default:
|
|
panic("invalid perspective")
|
|
}
|
|
}
|
|
|
|
// NewClientConnectionTracerWithRegisterer creates a new connection tracer for a connection
|
|
// dialed on the client side with a given Prometheus registerer.
|
|
func NewClientConnectionTracerWithRegisterer(registerer prometheus.Registerer) *logging.ConnectionTracer {
|
|
return newConnectionTracerWithRegisterer(registerer, true)
|
|
}
|
|
|
|
// NewServerConnectionTracerWithRegisterer creates a new connection tracer for a connection
|
|
// accepted on the server side with a given Prometheus registerer.
|
|
func NewServerConnectionTracerWithRegisterer(registerer prometheus.Registerer) *logging.ConnectionTracer {
|
|
return newConnectionTracerWithRegisterer(registerer, false)
|
|
}
|
|
|
|
func newConnectionTracerWithRegisterer(registerer prometheus.Registerer, isClient bool) *logging.ConnectionTracer {
|
|
for _, c := range [...]prometheus.Collector{
|
|
connStarted,
|
|
connHandshakeDuration,
|
|
connClosed,
|
|
connDuration,
|
|
packetsSent,
|
|
packetsReceived,
|
|
} {
|
|
if err := registerer.Register(c); err != nil {
|
|
if ok := errors.As(err, &prometheus.AlreadyRegisteredError{}); !ok {
|
|
panic(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
direction := "incoming"
|
|
if isClient {
|
|
direction = "outgoing"
|
|
}
|
|
|
|
var (
|
|
startTime time.Time
|
|
handshakeComplete bool
|
|
)
|
|
return &logging.ConnectionTracer{
|
|
StartedConnection: func(_, _ net.Addr, _, _ logging.ConnectionID) {
|
|
tags := getStringSlice()
|
|
defer putStringSlice(tags)
|
|
|
|
startTime = time.Now()
|
|
|
|
*tags = append(*tags, direction)
|
|
connStarted.WithLabelValues(*tags...).Inc()
|
|
},
|
|
ClosedConnection: func(e error) {
|
|
tags := getStringSlice()
|
|
defer putStringSlice(tags)
|
|
|
|
*tags = append(*tags, direction)
|
|
// call connDuration.Observe before adding any more labels
|
|
if handshakeComplete {
|
|
connDuration.WithLabelValues(*tags...).Observe(time.Since(startTime).Seconds())
|
|
}
|
|
|
|
var (
|
|
statelessResetErr *quic.StatelessResetError
|
|
handshakeTimeoutErr *quic.HandshakeTimeoutError
|
|
idleTimeoutErr *quic.IdleTimeoutError
|
|
applicationErr *quic.ApplicationError
|
|
transportErr *quic.TransportError
|
|
versionNegotiationErr *quic.VersionNegotiationError
|
|
)
|
|
var reason string
|
|
switch {
|
|
case errors.As(e, &statelessResetErr):
|
|
reason = "stateless_reset"
|
|
case errors.As(e, &handshakeTimeoutErr):
|
|
reason = "handshake_timeout"
|
|
case errors.As(e, &idleTimeoutErr):
|
|
if handshakeComplete {
|
|
reason = "idle_timeout"
|
|
} else {
|
|
reason = "handshake_timeout"
|
|
}
|
|
case errors.As(e, &applicationErr):
|
|
if applicationErr.Remote {
|
|
reason = "application_error (remote)"
|
|
} else {
|
|
reason = "application_error (local)"
|
|
}
|
|
case errors.As(e, &transportErr):
|
|
switch {
|
|
case transportErr.ErrorCode == qerr.ApplicationErrorErrorCode:
|
|
if transportErr.Remote {
|
|
reason = "application_error (remote)"
|
|
} else {
|
|
reason = "application_error (local)"
|
|
}
|
|
case transportErr.ErrorCode.IsCryptoError():
|
|
if transportErr.Remote {
|
|
reason = "crypto_error (remote)"
|
|
} else {
|
|
reason = "crypto_error (local)"
|
|
}
|
|
default:
|
|
if transportErr.Remote {
|
|
reason = "transport_error (remote)"
|
|
} else {
|
|
reason = fmt.Sprintf("transport_error (local): %s", transportErr.ErrorCode)
|
|
}
|
|
}
|
|
case errors.As(e, &versionNegotiationErr):
|
|
reason = "version_mismatch"
|
|
default:
|
|
reason = "unknown"
|
|
}
|
|
*tags = append(*tags, reason)
|
|
connClosed.WithLabelValues(*tags...).Inc()
|
|
},
|
|
UpdatedKeyFromTLS: func(l logging.EncryptionLevel, p logging.Perspective) {
|
|
// The client derives both 1-RTT keys when the handshake completes.
|
|
// The server derives the 1-RTT read key when the handshake completes.
|
|
if l != logging.Encryption1RTT || p != logging.PerspectiveClient {
|
|
return
|
|
}
|
|
handshakeComplete = true
|
|
|
|
tags := getStringSlice()
|
|
defer putStringSlice(tags)
|
|
|
|
*tags = append(*tags, direction)
|
|
connHandshakeDuration.WithLabelValues(*tags...).Observe(time.Since(startTime).Seconds())
|
|
},
|
|
SentLongHeaderPacket: func(hdr *logging.ExtendedHeader, _ logging.ByteCount, _ logging.ECN, _ *logging.AckFrame, _ []logging.Frame) {
|
|
tags := getStringSlice()
|
|
defer putStringSlice(tags)
|
|
|
|
*tags = append(*tags, longHeaderType(hdr))
|
|
packetsSent.WithLabelValues(*tags...).Inc()
|
|
},
|
|
SentShortHeaderPacket: func(*logging.ShortHeader, logging.ByteCount, logging.ECN, *logging.AckFrame, []logging.Frame) {
|
|
tags := getStringSlice()
|
|
defer putStringSlice(tags)
|
|
|
|
*tags = append(*tags, "1rtt")
|
|
packetsSent.WithLabelValues(*tags...).Inc()
|
|
},
|
|
ReceivedLongHeaderPacket: func(hdr *logging.ExtendedHeader, _ logging.ByteCount, _ logging.ECN, _ []logging.Frame) {
|
|
tags := getStringSlice()
|
|
defer putStringSlice(tags)
|
|
|
|
*tags = append(*tags, longHeaderType(hdr))
|
|
packetsReceived.WithLabelValues(*tags...).Inc()
|
|
},
|
|
ReceivedShortHeaderPacket: func(*logging.ShortHeader, logging.ByteCount, logging.ECN, []logging.Frame) {
|
|
tags := getStringSlice()
|
|
defer putStringSlice(tags)
|
|
|
|
*tags = append(*tags, "1rtt")
|
|
packetsReceived.WithLabelValues(*tags...).Inc()
|
|
},
|
|
}
|
|
}
|
|
|
|
func longHeaderType(hdr *logging.ExtendedHeader) string {
|
|
//nolint:exhaustive // only these packet types are of interest
|
|
switch logging.PacketTypeFromHeader(&hdr.Header) {
|
|
case logging.PacketTypeRetry:
|
|
return "retry"
|
|
case logging.PacketTypeInitial:
|
|
return "initial"
|
|
case logging.PacketTypeHandshake:
|
|
return "handshake"
|
|
case logging.PacketType0RTT:
|
|
return "0rtt"
|
|
case logging.PacketTypeStatelessReset:
|
|
return "stateless_reset"
|
|
case logging.PacketTypeVersionNegotiation:
|
|
return "version_negotiation"
|
|
}
|
|
return "unknown"
|
|
}
|