diff --git a/metrics/connection_tracer.go b/metrics/connection_tracer.go new file mode 100644 index 00000000..3620740c --- /dev/null +++ b/metrics/connection_tracer.go @@ -0,0 +1,114 @@ +package metrics + +import ( + "context" + "errors" + "net" + "time" + + "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"}, + ) + 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"}, + ) +) + +// DefaultTracer returns a callback that creates a metrics ConnectionTracer. +// The ConnectionTracer returned can be set on the quic.Config for a new connection. +// It should be reused across QUIC connections. +func DefaultTracer() func(_ context.Context, p logging.Perspective, _ logging.ConnectionID) *logging.ConnectionTracer { + return DefaultTracerWithRegisterer(prometheus.DefaultRegisterer) +} + +// DefaultTracerWithRegisterer returns a callback that creates a metrics ConnectionTracer +// using a given Prometheus registerer. +func DefaultTracerWithRegisterer(registerer prometheus.Registerer) func(_ context.Context, p logging.Perspective, _ logging.ConnectionID) *logging.ConnectionTracer { + return func(_ context.Context, p logging.Perspective, _ logging.ConnectionID) *logging.ConnectionTracer { + switch p { + case logging.PerspectiveClient: + return NewClientConnectionTracerWithRegisterer(registerer) + case logging.PerspectiveServer: + return NewServerConnectionTracerWithRegisterer(registerer) + 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, + connClosed, + connDuration, + } { + 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 + 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(_ error) { + tags := getStringSlice() + defer putStringSlice(tags) + + *tags = append(*tags, direction) + connDuration.WithLabelValues(*tags...).Observe(time.Since(startTime).Seconds()) + connClosed.WithLabelValues(*tags...).Inc() + }, + } +} diff --git a/metrics/dashboards/README.md b/metrics/dashboards/README.md index 9d1a68d1..f7620421 100644 --- a/metrics/dashboards/README.md +++ b/metrics/dashboards/README.md @@ -19,6 +19,18 @@ quic.Transport{ When using multiple `Transport`s, it is recommended to use the metrics tracer struct for all of them. + +Set a metrics connection tracer on the `Config`: +```go +tracer := metrics.DefaultTracer() +quic.Config{ + Tracer: tracer, +} +``` + +It is recommended to use the same connection tracer returned by `DefaultTracer` on the `Config`s for all connections. + + Running: ```shell docker-compose up diff --git a/metrics/tracer.go b/metrics/tracer.go index b2246033..41245bbd 100644 --- a/metrics/tracer.go +++ b/metrics/tracer.go @@ -83,6 +83,11 @@ func NewTracerWithRegisterer(registerer prometheus.Registerer) *logging.Tracer { break } } + // This should never happen. We only send Initials before creating the connection in order to + // reject a connection attempt. + if ccf == nil { + return + } if ccf.IsApplicationError { //nolint:exhaustive // Only a few error codes applicable. switch qerr.TransportErrorCode(ccf.ErrorCode) {