// 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() }