From 002a61441cf35a8d1efc69357096f2bf2414819b Mon Sep 17 00:00:00 2001 From: Vladimir Glafirov Date: Wed, 22 Oct 2025 17:28:22 +0200 Subject: [PATCH 1/5] added Otel metrics --- go.mod | 15 +- go.sum | 48 ++++-- metrics/doc.go | 9 ++ metrics/grpc/client_factory.go | 42 +++++ metrics/grpc/client_factory_test.go | 96 ++++++++++++ metrics/grpc/doc.go | 41 +++++ metrics/grpc/examples_test.go | 232 ++++++++++++++++++++++++++++ metrics/grpc/options.go | 73 +++++++++ metrics/grpc/server_factory.go | 42 +++++ metrics/grpc/server_factory_test.go | 106 +++++++++++++ 10 files changed, 692 insertions(+), 12 deletions(-) create mode 100644 metrics/grpc/client_factory.go create mode 100644 metrics/grpc/client_factory_test.go create mode 100644 metrics/grpc/doc.go create mode 100644 metrics/grpc/examples_test.go create mode 100644 metrics/grpc/options.go create mode 100644 metrics/grpc/server_factory.go create mode 100644 metrics/grpc/server_factory_test.go diff --git a/go.mod b/go.mod index dda720d3..a01a2794 100644 --- a/go.mod +++ b/go.mod @@ -16,13 +16,16 @@ require ( github.com/prometheus/client_model v0.6.1 github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a github.com/sirupsen/logrus v1.9.3 - github.com/stretchr/testify v1.10.0 + github.com/stretchr/testify v1.11.1 github.com/uber/jaeger-client-go v2.29.1+incompatible gitlab.com/gitlab-org/go/reopen v1.0.0 go.opencensus.io v0.23.0 + go.opentelemetry.io/otel/metric v1.38.0 + go.opentelemetry.io/otel/sdk/metric v1.38.0 golang.org/x/crypto v0.41.0 google.golang.org/api v0.54.0 - google.golang.org/grpc v1.67.1 + google.golang.org/grpc v1.68.0 + google.golang.org/grpc/stats/opentelemetry v0.0.0-20241028142157-ada6787961b3 google.golang.org/protobuf v1.36.6 gopkg.in/DataDog/dd-trace-go.v1 v1.32.0 ) @@ -41,10 +44,12 @@ require ( github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.2.4 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect - github.com/golang/protobuf v1.5.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect github.com/google/pprof v0.0.0-20210804190019-f964ff605595 // indirect github.com/google/uuid v1.6.0 // indirect github.com/googleapis/gax-go/v2 v2.0.5 // indirect @@ -65,6 +70,10 @@ require ( github.com/tklauser/go-sysconf v0.3.4 // indirect github.com/tklauser/numcpus v0.2.1 // indirect github.com/uber/jaeger-lib v2.4.1+incompatible // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/sdk v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect go.uber.org/atomic v1.4.0 // indirect golang.org/x/net v0.42.0 // indirect golang.org/x/oauth2 v0.23.0 // indirect diff --git a/go.sum b/go.sum index 7653da72..808a20e6 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +cel.dev/expr v0.23.1 h1:K4KOtPCJQjVggkARsjG9RWXP6O4R73aHeJMa/dmCQQg= +cel.dev/expr v0.23.1/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= @@ -85,6 +87,8 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 h1:QVw89YDxXxEe+l8gU8ETbOasdwEV+avkR75ZzsVV9WI= +github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -96,7 +100,11 @@ github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5y github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.13.1 h1:vPfJZCkob6yTMEgS+0TwfTUfbHjfy/6vOJ8hUWX/uXE= +github.com/envoyproxy/go-control-plane v0.13.1/go.mod h1:X45hY0mufo6Fd0KW3rqsGvQMw58jvjymeCzBU3mWyHw= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM= +github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs= @@ -111,6 +119,11 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI= github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= @@ -148,8 +161,9 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= @@ -165,8 +179,8 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= @@ -259,6 +273,8 @@ github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTw github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI= @@ -272,8 +288,8 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a h1:iLcLb5Fwwz7g/DLK89F+uQBDeAhHhwdzB5fSlVdhGcM= github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a/go.mod h1:wozgYq9WEBQBaIJe4YZ0qTSFAMxmcwBhQH0fO0R34Z0= github.com/shirou/gopsutil/v3 v3.21.2 h1:fIOk3hyqV1oGKogfGNjUZa0lUbtlkx3+ZT0IoJth2uM= @@ -292,8 +308,8 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tinylib/msgp v1.1.2 h1:gWmO7n0Ys2RBEb7GPYB9Ujq8Mk5p2U08lRnmMcGy6BQ= github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= github.com/tklauser/go-sysconf v0.3.4 h1:HT8SVixZd3IzLdfs/xlpq0jeSfTX57g1v6wB1EuzV7M= @@ -320,6 +336,18 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.22.6/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= @@ -691,9 +719,11 @@ google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQ google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= -google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= -google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0= +google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= +google.golang.org/grpc/stats/opentelemetry v0.0.0-20241028142157-ada6787961b3 h1:hUfOButuEtpc0UvYiaYRbNwxVYr0mQQOWq6X8beJ9Gc= +google.golang.org/grpc/stats/opentelemetry v0.0.0-20241028142157-ada6787961b3/go.mod h1:jzYlkSMbKypzuu6xoAEijsNVo9ZeDF1u/zCfFgsx7jg= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= diff --git a/metrics/doc.go b/metrics/doc.go index ebfa6e78..cbd0a7c9 100644 --- a/metrics/doc.go +++ b/metrics/doc.go @@ -5,5 +5,14 @@ Package metrics is the primary entrypoint into LabKit's metrics utilities. - Provides http middlewares for recording metrics for an http server - Provides a collector for sql.DBStats metrics +- Provides OpenTelemetry-based gRPC metrics for both client and server instrumentation + +# gRPC Metrics + +The grpc subpackage provides OpenTelemetry instrumentation for gRPC applications +without requiring a global metrics registry. Clients must explicitly provide a +MeterProvider when creating metrics. + +See the grpc subpackage documentation for detailed usage examples. */ package metrics diff --git a/metrics/grpc/client_factory.go b/metrics/grpc/client_factory.go new file mode 100644 index 00000000..1101cd9f --- /dev/null +++ b/metrics/grpc/client_factory.go @@ -0,0 +1,42 @@ +package grpc + +import ( + "google.golang.org/grpc" + "google.golang.org/grpc/stats/opentelemetry" +) + +// ClientMetricsFactory creates a gRPC dial option for client-side metrics collection. +// The returned function can be called multiple times to obtain the same dial option. +type ClientMetricsFactory func() grpc.DialOption + +// NewClientMetricsFactory creates a factory for gRPC client metrics. +// The factory returns a dial option that enables OpenTelemetry instrumentation +// for a gRPC client connection. +// +// The MeterProvider must be provided via WithMeterProvider option, otherwise +// no metrics will be recorded. +// +// Example: +// +// factory := NewClientMetricsFactory( +// WithMeterProvider(meterProvider), +// ) +// conn, err := grpc.NewClient("target", factory()) +func NewClientMetricsFactory(opts ...MetricsFactoryOption) ClientMetricsFactory { + config := applyMetricsFactoryOptions(opts) + + // Create the OpenTelemetry options + otelOpts := opentelemetry.Options{ + MetricsOptions: opentelemetry.MetricsOptions{ + MeterProvider: config.meterProvider, + Metrics: config.metrics, + MethodAttributeFilter: config.methodAttributeFilter, + OptionalLabels: config.optionalLabels, + }, + } + + return func() grpc.DialOption { + return opentelemetry.DialOption(otelOpts) + } +} + diff --git a/metrics/grpc/client_factory_test.go b/metrics/grpc/client_factory_test.go new file mode 100644 index 00000000..9c4276ee --- /dev/null +++ b/metrics/grpc/client_factory_test.go @@ -0,0 +1,96 @@ +package grpc + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/otel/sdk/metric" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +func TestNewClientMetricsFactory(t *testing.T) { + tests := []struct { + name string + opts []MetricsFactoryOption + wantErr bool + }{ + { + name: "factory without meter provider", + opts: []MetricsFactoryOption{}, + }, + { + name: "factory with meter provider", + opts: []MetricsFactoryOption{ + WithMeterProvider(metric.NewMeterProvider()), + }, + }, + { + name: "factory with method attribute filter", + opts: []MetricsFactoryOption{ + WithMeterProvider(metric.NewMeterProvider()), + WithMethodAttributeFilter(func(method string) bool { + return method != "/test.Service/Test" + }), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + factory := NewClientMetricsFactory(tt.opts...) + assert.NotNil(t, factory) + + dialOpt := factory() + assert.NotNil(t, dialOpt) + }) + } +} + +func TestClientMetricsFactory_MultipleCallsReturnSameOption(t *testing.T) { + mp := metric.NewMeterProvider() + factory := NewClientMetricsFactory(WithMeterProvider(mp)) + + opt1 := factory() + opt2 := factory() + + assert.NotNil(t, opt1) + assert.NotNil(t, opt2) +} + +func TestClientMetricsFactory_CanCreateConnection(t *testing.T) { + mp := metric.NewMeterProvider() + factory := NewClientMetricsFactory(WithMeterProvider(mp)) + + dialOpt := factory() + assert.NotNil(t, dialOpt) + + // Create a gRPC client connection with metrics enabled + // Note: We're not actually connecting to a real server, just verifying the dial option is valid + cc, err := grpc.NewClient( + "localhost:9999", + dialOpt, + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + + // The connection may fail due to connection issues, but that's expected + // We're just verifying that the dial option is valid and can be used + if err == nil { + defer cc.Close() + } +} + +func TestClientMetricsFactory_WithAllOptions(t *testing.T) { + mp := metric.NewMeterProvider() + + factory := NewClientMetricsFactory( + WithMeterProvider(mp), + WithMethodAttributeFilter(func(method string) bool { + return true + }), + WithOptionalLabels([]string{"label1", "label2"}), + ) + + dialOpt := factory() + assert.NotNil(t, dialOpt) +} diff --git a/metrics/grpc/doc.go b/metrics/grpc/doc.go new file mode 100644 index 00000000..53635733 --- /dev/null +++ b/metrics/grpc/doc.go @@ -0,0 +1,41 @@ +/* +Package grpc provides OpenTelemetry instrumentation for gRPC clients and servers. + +This package uses the official OpenTelemetry gRPC metrics instrumentation from +google.golang.org/grpc/stats/opentelemetry, allowing applications to collect +metrics without relying on a global metrics registry. + +# Client-Side Metrics + +To instrument a gRPC client, create a dial option using NewClientMetricsFactory: + + factory := grpc.NewClientMetricsFactory( + grpc.WithMeterProvider(meterProvider), + ) + dialOption := factory() + conn, err := grpc.NewClient("target", dialOption) + +# Server-Side Metrics + +To instrument a gRPC server, create a server option using NewServerMetricsFactory: + + factory := grpc.NewServerMetricsFactory( + grpc.WithMeterProvider(meterProvider), + ) + serverOption := factory() + server := grpc.NewServer(serverOption) + +# Metrics Registration + +Unlike traditional metrics packages that use a global registry, this package +requires clients to explicitly provide a MeterProvider. This allows for: + +- Better control over metric collection +- Support for multiple isolated metric collectors +- Cleaner testing without global state +- More flexible deployment scenarios + +The MeterProvider should be configured with appropriate views and bounds as needed. +*/ +package grpc + diff --git a/metrics/grpc/examples_test.go b/metrics/grpc/examples_test.go new file mode 100644 index 00000000..4222880c --- /dev/null +++ b/metrics/grpc/examples_test.go @@ -0,0 +1,232 @@ +package grpc + +import ( + "context" + "fmt" + "log" + "net" + "testing" + + "go.opentelemetry.io/otel/sdk/metric" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +// Mock gRPC service for testing +type testServer struct{} + +func (s *testServer) UnaryCall(ctx context.Context, req any) (any, error) { + return nil, nil +} + +func ExampleNewClientMetricsFactory() { + // Create a meter provider - in production, you would configure this + // with appropriate exporters (e.g., Prometheus, OTLP) + meterProvider := metric.NewMeterProvider() + + // Create a client metrics factory with the meter provider + clientFactory := NewClientMetricsFactory( + WithMeterProvider(meterProvider), + ) + + // Use the factory to create a dial option + dialOption := clientFactory() + + // Create a gRPC client connection with metrics enabled + conn, err := grpc.NewClient( + "localhost:50051", + dialOption, + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + if err != nil { + log.Printf("failed to create client: %v", err) + return + } + defer conn.Close() + + fmt.Println("Client created with metrics collection enabled") +} + +func ExampleNewServerMetricsFactory() { + // Create a meter provider - in production, you would configure this + // with appropriate exporters (e.g., Prometheus, OTLP) + meterProvider := metric.NewMeterProvider() + + // Create a server metrics factory with the meter provider + serverFactory := NewServerMetricsFactory( + WithMeterProvider(meterProvider), + ) + + // Use the factory to create a server option + serverOption := serverFactory() + + // Create a gRPC server with metrics enabled + server := grpc.NewServer(serverOption) + defer server.Stop() + + fmt.Println("Server created with metrics collection enabled") +} + +func ExampleWithMeterProvider() { + // Create a custom meter provider with specific configuration + meterProvider := metric.NewMeterProvider() + + // Create a client factory with the meter provider + clientFactory := NewClientMetricsFactory( + WithMeterProvider(meterProvider), + ) + + dialOption := clientFactory() + _ = dialOption + + fmt.Println("Client factory created with custom meter provider") +} + +func ExampleWithMethodAttributeFilter() { + meterProvider := metric.NewMeterProvider() + + // Create a server factory with method attribute filtering + // This is useful to limit cardinality when using unknown service handlers + serverFactory := NewServerMetricsFactory( + WithMeterProvider(meterProvider), + WithMethodAttributeFilter(func(method string) bool { + // Only record metrics for specific methods + // This prevents high cardinality issues with dynamic methods + return method != "/grpc.health.v1.Health/Check" + }), + ) + + serverOption := serverFactory() + _ = serverOption + + fmt.Println("Server factory created with method attribute filter") +} + +func ExampleWithOptionalLabels() { + meterProvider := metric.NewMeterProvider() + + // Create a server factory with optional labels from load balancer policies + serverFactory := NewServerMetricsFactory( + WithMeterProvider(meterProvider), + WithOptionalLabels([]string{"zone", "instance"}), + ) + + serverOption := serverFactory() + _ = serverOption + + fmt.Println("Server factory created with optional labels") +} + +func TestExampleClientServerIntegration(t *testing.T) { + // This test demonstrates client and server metrics working together + meterProvider := metric.NewMeterProvider() + + // Create server with metrics + serverFactory := NewServerMetricsFactory( + WithMeterProvider(meterProvider), + ) + + server := grpc.NewServer(serverFactory()) + defer server.Stop() + + // Listen on a random port + listener, err := net.Listen("tcp", "localhost:0") + if err != nil { + t.Fatalf("failed to listen: %v", err) + } + + // Start server in a goroutine + go func() { + if err := server.Serve(listener); err != nil { + log.Printf("server error: %v", err) + } + }() + + // Create client with metrics + clientFactory := NewClientMetricsFactory( + WithMeterProvider(meterProvider), + ) + + conn, err := grpc.NewClient( + listener.Addr().String(), + clientFactory(), + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + if err != nil { + t.Fatalf("failed to create client: %v", err) + } + defer conn.Close() + + // Verify connection is established + if conn == nil { + t.Fatal("connection is nil") + } +} + +func TestExampleMultipleFactories(t *testing.T) { + // Demonstrate creating multiple factories with different configurations + mp1 := metric.NewMeterProvider() + mp2 := metric.NewMeterProvider() + + // Create first client factory + clientFactory1 := NewClientMetricsFactory( + WithMeterProvider(mp1), + ) + + // Create second client factory with different configuration + clientFactory2 := NewClientMetricsFactory( + WithMeterProvider(mp2), + WithMethodAttributeFilter(func(method string) bool { + return true + }), + ) + + opt1 := clientFactory1() + opt2 := clientFactory2() + + if opt1 == nil || opt2 == nil { + t.Fatal("dial options are nil") + } +} + +func TestExampleFactoryReusability(t *testing.T) { + // Demonstrate that factories can be reused to create multiple connections/servers + meterProvider := metric.NewMeterProvider() + clientFactory := NewClientMetricsFactory( + WithMeterProvider(meterProvider), + ) + + // Create multiple dial options from the same factory + dialOpt1 := clientFactory() + dialOpt2 := clientFactory() + dialOpt3 := clientFactory() + + if dialOpt1 == nil || dialOpt2 == nil || dialOpt3 == nil { + t.Fatal("dial options are nil") + } +} + +func TestExampleNoGlobalRegistry(t *testing.T) { + // This test demonstrates that metrics are NOT registered in a global registry + // Instead, they are collected via the MeterProvider passed to the factory + + meterProvider := metric.NewMeterProvider() + + // Create server factory - this does NOT register metrics globally + serverFactory := NewServerMetricsFactory( + WithMeterProvider(meterProvider), + ) + + // Create multiple servers with the same factory + server1 := grpc.NewServer(serverFactory()) + defer server1.Stop() + + server2 := grpc.NewServer(serverFactory()) + defer server2.Stop() + + // Both servers are using the same meter provider, not a global registry + // This allows for better isolation and testing + if server1 == nil || server2 == nil { + t.Fatal("servers are nil") + } +} diff --git a/metrics/grpc/options.go b/metrics/grpc/options.go new file mode 100644 index 00000000..776512ba --- /dev/null +++ b/metrics/grpc/options.go @@ -0,0 +1,73 @@ +package grpc + +import ( + "go.opentelemetry.io/otel/metric" + "google.golang.org/grpc/experimental/stats" +) + +// MetricsFactoryOption is used to configure a metrics factory. +type MetricsFactoryOption interface { + apply(*metricsFactoryConfig) +} + +type metricsFactoryConfig struct { + meterProvider metric.MeterProvider + metrics *stats.Metrics + methodAttributeFilter func(string) bool + optionalLabels []string +} + +type funcMetricsFactoryOption struct { + f func(*metricsFactoryConfig) +} + +func (fdo funcMetricsFactoryOption) apply(do *metricsFactoryConfig) { + fdo.f(do) +} + +func newFuncMetricsFactoryOption(f func(*metricsFactoryConfig)) funcMetricsFactoryOption { + return funcMetricsFactoryOption{ + f: f, + } +} + +// WithMeterProvider sets the MeterProvider to use for creating instruments. +// This is required for metrics collection. If not set, no metrics will be recorded. +func WithMeterProvider(mp metric.MeterProvider) MetricsFactoryOption { + return newFuncMetricsFactoryOption(func(c *metricsFactoryConfig) { + c.meterProvider = mp + }) +} + +// WithMetrics specifies which metrics to instrument. If not set, default metrics will be recorded. +func WithMetrics(m *stats.Metrics) MetricsFactoryOption { + return newFuncMetricsFactoryOption(func(c *metricsFactoryConfig) { + c.metrics = m + }) +} + +// WithMethodAttributeFilter sets a filter for recording method names of RPCs. +// This is useful to limit cardinality for unknown service handlers. +// Only applies to server-side metrics. +func WithMethodAttributeFilter(filter func(string) bool) MetricsFactoryOption { + return newFuncMetricsFactoryOption(func(c *metricsFactoryConfig) { + c.methodAttributeFilter = filter + }) +} + +// WithOptionalLabels sets labels received from LB Policies that should be added +// to metrics that record after receiving incoming metadata. +func WithOptionalLabels(labels []string) MetricsFactoryOption { + return newFuncMetricsFactoryOption(func(c *metricsFactoryConfig) { + c.optionalLabels = labels + }) +} + +func applyMetricsFactoryOptions(opts []MetricsFactoryOption) *metricsFactoryConfig { + config := &metricsFactoryConfig{} + for _, opt := range opts { + opt.apply(config) + } + return config +} + diff --git a/metrics/grpc/server_factory.go b/metrics/grpc/server_factory.go new file mode 100644 index 00000000..b368385e --- /dev/null +++ b/metrics/grpc/server_factory.go @@ -0,0 +1,42 @@ +package grpc + +import ( + "google.golang.org/grpc" + "google.golang.org/grpc/stats/opentelemetry" +) + +// ServerMetricsFactory creates a gRPC server option for server-side metrics collection. +// The returned function can be called multiple times to obtain the same server option. +type ServerMetricsFactory func() grpc.ServerOption + +// NewServerMetricsFactory creates a factory for gRPC server metrics. +// The factory returns a server option that enables OpenTelemetry instrumentation +// for a gRPC server. +// +// The MeterProvider must be provided via WithMeterProvider option, otherwise +// no metrics will be recorded. +// +// Example: +// +// factory := NewServerMetricsFactory( +// WithMeterProvider(meterProvider), +// ) +// server := grpc.NewServer(factory()) +func NewServerMetricsFactory(opts ...MetricsFactoryOption) ServerMetricsFactory { + config := applyMetricsFactoryOptions(opts) + + // Create the OpenTelemetry options + otelOpts := opentelemetry.Options{ + MetricsOptions: opentelemetry.MetricsOptions{ + MeterProvider: config.meterProvider, + Metrics: config.metrics, + MethodAttributeFilter: config.methodAttributeFilter, + OptionalLabels: config.optionalLabels, + }, + } + + return func() grpc.ServerOption { + return opentelemetry.ServerOption(otelOpts) + } +} + diff --git a/metrics/grpc/server_factory_test.go b/metrics/grpc/server_factory_test.go new file mode 100644 index 00000000..919720ab --- /dev/null +++ b/metrics/grpc/server_factory_test.go @@ -0,0 +1,106 @@ +package grpc + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/otel/sdk/metric" + "google.golang.org/grpc" +) + +func TestNewServerMetricsFactory(t *testing.T) { + tests := []struct { + name string + opts []MetricsFactoryOption + wantErr bool + }{ + { + name: "factory without meter provider", + opts: []MetricsFactoryOption{}, + }, + { + name: "factory with meter provider", + opts: []MetricsFactoryOption{ + WithMeterProvider(metric.NewMeterProvider()), + }, + }, + { + name: "factory with method attribute filter", + opts: []MetricsFactoryOption{ + WithMeterProvider(metric.NewMeterProvider()), + WithMethodAttributeFilter(func(method string) bool { + return method != "/test.Service/Test" + }), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + factory := NewServerMetricsFactory(tt.opts...) + assert.NotNil(t, factory) + + serverOpt := factory() + assert.NotNil(t, serverOpt) + }) + } +} + +func TestServerMetricsFactory_MultipleCallsReturnSameOption(t *testing.T) { + mp := metric.NewMeterProvider() + factory := NewServerMetricsFactory(WithMeterProvider(mp)) + + opt1 := factory() + opt2 := factory() + + assert.NotNil(t, opt1) + assert.NotNil(t, opt2) +} + +func TestServerMetricsFactory_CanCreateServer(t *testing.T) { + mp := metric.NewMeterProvider() + factory := NewServerMetricsFactory(WithMeterProvider(mp)) + + serverOpt := factory() + assert.NotNil(t, serverOpt) + + // Verify it's a valid grpc.ServerOption by attempting to use it + server := grpc.NewServer(serverOpt) + assert.NotNil(t, server) + server.Stop() +} + +func TestServerMetricsFactory_WithAllOptions(t *testing.T) { + mp := metric.NewMeterProvider() + + factory := NewServerMetricsFactory( + WithMeterProvider(mp), + WithMethodAttributeFilter(func(method string) bool { + return true + }), + WithOptionalLabels([]string{"label1", "label2"}), + ) + + serverOpt := factory() + assert.NotNil(t, serverOpt) + + server := grpc.NewServer(serverOpt) + assert.NotNil(t, server) + server.Stop() +} + +func TestServerMetricsFactory_MultipleServers(t *testing.T) { + mp := metric.NewMeterProvider() + factory := NewServerMetricsFactory(WithMeterProvider(mp)) + + // Create multiple servers with the same factory + server1 := grpc.NewServer(factory()) + defer server1.Stop() + + server2 := grpc.NewServer(factory()) + defer server2.Stop() + + assert.NotNil(t, server1) + assert.NotNil(t, server2) +} + -- GitLab From 2c6f2ccc86b5ce5744b288fda8297b7360eed03b Mon Sep 17 00:00:00 2001 From: Vladimir Glafirov Date: Thu, 23 Oct 2025 13:12:16 +0200 Subject: [PATCH 2/5] removed factory pattern --- metrics/grpc/client.go | 34 +++++ metrics/grpc/client_factory.go | 42 ------ ...{client_factory_test.go => client_test.go} | 43 ++---- metrics/grpc/doc.go | 11 +- metrics/grpc/examples_test.go | 130 ++++++++++-------- metrics/grpc/options.go | 47 ++++--- metrics/grpc/server.go | 34 +++++ metrics/grpc/server_factory.go | 42 ------ ...{server_factory_test.go => server_test.go} | 54 +++----- 9 files changed, 202 insertions(+), 235 deletions(-) create mode 100644 metrics/grpc/client.go delete mode 100644 metrics/grpc/client_factory.go rename metrics/grpc/{client_factory_test.go => client_test.go} (58%) create mode 100644 metrics/grpc/server.go delete mode 100644 metrics/grpc/server_factory.go rename metrics/grpc/{server_factory_test.go => server_test.go} (50%) diff --git a/metrics/grpc/client.go b/metrics/grpc/client.go new file mode 100644 index 00000000..30acf33e --- /dev/null +++ b/metrics/grpc/client.go @@ -0,0 +1,34 @@ +package grpc + +import ( + "google.golang.org/grpc" + "google.golang.org/grpc/stats/opentelemetry" +) + +// NewClientMetrics creates a gRPC dial option for client-side metrics collection. +// The returned DialOption enables OpenTelemetry instrumentation for a gRPC client connection. +// +// The MeterProvider must be provided via WithMeterProvider option, otherwise +// no metrics will be recorded. +// +// Example: +// +// dialOpt := NewClientMetrics( +// WithMeterProvider(meterProvider), +// ) +// conn, err := grpc.NewClient("target", dialOpt) +func NewClientMetrics(opts ...MetricsOption) grpc.DialOption { + config := applyMetricsOptions(opts) + + // Create the OpenTelemetry options + otelOpts := opentelemetry.Options{ + MetricsOptions: opentelemetry.MetricsOptions{ + MeterProvider: config.meterProvider, + Metrics: config.metrics, + MethodAttributeFilter: config.methodAttributeFilter, + OptionalLabels: config.optionalLabels, + }, + } + + return opentelemetry.DialOption(otelOpts) +} diff --git a/metrics/grpc/client_factory.go b/metrics/grpc/client_factory.go deleted file mode 100644 index 1101cd9f..00000000 --- a/metrics/grpc/client_factory.go +++ /dev/null @@ -1,42 +0,0 @@ -package grpc - -import ( - "google.golang.org/grpc" - "google.golang.org/grpc/stats/opentelemetry" -) - -// ClientMetricsFactory creates a gRPC dial option for client-side metrics collection. -// The returned function can be called multiple times to obtain the same dial option. -type ClientMetricsFactory func() grpc.DialOption - -// NewClientMetricsFactory creates a factory for gRPC client metrics. -// The factory returns a dial option that enables OpenTelemetry instrumentation -// for a gRPC client connection. -// -// The MeterProvider must be provided via WithMeterProvider option, otherwise -// no metrics will be recorded. -// -// Example: -// -// factory := NewClientMetricsFactory( -// WithMeterProvider(meterProvider), -// ) -// conn, err := grpc.NewClient("target", factory()) -func NewClientMetricsFactory(opts ...MetricsFactoryOption) ClientMetricsFactory { - config := applyMetricsFactoryOptions(opts) - - // Create the OpenTelemetry options - otelOpts := opentelemetry.Options{ - MetricsOptions: opentelemetry.MetricsOptions{ - MeterProvider: config.meterProvider, - Metrics: config.metrics, - MethodAttributeFilter: config.methodAttributeFilter, - OptionalLabels: config.optionalLabels, - }, - } - - return func() grpc.DialOption { - return opentelemetry.DialOption(otelOpts) - } -} - diff --git a/metrics/grpc/client_factory_test.go b/metrics/grpc/client_test.go similarity index 58% rename from metrics/grpc/client_factory_test.go rename to metrics/grpc/client_test.go index 9c4276ee..f1bf96ff 100644 --- a/metrics/grpc/client_factory_test.go +++ b/metrics/grpc/client_test.go @@ -9,25 +9,25 @@ import ( "google.golang.org/grpc/credentials/insecure" ) -func TestNewClientMetricsFactory(t *testing.T) { +func TestNewClientMetrics(t *testing.T) { tests := []struct { name string - opts []MetricsFactoryOption + opts []MetricsOption wantErr bool }{ { - name: "factory without meter provider", - opts: []MetricsFactoryOption{}, + name: "metrics without meter provider", + opts: []MetricsOption{}, }, { - name: "factory with meter provider", - opts: []MetricsFactoryOption{ + name: "metrics with meter provider", + opts: []MetricsOption{ WithMeterProvider(metric.NewMeterProvider()), }, }, { - name: "factory with method attribute filter", - opts: []MetricsFactoryOption{ + name: "metrics with method attribute filter", + opts: []MetricsOption{ WithMeterProvider(metric.NewMeterProvider()), WithMethodAttributeFilter(func(method string) bool { return method != "/test.Service/Test" @@ -38,31 +38,15 @@ func TestNewClientMetricsFactory(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - factory := NewClientMetricsFactory(tt.opts...) - assert.NotNil(t, factory) - - dialOpt := factory() + dialOpt := NewClientMetrics(tt.opts...) assert.NotNil(t, dialOpt) }) } } -func TestClientMetricsFactory_MultipleCallsReturnSameOption(t *testing.T) { +func TestClientMetrics_CanCreateConnection(t *testing.T) { mp := metric.NewMeterProvider() - factory := NewClientMetricsFactory(WithMeterProvider(mp)) - - opt1 := factory() - opt2 := factory() - - assert.NotNil(t, opt1) - assert.NotNil(t, opt2) -} - -func TestClientMetricsFactory_CanCreateConnection(t *testing.T) { - mp := metric.NewMeterProvider() - factory := NewClientMetricsFactory(WithMeterProvider(mp)) - - dialOpt := factory() + dialOpt := NewClientMetrics(WithMeterProvider(mp)) assert.NotNil(t, dialOpt) // Create a gRPC client connection with metrics enabled @@ -80,10 +64,10 @@ func TestClientMetricsFactory_CanCreateConnection(t *testing.T) { } } -func TestClientMetricsFactory_WithAllOptions(t *testing.T) { +func TestClientMetrics_WithAllOptions(t *testing.T) { mp := metric.NewMeterProvider() - factory := NewClientMetricsFactory( + dialOpt := NewClientMetrics( WithMeterProvider(mp), WithMethodAttributeFilter(func(method string) bool { return true @@ -91,6 +75,5 @@ func TestClientMetricsFactory_WithAllOptions(t *testing.T) { WithOptionalLabels([]string{"label1", "label2"}), ) - dialOpt := factory() assert.NotNil(t, dialOpt) } diff --git a/metrics/grpc/doc.go b/metrics/grpc/doc.go index 53635733..9b57c5e4 100644 --- a/metrics/grpc/doc.go +++ b/metrics/grpc/doc.go @@ -7,22 +7,20 @@ metrics without relying on a global metrics registry. # Client-Side Metrics -To instrument a gRPC client, create a dial option using NewClientMetricsFactory: +To instrument a gRPC client, create a dial option using NewClientMetrics: - factory := grpc.NewClientMetricsFactory( + dialOption := grpc.NewClientMetrics( grpc.WithMeterProvider(meterProvider), ) - dialOption := factory() conn, err := grpc.NewClient("target", dialOption) # Server-Side Metrics -To instrument a gRPC server, create a server option using NewServerMetricsFactory: +To instrument a gRPC server, create a server option using NewServerMetrics: - factory := grpc.NewServerMetricsFactory( + serverOption := grpc.NewServerMetrics( grpc.WithMeterProvider(meterProvider), ) - serverOption := factory() server := grpc.NewServer(serverOption) # Metrics Registration @@ -38,4 +36,3 @@ requires clients to explicitly provide a MeterProvider. This allows for: The MeterProvider should be configured with appropriate views and bounds as needed. */ package grpc - diff --git a/metrics/grpc/examples_test.go b/metrics/grpc/examples_test.go index 4222880c..a854e1fe 100644 --- a/metrics/grpc/examples_test.go +++ b/metrics/grpc/examples_test.go @@ -19,19 +19,16 @@ func (s *testServer) UnaryCall(ctx context.Context, req any) (any, error) { return nil, nil } -func ExampleNewClientMetricsFactory() { +func ExampleNewClientMetrics() { // Create a meter provider - in production, you would configure this // with appropriate exporters (e.g., Prometheus, OTLP) meterProvider := metric.NewMeterProvider() - // Create a client metrics factory with the meter provider - clientFactory := NewClientMetricsFactory( + // Create a client metrics dial option with the meter provider + dialOption := NewClientMetrics( WithMeterProvider(meterProvider), ) - // Use the factory to create a dial option - dialOption := clientFactory() - // Create a gRPC client connection with metrics enabled conn, err := grpc.NewClient( "localhost:50051", @@ -47,19 +44,16 @@ func ExampleNewClientMetricsFactory() { fmt.Println("Client created with metrics collection enabled") } -func ExampleNewServerMetricsFactory() { +func ExampleNewServerMetrics() { // Create a meter provider - in production, you would configure this // with appropriate exporters (e.g., Prometheus, OTLP) meterProvider := metric.NewMeterProvider() - // Create a server metrics factory with the meter provider - serverFactory := NewServerMetricsFactory( + // Create a server metrics option with the meter provider + serverOption := NewServerMetrics( WithMeterProvider(meterProvider), ) - // Use the factory to create a server option - serverOption := serverFactory() - // Create a gRPC server with metrics enabled server := grpc.NewServer(serverOption) defer server.Stop() @@ -71,23 +65,21 @@ func ExampleWithMeterProvider() { // Create a custom meter provider with specific configuration meterProvider := metric.NewMeterProvider() - // Create a client factory with the meter provider - clientFactory := NewClientMetricsFactory( + // Create a client dial option with the meter provider + dialOption := NewClientMetrics( WithMeterProvider(meterProvider), ) - - dialOption := clientFactory() _ = dialOption - fmt.Println("Client factory created with custom meter provider") + fmt.Println("Client dial option created with custom meter provider") } func ExampleWithMethodAttributeFilter() { meterProvider := metric.NewMeterProvider() - // Create a server factory with method attribute filtering + // Create a server option with method attribute filtering // This is useful to limit cardinality when using unknown service handlers - serverFactory := NewServerMetricsFactory( + serverOption := NewServerMetrics( WithMeterProvider(meterProvider), WithMethodAttributeFilter(func(method string) bool { // Only record metrics for specific methods @@ -96,25 +88,23 @@ func ExampleWithMethodAttributeFilter() { }), ) - serverOption := serverFactory() _ = serverOption - fmt.Println("Server factory created with method attribute filter") + fmt.Println("Server option created with method attribute filter") } func ExampleWithOptionalLabels() { meterProvider := metric.NewMeterProvider() - // Create a server factory with optional labels from load balancer policies - serverFactory := NewServerMetricsFactory( + // Create a server option with optional labels from load balancer policies + serverOption := NewServerMetrics( WithMeterProvider(meterProvider), WithOptionalLabels([]string{"zone", "instance"}), ) - serverOption := serverFactory() _ = serverOption - fmt.Println("Server factory created with optional labels") + fmt.Println("Server option created with optional labels") } func TestExampleClientServerIntegration(t *testing.T) { @@ -122,11 +112,11 @@ func TestExampleClientServerIntegration(t *testing.T) { meterProvider := metric.NewMeterProvider() // Create server with metrics - serverFactory := NewServerMetricsFactory( + serverOption := NewServerMetrics( WithMeterProvider(meterProvider), ) - server := grpc.NewServer(serverFactory()) + server := grpc.NewServer(serverOption) defer server.Stop() // Listen on a random port @@ -143,13 +133,13 @@ func TestExampleClientServerIntegration(t *testing.T) { }() // Create client with metrics - clientFactory := NewClientMetricsFactory( + dialOption := NewClientMetrics( WithMeterProvider(meterProvider), ) conn, err := grpc.NewClient( listener.Addr().String(), - clientFactory(), + dialOption, grpc.WithTransportCredentials(insecure.NewCredentials()), ) if err != nil { @@ -163,65 +153,97 @@ func TestExampleClientServerIntegration(t *testing.T) { } } -func TestExampleMultipleFactories(t *testing.T) { - // Demonstrate creating multiple factories with different configurations +func TestExampleMultipleMetricsOptions(t *testing.T) { + // Demonstrate creating multiple metrics options with different configurations mp1 := metric.NewMeterProvider() mp2 := metric.NewMeterProvider() - // Create first client factory - clientFactory1 := NewClientMetricsFactory( + // Create first client metrics option + dialOpt1 := NewClientMetrics( WithMeterProvider(mp1), ) - // Create second client factory with different configuration - clientFactory2 := NewClientMetricsFactory( + // Create second client metrics option with different configuration + dialOpt2 := NewClientMetrics( WithMeterProvider(mp2), WithMethodAttributeFilter(func(method string) bool { return true }), ) - opt1 := clientFactory1() - opt2 := clientFactory2() - - if opt1 == nil || opt2 == nil { + if dialOpt1 == nil || dialOpt2 == nil { t.Fatal("dial options are nil") } } -func TestExampleFactoryReusability(t *testing.T) { - // Demonstrate that factories can be reused to create multiple connections/servers +func TestExampleClientMetricsReusability(t *testing.T) { + // Demonstrate that the same dial option can be reused for multiple connections meterProvider := metric.NewMeterProvider() - clientFactory := NewClientMetricsFactory( + dialOption := NewClientMetrics( WithMeterProvider(meterProvider), ) - // Create multiple dial options from the same factory - dialOpt1 := clientFactory() - dialOpt2 := clientFactory() - dialOpt3 := clientFactory() + // Create multiple connections with the same dial option + conn1, err := grpc.NewClient( + "localhost:9999", + dialOption, + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + if err == nil { + defer conn1.Close() + } - if dialOpt1 == nil || dialOpt2 == nil || dialOpt3 == nil { - t.Fatal("dial options are nil") + conn2, err := grpc.NewClient( + "localhost:9998", + dialOption, + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + if err == nil { + defer conn2.Close() + } + + // The same dial option was used for both connections + if dialOption == nil { + t.Fatal("dial option is nil") + } +} + +func TestExampleServerMetricsReusability(t *testing.T) { + // Demonstrate that the same server option can be reused for multiple servers + meterProvider := metric.NewMeterProvider() + serverOption := NewServerMetrics( + WithMeterProvider(meterProvider), + ) + + // Create multiple servers with the same server option + server1 := grpc.NewServer(serverOption) + defer server1.Stop() + + server2 := grpc.NewServer(serverOption) + defer server2.Stop() + + // Both servers are using the same meter provider and metrics + if server1 == nil || server2 == nil { + t.Fatal("servers are nil") } } func TestExampleNoGlobalRegistry(t *testing.T) { // This test demonstrates that metrics are NOT registered in a global registry - // Instead, they are collected via the MeterProvider passed to the factory + // Instead, they are collected via the MeterProvider passed to the functions meterProvider := metric.NewMeterProvider() - // Create server factory - this does NOT register metrics globally - serverFactory := NewServerMetricsFactory( + // Create server option - this does NOT register metrics globally + serverOption := NewServerMetrics( WithMeterProvider(meterProvider), ) - // Create multiple servers with the same factory - server1 := grpc.NewServer(serverFactory()) + // Create multiple servers with the same server option + server1 := grpc.NewServer(serverOption) defer server1.Stop() - server2 := grpc.NewServer(serverFactory()) + server2 := grpc.NewServer(serverOption) defer server2.Stop() // Both servers are using the same meter provider, not a global registry diff --git a/metrics/grpc/options.go b/metrics/grpc/options.go index 776512ba..e66e20be 100644 --- a/metrics/grpc/options.go +++ b/metrics/grpc/options.go @@ -5,43 +5,43 @@ import ( "google.golang.org/grpc/experimental/stats" ) -// MetricsFactoryOption is used to configure a metrics factory. -type MetricsFactoryOption interface { - apply(*metricsFactoryConfig) +// MetricsOption is used to configure a metrics factory. +type MetricsOption interface { + apply(*metricsConfig) } -type metricsFactoryConfig struct { - meterProvider metric.MeterProvider - metrics *stats.Metrics - methodAttributeFilter func(string) bool - optionalLabels []string +type metricsConfig struct { + meterProvider metric.MeterProvider + metrics *stats.Metrics + methodAttributeFilter func(string) bool + optionalLabels []string } -type funcMetricsFactoryOption struct { - f func(*metricsFactoryConfig) +type funcMetricsOption struct { + f func(*metricsConfig) } -func (fdo funcMetricsFactoryOption) apply(do *metricsFactoryConfig) { +func (fdo funcMetricsOption) apply(do *metricsConfig) { fdo.f(do) } -func newFuncMetricsFactoryOption(f func(*metricsFactoryConfig)) funcMetricsFactoryOption { - return funcMetricsFactoryOption{ +func newFuncMetricsOption(f func(*metricsConfig)) funcMetricsOption { + return funcMetricsOption{ f: f, } } // WithMeterProvider sets the MeterProvider to use for creating instruments. // This is required for metrics collection. If not set, no metrics will be recorded. -func WithMeterProvider(mp metric.MeterProvider) MetricsFactoryOption { - return newFuncMetricsFactoryOption(func(c *metricsFactoryConfig) { +func WithMeterProvider(mp metric.MeterProvider) MetricsOption { + return newFuncMetricsOption(func(c *metricsConfig) { c.meterProvider = mp }) } // WithMetrics specifies which metrics to instrument. If not set, default metrics will be recorded. -func WithMetrics(m *stats.Metrics) MetricsFactoryOption { - return newFuncMetricsFactoryOption(func(c *metricsFactoryConfig) { +func WithMetrics(m *stats.Metrics) MetricsOption { + return newFuncMetricsOption(func(c *metricsConfig) { c.metrics = m }) } @@ -49,25 +49,24 @@ func WithMetrics(m *stats.Metrics) MetricsFactoryOption { // WithMethodAttributeFilter sets a filter for recording method names of RPCs. // This is useful to limit cardinality for unknown service handlers. // Only applies to server-side metrics. -func WithMethodAttributeFilter(filter func(string) bool) MetricsFactoryOption { - return newFuncMetricsFactoryOption(func(c *metricsFactoryConfig) { +func WithMethodAttributeFilter(filter func(string) bool) MetricsOption { + return newFuncMetricsOption(func(c *metricsConfig) { c.methodAttributeFilter = filter }) } // WithOptionalLabels sets labels received from LB Policies that should be added // to metrics that record after receiving incoming metadata. -func WithOptionalLabels(labels []string) MetricsFactoryOption { - return newFuncMetricsFactoryOption(func(c *metricsFactoryConfig) { +func WithOptionalLabels(labels []string) MetricsOption { + return newFuncMetricsOption(func(c *metricsConfig) { c.optionalLabels = labels }) } -func applyMetricsFactoryOptions(opts []MetricsFactoryOption) *metricsFactoryConfig { - config := &metricsFactoryConfig{} +func applyMetricsOptions(opts []MetricsOption) *metricsConfig { + config := &metricsConfig{} for _, opt := range opts { opt.apply(config) } return config } - diff --git a/metrics/grpc/server.go b/metrics/grpc/server.go new file mode 100644 index 00000000..58ad52f8 --- /dev/null +++ b/metrics/grpc/server.go @@ -0,0 +1,34 @@ +package grpc + +import ( + "google.golang.org/grpc" + "google.golang.org/grpc/stats/opentelemetry" +) + +// NewServerMetrics creates a gRPC server option for server-side metrics collection. +// The returned ServerOption enables OpenTelemetry instrumentation for a gRPC server. +// +// The MeterProvider must be provided via WithMeterProvider option, otherwise +// no metrics will be recorded. +// +// Example: +// +// serverOpt := NewServerMetrics( +// WithMeterProvider(meterProvider), +// ) +// server := grpc.NewServer(serverOpt) +func NewServerMetrics(opts ...MetricsOption) grpc.ServerOption { + config := applyMetricsOptions(opts) + + // Create the OpenTelemetry options + otelOpts := opentelemetry.Options{ + MetricsOptions: opentelemetry.MetricsOptions{ + MeterProvider: config.meterProvider, + Metrics: config.metrics, + MethodAttributeFilter: config.methodAttributeFilter, + OptionalLabels: config.optionalLabels, + }, + } + + return opentelemetry.ServerOption(otelOpts) +} diff --git a/metrics/grpc/server_factory.go b/metrics/grpc/server_factory.go deleted file mode 100644 index b368385e..00000000 --- a/metrics/grpc/server_factory.go +++ /dev/null @@ -1,42 +0,0 @@ -package grpc - -import ( - "google.golang.org/grpc" - "google.golang.org/grpc/stats/opentelemetry" -) - -// ServerMetricsFactory creates a gRPC server option for server-side metrics collection. -// The returned function can be called multiple times to obtain the same server option. -type ServerMetricsFactory func() grpc.ServerOption - -// NewServerMetricsFactory creates a factory for gRPC server metrics. -// The factory returns a server option that enables OpenTelemetry instrumentation -// for a gRPC server. -// -// The MeterProvider must be provided via WithMeterProvider option, otherwise -// no metrics will be recorded. -// -// Example: -// -// factory := NewServerMetricsFactory( -// WithMeterProvider(meterProvider), -// ) -// server := grpc.NewServer(factory()) -func NewServerMetricsFactory(opts ...MetricsFactoryOption) ServerMetricsFactory { - config := applyMetricsFactoryOptions(opts) - - // Create the OpenTelemetry options - otelOpts := opentelemetry.Options{ - MetricsOptions: opentelemetry.MetricsOptions{ - MeterProvider: config.meterProvider, - Metrics: config.metrics, - MethodAttributeFilter: config.methodAttributeFilter, - OptionalLabels: config.optionalLabels, - }, - } - - return func() grpc.ServerOption { - return opentelemetry.ServerOption(otelOpts) - } -} - diff --git a/metrics/grpc/server_factory_test.go b/metrics/grpc/server_test.go similarity index 50% rename from metrics/grpc/server_factory_test.go rename to metrics/grpc/server_test.go index 919720ab..d81de997 100644 --- a/metrics/grpc/server_factory_test.go +++ b/metrics/grpc/server_test.go @@ -8,25 +8,25 @@ import ( "google.golang.org/grpc" ) -func TestNewServerMetricsFactory(t *testing.T) { +func TestNewServerMetrics(t *testing.T) { tests := []struct { name string - opts []MetricsFactoryOption + opts []MetricsOption wantErr bool }{ { - name: "factory without meter provider", - opts: []MetricsFactoryOption{}, + name: "metrics without meter provider", + opts: []MetricsOption{}, }, { - name: "factory with meter provider", - opts: []MetricsFactoryOption{ + name: "metrics with meter provider", + opts: []MetricsOption{ WithMeterProvider(metric.NewMeterProvider()), }, }, { - name: "factory with method attribute filter", - opts: []MetricsFactoryOption{ + name: "metrics with method attribute filter", + opts: []MetricsOption{ WithMeterProvider(metric.NewMeterProvider()), WithMethodAttributeFilter(func(method string) bool { return method != "/test.Service/Test" @@ -37,31 +37,15 @@ func TestNewServerMetricsFactory(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - factory := NewServerMetricsFactory(tt.opts...) - assert.NotNil(t, factory) - - serverOpt := factory() + serverOpt := NewServerMetrics(tt.opts...) assert.NotNil(t, serverOpt) }) } } -func TestServerMetricsFactory_MultipleCallsReturnSameOption(t *testing.T) { - mp := metric.NewMeterProvider() - factory := NewServerMetricsFactory(WithMeterProvider(mp)) - - opt1 := factory() - opt2 := factory() - - assert.NotNil(t, opt1) - assert.NotNil(t, opt2) -} - -func TestServerMetricsFactory_CanCreateServer(t *testing.T) { +func TestServerMetrics_CanCreateServer(t *testing.T) { mp := metric.NewMeterProvider() - factory := NewServerMetricsFactory(WithMeterProvider(mp)) - - serverOpt := factory() + serverOpt := NewServerMetrics(WithMeterProvider(mp)) assert.NotNil(t, serverOpt) // Verify it's a valid grpc.ServerOption by attempting to use it @@ -70,10 +54,10 @@ func TestServerMetricsFactory_CanCreateServer(t *testing.T) { server.Stop() } -func TestServerMetricsFactory_WithAllOptions(t *testing.T) { +func TestServerMetrics_WithAllOptions(t *testing.T) { mp := metric.NewMeterProvider() - factory := NewServerMetricsFactory( + serverOpt := NewServerMetrics( WithMeterProvider(mp), WithMethodAttributeFilter(func(method string) bool { return true @@ -81,7 +65,6 @@ func TestServerMetricsFactory_WithAllOptions(t *testing.T) { WithOptionalLabels([]string{"label1", "label2"}), ) - serverOpt := factory() assert.NotNil(t, serverOpt) server := grpc.NewServer(serverOpt) @@ -89,18 +72,17 @@ func TestServerMetricsFactory_WithAllOptions(t *testing.T) { server.Stop() } -func TestServerMetricsFactory_MultipleServers(t *testing.T) { +func TestServerMetrics_MultipleServers(t *testing.T) { mp := metric.NewMeterProvider() - factory := NewServerMetricsFactory(WithMeterProvider(mp)) + serverOpt := NewServerMetrics(WithMeterProvider(mp)) - // Create multiple servers with the same factory - server1 := grpc.NewServer(factory()) + // Create multiple servers with the same option + server1 := grpc.NewServer(serverOpt) defer server1.Stop() - server2 := grpc.NewServer(factory()) + server2 := grpc.NewServer(serverOpt) defer server2.Stop() assert.NotNil(t, server1) assert.NotNil(t, server2) } - -- GitLab From 82bf1c61cfb55dd4ef024cccfe527baf5a2af895 Mon Sep 17 00:00:00 2001 From: Vladimir Glafirov Date: Thu, 23 Oct 2025 13:22:04 +0200 Subject: [PATCH 3/5] options --- metrics/grpc/client.go | 5 +---- metrics/grpc/client_test.go | 23 ----------------------- metrics/grpc/examples_test.go | 9 --------- metrics/grpc/options.go | 30 +----------------------------- metrics/grpc/server.go | 5 +---- metrics/grpc/server_test.go | 7 ------- 6 files changed, 3 insertions(+), 76 deletions(-) diff --git a/metrics/grpc/client.go b/metrics/grpc/client.go index 30acf33e..97c6b381 100644 --- a/metrics/grpc/client.go +++ b/metrics/grpc/client.go @@ -23,10 +23,7 @@ func NewClientMetrics(opts ...MetricsOption) grpc.DialOption { // Create the OpenTelemetry options otelOpts := opentelemetry.Options{ MetricsOptions: opentelemetry.MetricsOptions{ - MeterProvider: config.meterProvider, - Metrics: config.metrics, - MethodAttributeFilter: config.methodAttributeFilter, - OptionalLabels: config.optionalLabels, + MeterProvider: config.meterProvider, }, } diff --git a/metrics/grpc/client_test.go b/metrics/grpc/client_test.go index f1bf96ff..c23a299a 100644 --- a/metrics/grpc/client_test.go +++ b/metrics/grpc/client_test.go @@ -25,15 +25,6 @@ func TestNewClientMetrics(t *testing.T) { WithMeterProvider(metric.NewMeterProvider()), }, }, - { - name: "metrics with method attribute filter", - opts: []MetricsOption{ - WithMeterProvider(metric.NewMeterProvider()), - WithMethodAttributeFilter(func(method string) bool { - return method != "/test.Service/Test" - }), - }, - }, } for _, tt := range tests { @@ -63,17 +54,3 @@ func TestClientMetrics_CanCreateConnection(t *testing.T) { defer cc.Close() } } - -func TestClientMetrics_WithAllOptions(t *testing.T) { - mp := metric.NewMeterProvider() - - dialOpt := NewClientMetrics( - WithMeterProvider(mp), - WithMethodAttributeFilter(func(method string) bool { - return true - }), - WithOptionalLabels([]string{"label1", "label2"}), - ) - - assert.NotNil(t, dialOpt) -} diff --git a/metrics/grpc/examples_test.go b/metrics/grpc/examples_test.go index a854e1fe..4622f4d3 100644 --- a/metrics/grpc/examples_test.go +++ b/metrics/grpc/examples_test.go @@ -81,11 +81,6 @@ func ExampleWithMethodAttributeFilter() { // This is useful to limit cardinality when using unknown service handlers serverOption := NewServerMetrics( WithMeterProvider(meterProvider), - WithMethodAttributeFilter(func(method string) bool { - // Only record metrics for specific methods - // This prevents high cardinality issues with dynamic methods - return method != "/grpc.health.v1.Health/Check" - }), ) _ = serverOption @@ -99,7 +94,6 @@ func ExampleWithOptionalLabels() { // Create a server option with optional labels from load balancer policies serverOption := NewServerMetrics( WithMeterProvider(meterProvider), - WithOptionalLabels([]string{"zone", "instance"}), ) _ = serverOption @@ -166,9 +160,6 @@ func TestExampleMultipleMetricsOptions(t *testing.T) { // Create second client metrics option with different configuration dialOpt2 := NewClientMetrics( WithMeterProvider(mp2), - WithMethodAttributeFilter(func(method string) bool { - return true - }), ) if dialOpt1 == nil || dialOpt2 == nil { diff --git a/metrics/grpc/options.go b/metrics/grpc/options.go index e66e20be..66fe0308 100644 --- a/metrics/grpc/options.go +++ b/metrics/grpc/options.go @@ -2,7 +2,6 @@ package grpc import ( "go.opentelemetry.io/otel/metric" - "google.golang.org/grpc/experimental/stats" ) // MetricsOption is used to configure a metrics factory. @@ -11,10 +10,7 @@ type MetricsOption interface { } type metricsConfig struct { - meterProvider metric.MeterProvider - metrics *stats.Metrics - methodAttributeFilter func(string) bool - optionalLabels []string + meterProvider metric.MeterProvider } type funcMetricsOption struct { @@ -39,30 +35,6 @@ func WithMeterProvider(mp metric.MeterProvider) MetricsOption { }) } -// WithMetrics specifies which metrics to instrument. If not set, default metrics will be recorded. -func WithMetrics(m *stats.Metrics) MetricsOption { - return newFuncMetricsOption(func(c *metricsConfig) { - c.metrics = m - }) -} - -// WithMethodAttributeFilter sets a filter for recording method names of RPCs. -// This is useful to limit cardinality for unknown service handlers. -// Only applies to server-side metrics. -func WithMethodAttributeFilter(filter func(string) bool) MetricsOption { - return newFuncMetricsOption(func(c *metricsConfig) { - c.methodAttributeFilter = filter - }) -} - -// WithOptionalLabels sets labels received from LB Policies that should be added -// to metrics that record after receiving incoming metadata. -func WithOptionalLabels(labels []string) MetricsOption { - return newFuncMetricsOption(func(c *metricsConfig) { - c.optionalLabels = labels - }) -} - func applyMetricsOptions(opts []MetricsOption) *metricsConfig { config := &metricsConfig{} for _, opt := range opts { diff --git a/metrics/grpc/server.go b/metrics/grpc/server.go index 58ad52f8..ea5d5dbe 100644 --- a/metrics/grpc/server.go +++ b/metrics/grpc/server.go @@ -23,10 +23,7 @@ func NewServerMetrics(opts ...MetricsOption) grpc.ServerOption { // Create the OpenTelemetry options otelOpts := opentelemetry.Options{ MetricsOptions: opentelemetry.MetricsOptions{ - MeterProvider: config.meterProvider, - Metrics: config.metrics, - MethodAttributeFilter: config.methodAttributeFilter, - OptionalLabels: config.optionalLabels, + MeterProvider: config.meterProvider, }, } diff --git a/metrics/grpc/server_test.go b/metrics/grpc/server_test.go index d81de997..6bc3dc0b 100644 --- a/metrics/grpc/server_test.go +++ b/metrics/grpc/server_test.go @@ -28,9 +28,6 @@ func TestNewServerMetrics(t *testing.T) { name: "metrics with method attribute filter", opts: []MetricsOption{ WithMeterProvider(metric.NewMeterProvider()), - WithMethodAttributeFilter(func(method string) bool { - return method != "/test.Service/Test" - }), }, }, } @@ -59,10 +56,6 @@ func TestServerMetrics_WithAllOptions(t *testing.T) { serverOpt := NewServerMetrics( WithMeterProvider(mp), - WithMethodAttributeFilter(func(method string) bool { - return true - }), - WithOptionalLabels([]string{"label1", "label2"}), ) assert.NotNil(t, serverOpt) -- GitLab From 494a69bdbf76b58e882011f4a229d54a286738bc Mon Sep 17 00:00:00 2001 From: Vladimir Glafirov Date: Thu, 23 Oct 2025 14:47:53 +0200 Subject: [PATCH 4/5] removed examples --- metrics/grpc/examples_test.go | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/metrics/grpc/examples_test.go b/metrics/grpc/examples_test.go index 4622f4d3..60607db5 100644 --- a/metrics/grpc/examples_test.go +++ b/metrics/grpc/examples_test.go @@ -74,33 +74,6 @@ func ExampleWithMeterProvider() { fmt.Println("Client dial option created with custom meter provider") } -func ExampleWithMethodAttributeFilter() { - meterProvider := metric.NewMeterProvider() - - // Create a server option with method attribute filtering - // This is useful to limit cardinality when using unknown service handlers - serverOption := NewServerMetrics( - WithMeterProvider(meterProvider), - ) - - _ = serverOption - - fmt.Println("Server option created with method attribute filter") -} - -func ExampleWithOptionalLabels() { - meterProvider := metric.NewMeterProvider() - - // Create a server option with optional labels from load balancer policies - serverOption := NewServerMetrics( - WithMeterProvider(meterProvider), - ) - - _ = serverOption - - fmt.Println("Server option created with optional labels") -} - func TestExampleClientServerIntegration(t *testing.T) { // This test demonstrates client and server metrics working together meterProvider := metric.NewMeterProvider() -- GitLab From 0e57841e9759e8bd1cfdfbcde8aaea055cabf532 Mon Sep 17 00:00:00 2001 From: Vladimir Glafirov Date: Thu, 23 Oct 2025 15:28:18 +0200 Subject: [PATCH 5/5] lint --- metrics/grpc/examples_test.go | 8 -------- metrics/grpc/options.go | 2 +- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/metrics/grpc/examples_test.go b/metrics/grpc/examples_test.go index 60607db5..9c4a99f3 100644 --- a/metrics/grpc/examples_test.go +++ b/metrics/grpc/examples_test.go @@ -1,7 +1,6 @@ package grpc import ( - "context" "fmt" "log" "net" @@ -12,13 +11,6 @@ import ( "google.golang.org/grpc/credentials/insecure" ) -// Mock gRPC service for testing -type testServer struct{} - -func (s *testServer) UnaryCall(ctx context.Context, req any) (any, error) { - return nil, nil -} - func ExampleNewClientMetrics() { // Create a meter provider - in production, you would configure this // with appropriate exporters (e.g., Prometheus, OTLP) diff --git a/metrics/grpc/options.go b/metrics/grpc/options.go index 66fe0308..fc2d8d20 100644 --- a/metrics/grpc/options.go +++ b/metrics/grpc/options.go @@ -6,7 +6,7 @@ import ( // MetricsOption is used to configure a metrics factory. type MetricsOption interface { - apply(*metricsConfig) + apply(c *metricsConfig) } type metricsConfig struct { -- GitLab