Skip to content

Commit f58d513

Browse files
committed
add support for exposing prometheus metrics for function runs
Signed-off-by: Siddharth <[email protected]>
1 parent 4b0a1c8 commit f58d513

File tree

3 files changed

+103
-1
lines changed

3 files changed

+103
-1
lines changed

go.mod

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ require (
99
github.com/go-logr/logr v1.4.2
1010
github.com/go-logr/zapr v1.3.0
1111
github.com/google/go-cmp v0.6.0
12+
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0
1213
github.com/pkg/errors v0.9.1
14+
github.com/prometheus/client_golang v1.19.1
1315
go.uber.org/zap v1.27.0
1416
google.golang.org/grpc v1.67.0
1517
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1
@@ -35,9 +37,11 @@ require (
3537
github.com/Microsoft/go-winio v0.6.2 // indirect
3638
github.com/Microsoft/hcsshim v0.12.6 // indirect
3739
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
40+
github.com/beorn7/perks v1.0.1 // indirect
3841
github.com/bufbuild/protocompile v0.14.1 // indirect
3942
github.com/bufbuild/protoplugin v0.0.0-20240911180120-7bb73e41a54a // indirect
4043
github.com/bufbuild/protovalidate-go v0.6.5 // indirect
44+
github.com/cespare/xxhash/v2 v2.3.0 // indirect
4145
github.com/containerd/cgroups/v3 v3.0.3 // indirect
4246
github.com/containerd/containerd v1.7.22 // indirect
4347
github.com/containerd/continuity v0.4.3 // indirect
@@ -80,6 +84,7 @@ require (
8084
github.com/google/gofuzz v1.2.0 // indirect
8185
github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 // indirect
8286
github.com/google/uuid v1.6.0 // indirect
87+
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 // indirect
8388
github.com/inconshreveable/mousetrap v1.1.0 // indirect
8489
github.com/jdx/go-netrc v1.0.0 // indirect
8590
github.com/josharian/intern v1.0.0 // indirect
@@ -109,6 +114,9 @@ require (
109114
github.com/opencontainers/runtime-spec v1.2.0 // indirect
110115
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
111116
github.com/pkg/profile v1.7.0 // indirect
117+
github.com/prometheus/client_model v0.6.1 // indirect
118+
github.com/prometheus/common v0.55.0 // indirect
119+
github.com/prometheus/procfs v0.15.1 // indirect
112120
github.com/quic-go/qpack v0.5.1 // indirect
113121
github.com/quic-go/quic-go v0.48.2 // indirect
114122
github.com/rs/cors v1.11.1 // indirect

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,10 @@ github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134/go.mod h1:vavhavw2zAx
192192
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
193193
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
194194
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
195+
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o=
196+
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20=
197+
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 h1:pRhl55Yx1eC7BZ1N+BBWwnKaMyD8uC+34TLdndZMAKk=
198+
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0/go.mod h1:XKMd7iuf/RGPSMJ/U4HP0zS2Z9Fh8Ps9a+6X26m/tmI=
195199
github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
196200
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
197201
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=

sdk.go

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,13 @@ import (
2222
"crypto/tls"
2323
"crypto/x509"
2424
"net"
25+
"net/http"
2526
"os"
2627
"path/filepath"
2728

2829
"github.com/pkg/errors"
30+
"github.com/prometheus/client_golang/prometheus"
31+
"github.com/prometheus/client_golang/prometheus/promhttp"
2932
"google.golang.org/grpc"
3033
"google.golang.org/grpc/credentials"
3134
ginsecure "google.golang.org/grpc/credentials/insecure"
@@ -36,13 +39,16 @@ import (
3639
"github.com/crossplane/function-sdk-go/logging"
3740
v1 "github.com/crossplane/function-sdk-go/proto/v1"
3841
"github.com/crossplane/function-sdk-go/proto/v1beta1"
42+
43+
grpcprometheus "github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus"
3944
)
4045

4146
// Default ServeOptions.
4247
const (
4348
DefaultNetwork = "tcp"
4449
DefaultAddress = ":9443"
4550
DefaultMaxRecvMsgSize = 1024 * 1024 * 4
51+
DefaultMetricsAddress = ":8080"
4652
)
4753

4854
// ServeOptions configure how a Function is served.
@@ -52,6 +58,13 @@ type ServeOptions struct {
5258
MaxRecvMsgSize int
5359
Credentials credentials.TransportCredentials
5460
HealthServer healthgrpc.HealthServer
61+
62+
// Metrics options
63+
EnableMetrics bool
64+
MetricsAddress string
65+
MetricsServer *http.Server
66+
MetricsRegistry *prometheus.Registry
67+
UnaryInterceptors []grpc.UnaryServerInterceptor
5568
}
5669

5770
// A ServeOption configures how a Function is served.
@@ -60,6 +73,7 @@ type ServeOption func(o *ServeOptions) error
6073
// Listen configures the network, address, and maximum message size on which the
6174
// Function will listen for RunFunctionRequests.
6275
func Listen(network, address string) ServeOption {
76+
grpc.NewServer()
6377
return func(o *ServeOptions) error {
6478
o.Network = network
6579
o.Address = address
@@ -143,6 +157,33 @@ func WithHealthServer(srv healthgrpc.HealthServer) ServeOption {
143157
}
144158
}
145159

160+
// WithMetrics enables Prometheus metrics collection using gRPC interceptors.
161+
func WithMetrics() ServeOption {
162+
return func(o *ServeOptions) error {
163+
o.EnableMetrics = true
164+
return nil
165+
}
166+
}
167+
168+
// WithMetricsServer configures the metrics server address and starts an HTTP server
169+
// to expose Prometheus metrics on /metrics endpoint.
170+
func WithMetricsServer(address string) ServeOption {
171+
return func(o *ServeOptions) error {
172+
o.MetricsAddress = address
173+
o.EnableMetrics = true
174+
return nil
175+
}
176+
}
177+
178+
// WithMetricsRegistry configures a custom Prometheus registry for metrics.
179+
func WithMetricsRegistry(registry *prometheus.Registry) ServeOption {
180+
return func(o *ServeOptions) error {
181+
o.MetricsRegistry = registry
182+
o.EnableMetrics = true
183+
return nil
184+
}
185+
}
186+
146187
// Serve the supplied Function by creating a gRPC server and listening for
147188
// RunFunctionRequests. Blocks until the server returns an error.
148189
func Serve(fn v1.FunctionRunnerServiceServer, o ...ServeOption) error {
@@ -167,7 +208,31 @@ func Serve(fn v1.FunctionRunnerServiceServer, o ...ServeOption) error {
167208
return errors.Wrapf(err, "cannot listen for %s connections at address %q", so.Network, so.Address)
168209
}
169210

170-
srv := grpc.NewServer(grpc.MaxRecvMsgSize(so.MaxRecvMsgSize), grpc.Creds(so.Credentials))
211+
// Build interceptors based on options
212+
var unaryInterceptors []grpc.UnaryServerInterceptor
213+
214+
// Add metrics interceptor if enabled
215+
if so.EnableMetrics {
216+
// Use Prometheus metrics
217+
metrics := grpcprometheus.NewServerMetrics()
218+
// Add unary metrics interceptor (Crossplane Functions only use unary calls)
219+
unaryInterceptors = append(unaryInterceptors, metrics.UnaryServerInterceptor())
220+
}
221+
222+
// Add custom interceptors
223+
unaryInterceptors = append(unaryInterceptors, so.UnaryInterceptors...)
224+
225+
// Create server with interceptors
226+
serverOpts := []grpc.ServerOption{
227+
grpc.MaxRecvMsgSize(so.MaxRecvMsgSize),
228+
grpc.Creds(so.Credentials),
229+
}
230+
231+
if len(unaryInterceptors) > 0 {
232+
serverOpts = append(serverOpts, grpc.ChainUnaryInterceptor(unaryInterceptors...))
233+
}
234+
235+
srv := grpc.NewServer(serverOpts...)
171236
reflection.Register(srv)
172237
v1.RegisterFunctionRunnerServiceServer(srv, fn)
173238
v1beta1.RegisterFunctionRunnerServiceServer(srv, ServeBeta(fn))
@@ -176,6 +241,31 @@ func Serve(fn v1.FunctionRunnerServiceServer, o ...ServeOption) error {
176241
healthgrpc.RegisterHealthServer(srv, so.HealthServer)
177242
}
178243

244+
// Start metrics server if enabled
245+
if so.EnableMetrics && so.MetricsAddress != "" {
246+
// Use custom registry if provided, otherwise use default
247+
var handler http.Handler
248+
if so.MetricsRegistry != nil {
249+
handler = promhttp.HandlerFor(so.MetricsRegistry, promhttp.HandlerOpts{})
250+
} else {
251+
handler = promhttp.Handler()
252+
}
253+
254+
metricsServer := &http.Server{
255+
Addr: so.MetricsAddress,
256+
Handler: handler,
257+
}
258+
so.MetricsServer = metricsServer
259+
260+
// Start metrics server in a goroutine
261+
go func() {
262+
if err := metricsServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
263+
// Log error but don't fail the main server
264+
// In a real implementation, you'd want to use a proper logger
265+
}
266+
}()
267+
}
268+
179269
return errors.Wrap(srv.Serve(lis), "cannot serve mTLS gRPC connections")
180270
}
181271

0 commit comments

Comments
 (0)