2022-12-08 13:05:19 -03:00

125 lines
3.6 KiB
Go

// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
// Licensed under the GNU Affero General Public License (AGPL).
// See License.AGPL.txt in the project root for license information.
package server
import (
"context"
"fmt"
"strings"
"time"
"github.com/bufbuild/connect-go"
"github.com/prometheus/client_golang/prometheus"
)
type ConnectMetrics struct {
ServerRequestsStarted *prometheus.CounterVec
ServerRequestsHandled *prometheus.HistogramVec
ClientRequestsStarted *prometheus.CounterVec
ClientRequestsHandled *prometheus.HistogramVec
}
func (m *ConnectMetrics) Register(registry *prometheus.Registry) error {
metrics := []prometheus.Collector{
m.ServerRequestsStarted,
m.ServerRequestsHandled,
m.ClientRequestsStarted,
m.ClientRequestsHandled,
}
for _, metric := range metrics {
err := registry.Register(metric)
if err != nil {
return fmt.Errorf("failed to register metric: %w", err)
}
}
return nil
}
func NewConnectMetrics() *ConnectMetrics {
return &ConnectMetrics{
ServerRequestsStarted: prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "connect_server_started_total",
Help: "Counter of server connect (gRPC/HTTP) requests started",
}, []string{"package", "call", "call_type"}),
ServerRequestsHandled: prometheus.NewHistogramVec(prometheus.HistogramOpts{
Name: "connect_server_handled_seconds",
Help: "Histogram of server connect (gRPC/HTTP) requests completed",
}, []string{"package", "call", "call_type", "code"}),
ClientRequestsStarted: prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "connect_client_started_total",
Help: "Counter of client connect (gRPC/HTTP) requests started",
}, []string{"package", "call", "call_type"}),
ClientRequestsHandled: prometheus.NewHistogramVec(prometheus.HistogramOpts{
Name: "connect_client_handled_seconds",
Help: "Histogram of client connect (gRPC/HTTP) requests completed",
}, []string{"package", "call", "call_type", "code"}),
}
}
func NewMetricsInterceptor(metrics *ConnectMetrics) connect.UnaryInterceptorFunc {
interceptor := func(next connect.UnaryFunc) connect.UnaryFunc {
return connect.UnaryFunc(func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
now := time.Now()
callPackage, callName := splitServiceCall(req.Spec().Procedure)
callType := streamType(req.Spec().StreamType)
isClient := req.Spec().IsClient
if isClient {
metrics.ClientRequestsStarted.WithLabelValues(callPackage, callName, callType).Inc()
} else {
metrics.ServerRequestsStarted.WithLabelValues(callPackage, callName, callType).Inc()
}
resp, err := next(ctx, req)
code := codeOf(err)
if isClient {
metrics.ClientRequestsHandled.WithLabelValues(callPackage, callName, callType, code).Observe(time.Since(now).Seconds())
} else {
metrics.ServerRequestsHandled.WithLabelValues(callPackage, callName, callType, code).Observe(time.Since(now).Seconds())
}
return resp, err
})
}
return connect.UnaryInterceptorFunc(interceptor)
}
func splitServiceCall(procedure string) (string, string) {
procedure = strings.TrimPrefix(procedure, "/") // remove leading slash
if i := strings.Index(procedure, "/"); i >= 0 {
return procedure[:i], procedure[i+1:]
}
return "unknown", "unknown"
}
func streamType(st connect.StreamType) string {
switch st {
case connect.StreamTypeUnary:
return "unary"
case connect.StreamTypeClient:
return "client_stream"
case connect.StreamTypeServer:
return "server_stream"
case connect.StreamTypeBidi:
return "bidi"
default:
return "unknown"
}
}
func codeOf(err error) string {
if err == nil {
return "ok"
}
return connect.CodeOf(err).String()
}