Skip to content

Commit 4c3ab5b

Browse files
committed
Add initial set of metrics for BBR
1 parent a70d66e commit 4c3ab5b

File tree

4 files changed

+198
-0
lines changed

4 files changed

+198
-0
lines changed

Diff for: cmd/body-based-routing/main.go

+70
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,26 @@ package main
1818

1919
import (
2020
"flag"
21+
"net"
22+
"net/http"
2123
"os"
24+
"strconv"
2225

2326
"github.com/go-logr/logr"
27+
"github.com/prometheus/client_golang/prometheus/promhttp"
2428
uberzap "go.uber.org/zap"
2529
"go.uber.org/zap/zapcore"
2630
"google.golang.org/grpc"
2731
healthPb "google.golang.org/grpc/health/grpc_health_v1"
32+
"k8s.io/client-go/rest"
33+
"k8s.io/component-base/metrics/legacyregistry"
2834
ctrl "sigs.k8s.io/controller-runtime"
2935
"sigs.k8s.io/controller-runtime/pkg/log/zap"
3036
"sigs.k8s.io/controller-runtime/pkg/manager"
37+
"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
3138
"sigs.k8s.io/gateway-api-inference-extension/internal/runnable"
3239
runserver "sigs.k8s.io/gateway-api-inference-extension/pkg/body-based-routing/server"
40+
"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/metrics"
3341
"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/logging"
3442
)
3543

@@ -42,6 +50,8 @@ var (
4250
"grpcHealthPort",
4351
9003,
4452
"The port used for gRPC liveness and readiness probes")
53+
metricsPort = flag.Int(
54+
"metricsPort", 9090, "The metrics port")
4555
logVerbosity = flag.Int("v", logging.DEFAULT, "number for the log level verbosity")
4656

4757
setupLog = ctrl.Log.WithName("setup")
@@ -95,6 +105,11 @@ func run() error {
95105
return err
96106
}
97107

108+
// Register metrics handler.
109+
if err := registerMetricsHandler(mgr, *metricsPort, cfg); err != nil {
110+
return err
111+
}
112+
98113
// Start the manager. This blocks until a signal is received.
99114
setupLog.Info("Manager starting")
100115
if err := mgr.Start(ctx); err != nil {
@@ -135,3 +150,58 @@ func initLogging(opts *zap.Options) {
135150
logger := zap.New(zap.UseFlagOptions(opts), zap.RawZapOpts(uberzap.AddCaller()))
136151
ctrl.SetLogger(logger)
137152
}
153+
154+
const metricsEndpoint = "/metrics"
155+
156+
// registerMetricsHandler adds the metrics HTTP handler as a Runnable to the given manager.
157+
func registerMetricsHandler(mgr manager.Manager, port int, cfg *rest.Config) error {
158+
metrics.Register()
159+
160+
// Init HTTP server.
161+
h, err := metricsHandlerWithAuthenticationAndAuthorization(cfg)
162+
if err != nil {
163+
return err
164+
}
165+
166+
mux := http.NewServeMux()
167+
mux.Handle(metricsEndpoint, h)
168+
169+
srv := &http.Server{
170+
Addr: net.JoinHostPort("", strconv.Itoa(port)),
171+
Handler: mux,
172+
}
173+
174+
if err := mgr.Add(&manager.Server{
175+
Name: "metrics",
176+
Server: srv,
177+
}); err != nil {
178+
setupLog.Error(err, "Failed to register metrics HTTP handler")
179+
return err
180+
}
181+
return nil
182+
}
183+
184+
func metricsHandlerWithAuthenticationAndAuthorization(cfg *rest.Config) (http.Handler, error) {
185+
h := promhttp.HandlerFor(
186+
legacyregistry.DefaultGatherer,
187+
promhttp.HandlerOpts{},
188+
)
189+
httpClient, err := rest.HTTPClientFor(cfg)
190+
if err != nil {
191+
setupLog.Error(err, "Failed to create http client for metrics auth")
192+
return nil, err
193+
}
194+
195+
filter, err := filters.WithAuthenticationAndAuthorization(cfg, httpClient)
196+
if err != nil {
197+
setupLog.Error(err, "Failed to create metrics filter for auth")
198+
return nil, err
199+
}
200+
metricsLogger := ctrl.Log.WithName("metrics").WithValues("path", metricsEndpoint)
201+
metricsAuthHandler, err := filter(metricsLogger, h)
202+
if err != nil {
203+
setupLog.Error(err, "Failed to create metrics auth handler")
204+
return nil, err
205+
}
206+
return metricsAuthHandler, nil
207+
}

Diff for: pkg/body-based-routing/handlers/request.go

+4
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
basepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
2525
eppb "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3"
2626
"sigs.k8s.io/controller-runtime/pkg/log"
27+
"sigs.k8s.io/gateway-api-inference-extension/pkg/body-based-routing/metrics"
2728
logutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/logging"
2829
)
2930

@@ -38,6 +39,7 @@ func (s *Server) HandleRequestBody(ctx context.Context, body *eppb.HttpBody) (*e
3839

3940
modelVal, ok := data["model"]
4041
if !ok {
42+
metrics.RecordModelNotInBodyCounter()
4143
logger.V(logutil.DEFAULT).Info("Request body does not contain model parameter")
4244
return &eppb.ProcessingResponse{
4345
Response: &eppb.ProcessingResponse_RequestBody{
@@ -48,6 +50,7 @@ func (s *Server) HandleRequestBody(ctx context.Context, body *eppb.HttpBody) (*e
4850

4951
modelStr, ok := modelVal.(string)
5052
if !ok {
53+
metrics.RecordModelNotParsedCounter()
5154
logger.V(logutil.DEFAULT).Info("Model parameter value is not a string")
5255
return &eppb.ProcessingResponse{
5356
Response: &eppb.ProcessingResponse_RequestBody{
@@ -56,6 +59,7 @@ func (s *Server) HandleRequestBody(ctx context.Context, body *eppb.HttpBody) (*e
5659
}, fmt.Errorf("the model parameter value %v is not a string", modelVal)
5760
}
5861

62+
metrics.RecordSuccessCounter()
5963
return &eppb.ProcessingResponse{
6064
Response: &eppb.ProcessingResponse_RequestBody{
6165
RequestBody: &eppb.BodyResponse{

Diff for: pkg/body-based-routing/handlers/request_test.go

+21
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,16 @@ package handlers
1818

1919
import (
2020
"context"
21+
"strings"
2122
"testing"
2223

2324
basepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
2425
extProcPb "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3"
2526
"github.com/google/go-cmp/cmp"
2627
"google.golang.org/protobuf/testing/protocmp"
28+
"k8s.io/component-base/metrics/legacyregistry"
29+
metricsutils "k8s.io/component-base/metrics/testutil"
30+
"sigs.k8s.io/gateway-api-inference-extension/pkg/body-based-routing/metrics"
2731
logutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/logging"
2832
)
2933

@@ -48,6 +52,7 @@ const (
4852
)
4953

5054
func TestHandleRequestBody(t *testing.T) {
55+
metrics.Register()
5156
ctx := logutil.NewTestLoggerIntoContext(context.Background())
5257

5358
tests := []struct {
@@ -125,4 +130,20 @@ func TestHandleRequestBody(t *testing.T) {
125130
}
126131
})
127132
}
133+
134+
wantMetrics := `
135+
# HELP bbr_model_not_in_body_total [ALPHA] Count of times the model was not present in the request body.
136+
# TYPE bbr_model_not_in_body_total counter
137+
bbr_model_not_in_body_total{} 1
138+
# HELP bbr_model_not_parsed_total [ALPHA] Count of times the model was in the request body but we could not parse it.
139+
# TYPE bbr_model_not_parsed_total counter
140+
bbr_model_not_parsed_total{} 1
141+
# HELP bbr_success_total [ALPHA] Count of successes pulling model name from body and injecting it in the request headers.
142+
# TYPE bbr_success_total counter
143+
bbr_success_total{} 1
144+
`
145+
146+
if err := metricsutils.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(wantMetrics), "inference_model_request_total"); err != nil {
147+
t.Error(err)
148+
}
128149
}

Diff for: pkg/body-based-routing/metrics/metrics.go

+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package metrics
18+
19+
import (
20+
"sync"
21+
22+
compbasemetrics "k8s.io/component-base/metrics"
23+
"k8s.io/component-base/metrics/legacyregistry"
24+
)
25+
26+
const component = "bbr"
27+
28+
var (
29+
successCounter = compbasemetrics.NewCounterVec(
30+
&compbasemetrics.CounterOpts{
31+
Subsystem: component,
32+
Name: "success_total",
33+
Help: "Count of successes pulling model name from body and injecting it in the request headers.",
34+
StabilityLevel: compbasemetrics.ALPHA,
35+
},
36+
[]string{},
37+
)
38+
modelNotInBodyCounter = compbasemetrics.NewCounterVec(
39+
&compbasemetrics.CounterOpts{
40+
Subsystem: component,
41+
Name: "model_not_in_body_total",
42+
Help: "Count of times the model was not present in the request body.",
43+
StabilityLevel: compbasemetrics.ALPHA,
44+
},
45+
[]string{},
46+
)
47+
modelNotParsedCounter = compbasemetrics.NewCounterVec(
48+
&compbasemetrics.CounterOpts{
49+
Subsystem: component,
50+
Name: "model_not_parsed_total",
51+
Help: "Count of times the model was in the request body but we could not parse it.",
52+
StabilityLevel: compbasemetrics.ALPHA,
53+
},
54+
[]string{},
55+
)
56+
57+
// TODO: Uncomment and use this metrics once the core server implementation has handling to skip body parsing if header exists.
58+
/*
59+
modelAlreadyPresentInHeaderCounter = compbasemetrics.NewCounterVec(
60+
&compbasemetrics.CounterOpts{
61+
Subsystem: component,
62+
Name: "model_already_present_in_header_total",
63+
Help: "Count of times the model was already present in request headers.",
64+
StabilityLevel: compbasemetrics.ALPHA,
65+
},
66+
[]string{},
67+
)
68+
*/
69+
)
70+
71+
var registerMetrics sync.Once
72+
73+
// Register all metrics.
74+
func Register() {
75+
registerMetrics.Do(func() {
76+
legacyregistry.MustRegister(successCounter)
77+
legacyregistry.MustRegister(modelNotInBodyCounter)
78+
legacyregistry.MustRegister(modelNotParsedCounter)
79+
// legacyregistry.MustRegister(modelAlreadyPresentInHeaderCounter)
80+
})
81+
}
82+
83+
// RecordSuccessCounter records the number of successful requests to inject the model name into request headers.
84+
func RecordSuccessCounter() {
85+
successCounter.WithLabelValues().Inc()
86+
}
87+
88+
// RecordModelNotInBodyCounter records the number of times the model was not found in the request body.
89+
func RecordModelNotInBodyCounter() {
90+
modelNotInBodyCounter.WithLabelValues().Inc()
91+
}
92+
93+
// RecordModelNotParsedCounter records the number of times the model was found in the body but it could not be parsed.
94+
func RecordModelNotParsedCounter() {
95+
modelNotParsedCounter.WithLabelValues().Inc()
96+
}
97+
98+
/*
99+
// RecordModelAlreadyInHeaderCounter records the number of times the model was already found in the request headers.
100+
func RecordModelAlreadyInHeaderCounter() {
101+
modelAlreadyPresentInHeaderCounter.WithLabelValues().Inc()
102+
}
103+
*/

0 commit comments

Comments
 (0)