Repository: grpc-ecosystem/go-grpc-middleware
Pull request #643 (2023-09-18 08:15:07)

Support for subsystem in grpc prometheus counter and histogram metrics
## Changes

Introduction of apis: WithSubsystem() and WithHistogramSubsystem()


## Verification
Added new test cases for client and server metrics both.

cd /home/ubuntu/datadisk/CLONES/go-grpc-middleware-subsystem/providers/prometheus && go test -v -race ./...
=== RUN TestClientInterceptorSuite
=== RUN TestClientInterceptorSuite/TestStartedStreamingIncrementsStarted
=== RUN TestClientInterceptorSuite/TestStreamingIncrementsMetrics
=== RUN TestClientInterceptorSuite/TestUnaryIncrementsMetrics
=== RUN TestClientInterceptorSuite/TestWithSubsystem
=== NAME TestClientInterceptorSuite
interceptor_suite.go:154: stopped grpc.Server at: 127.0.0.1:40133
--- PASS: TestClientInterceptorSuite (0.24s)
--- PASS: TestClientInterceptorSuite/TestStartedStreamingIncrementsStarted (0.00s)
--- PASS: TestClientInterceptorSuite/TestStreamingIncrementsMetrics (0.01s)
--- PASS: TestClientInterceptorSuite/TestUnaryIncrementsMetrics (0.00s)
--- PASS: TestClientInterceptorSuite/TestWithSubsystem (0.00s) <<<<<<<<
=== RUN TestServerInterceptorSuite
=== RUN TestServerInterceptorSuite/TestContextCancelledTreatedAsStatus
=== RUN TestServerInterceptorSuite/TestRegisterPresetsStuff
=== RUN TestServerInterceptorSuite/TestStartedStreamingIncrementsStarted
=== RUN TestServerInterceptorSuite/TestStreamingIncrementsMetrics
=== RUN TestServerInterceptorSuite/TestUnaryIncrementsMetrics
=== RUN TestServerInterceptorSuite/TestWithSubsystem
=== NAME TestServerInterceptorSuite
interceptor_suite.go:154: stopped grpc.Server at: 127.0.0.1:38755
--- PASS: TestServerInterceptorSuite (0.73s)
--- PASS: TestServerInterceptorSuite/TestContextCancelledTreatedAsStatus (0.10s)
--- PASS: TestServerInterceptorSuite/TestRegisterPresetsStuff (0.03s)
--- PASS: TestServerInterceptorSuite/TestStartedStreamingIncrementsStarted (0.20s)
--- PASS: TestServerInterceptorSuite/TestStreamingIncrementsMetrics (0.11s)
--- PASS: TestServerInterceptorSuite/TestUnaryIncrementsMetrics (0.00s)
--- PASS: TestServerInterceptorSuite/TestWithSubsystem (0.00s) <<<<<<<<
PASS
ok github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus 1.006s


johanbrandhorst: Just giving Bartek a chance to review as well before merge.

Patch:
diff --git a/README.md b/README.md
index 5f17174..8c4ac16 100644
--- a/README.md
+++ b/README.md
@@ -54,6 +54,7 @@ All paths should work with `go get <path>`.
   * [`github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus`⚡](providers/prometheus) - Prometheus client-side and server-side monitoring middleware. Supports exemplars. Moved from deprecated now [`go-grpc-prometheus`](https://github.com/grpc-ecosystem/go-grpc-prometheus).
   * (external) [`go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc`](https://go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc) - official OpenTelemetry interceptors (metric and tracing).
 * Logging with [`github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/logging`](interceptors/logging) - a customizable logging middleware offering extended per request logging. It requires logging adapter, see examples in [`interceptors/logging/examples`](interceptors/logging/examples) for `go-kit`, `log`, `logr`, `logrus`, `slog`, `zap` and `zerolog`.
+  * NOTE: Interceptors with [context](https://pkg.go.dev/context) field injections need to be chained before the adapter function.
 * Tracing:
   * (external) [`go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc`](https://go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc) - official OpenTelemetry interceptors (metric and tracing) as used in [example](examples).
   * (external) [`github.com/grpc-ecosystem/go-grpc-middleware/tracing/opentracing`](https://pkg.go.dev/github.com/grpc-ecosystem/go-grpc-middleware@v1.4.0/tracing/opentracing) - deprecated [OpenTracing](http://opentracing.io/) client-side and server-side interceptors if you still need it!
diff --git a/interceptors/logging/examples/go.mod b/interceptors/logging/examples/go.mod
index 49d4a3e..984e9c4 100644
--- a/interceptors/logging/examples/go.mod
+++ b/interceptors/logging/examples/go.mod
@@ -10,7 +10,7 @@ require (
 	github.com/rs/zerolog v1.29.0
 	github.com/sirupsen/logrus v1.9.0
 	go.uber.org/zap v1.24.0
-	golang.org/x/exp v0.0.0-20230321023759-10a507213a29
+	golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
 	google.golang.org/grpc v1.53.0
 	k8s.io/klog/v2 v2.90.1
 )
@@ -22,11 +22,11 @@ require (
 	github.com/mattn/go-isatty v0.0.14 // indirect
 	go.uber.org/atomic v1.7.0 // indirect
 	go.uber.org/multierr v1.6.0 // indirect
-	golang.org/x/net v0.8.0 // indirect
-	golang.org/x/sys v0.6.0 // indirect
-	golang.org/x/text v0.8.0 // indirect
+	golang.org/x/net v0.14.0 // indirect
+	golang.org/x/sys v0.11.0 // indirect
+	golang.org/x/text v0.12.0 // indirect
 	google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect
-	google.golang.org/protobuf v1.30.0 // indirect
+	google.golang.org/protobuf v1.31.0 // indirect
 )
 
 replace github.com/grpc-ecosystem/go-grpc-middleware/v2 => ../../../
diff --git a/interceptors/logging/examples/go.sum b/interceptors/logging/examples/go.sum
index 04c9054..ff873eb 100644
--- a/interceptors/logging/examples/go.sum
+++ b/interceptors/logging/examples/go.sum
@@ -44,15 +44,19 @@ go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
 go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
 golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug=
 golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
+golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
 golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
 golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
+golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
 golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f h1:BWUVssLB0HVOSY78gIdvk1dTVYtT1y8SBWtPYuTJ/6w=
 google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
@@ -62,6 +66,7 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
 google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
diff --git a/interceptors/logging/interceptors.go b/interceptors/logging/interceptors.go
index 7161dac..125028c 100644
--- a/interceptors/logging/interceptors.go
+++ b/interceptors/logging/interceptors.go
@@ -142,7 +142,12 @@ func reportable(logger Logger, opts *options) interceptors.CommonReportableFunc
 		}
 
 		// Field dups from context override the common fields.
-		fields := newCommonFields(kind, c).WithUnique(ExtractFields(ctx))
+		fields := newCommonFields(kind, c)
+		if opts.disableGrpcLogFields != nil {
+			fields = disableCommonLoggingFields(kind, c, opts.disableGrpcLogFields)
+		}
+		fields = fields.WithUnique(ExtractFields(ctx))
+
 		if !c.IsClient {
 			if peer, ok := peer.FromContext(ctx); ok {
 				fields = append(fields, "peer.address", peer.Addr.String())
diff --git a/interceptors/logging/logging.go b/interceptors/logging/logging.go
index 4b02478..510344f 100644
--- a/interceptors/logging/logging.go
+++ b/interceptors/logging/logging.go
@@ -38,6 +38,21 @@ func newCommonFields(kind string, c interceptors.CallMeta) Fields {
 	}
 }
 
+// disableCommonLoggingFields returns copy of newCommonFields with disabled fields removed from the following
+// default list. The following are the default logging fields:
+//   - SystemTag[0]
+//   - ComponentFieldKey
+//   - ServiceFieldKey
+//   - MethodFieldKey
+//   - MethodTypeFieldKey
+func disableCommonLoggingFields(kind string, c interceptors.CallMeta, disableFields []string) Fields {
+	commonFields := newCommonFields(kind, c)
+	for _, key := range disableFields {
+		commonFields.Delete(key)
+	}
+	return commonFields
+}
+
 // Fields loosely represents key value pairs that adds context to log lines. The key has to be type of string, whereas
 // value can be an arbitrary object.
 type Fields []any
@@ -76,6 +91,18 @@ func (i *iter) At() (k string, v any) {
 	return i.f[i.i].(string), i.f[i.i+1]
 }
 
+func (f *Fields) Delete(key string) {
+	i := f.Iterator()
+	for i.Next() {
+		k, _ := i.At()
+		if k != key {
+			continue
+		}
+		*f = append((*f)[:i.i], (*f)[i.i+2:]...)
+		return
+	}
+}
+
 // WithUnique returns copy of fields which is the union of all unique keys.
 // Any duplicates in the added or current fields will be deduplicated where first occurrence takes precedence.
 func (f Fields) WithUnique(add Fields) Fields {
diff --git a/interceptors/logging/options.go b/interceptors/logging/options.go
index 4b9d082..5308d8b 100644
--- a/interceptors/logging/options.go
+++ b/interceptors/logging/options.go
@@ -48,8 +48,9 @@ var (
 		codeFunc:          DefaultErrorToCode,
 		durationFieldFunc: DefaultDurationToFields,
 		// levelFunc depends if it's client or server.
-		levelFunc:       nil,
-		timestampFormat: time.RFC3339,
+		levelFunc:            nil,
+		timestampFormat:      time.RFC3339,
+		disableGrpcLogFields: nil,
 	}
 )
 
@@ -60,6 +61,7 @@ type options struct {
 	durationFieldFunc       DurationToFields
 	timestampFormat         string
 	fieldsFromCtxCallMetaFn fieldsFromCtxCallMetaFn
+	disableGrpcLogFields    []string
 }
 
 type Option func(*options)
@@ -204,3 +206,10 @@ func WithTimestampFormat(format string) Option {
 		o.timestampFormat = format
 	}
 }
+
+// WithDisableLoggingFields disables logging of gRPC fields provided.
+func WithDisableLoggingFields(disableGrpcLogFields ...string) Option {
+	return func(o *options) {
+		o.disableGrpcLogFields = disableGrpcLogFields
+	}
+}
diff --git a/interceptors/retry/retry.go b/interceptors/retry/retry.go
index 2abdb43..9ea2e80 100644
--- a/interceptors/retry/retry.go
+++ b/interceptors/retry/retry.go
@@ -94,10 +94,8 @@ func StreamClientInterceptor(optFuncs ...CallOption) grpc.StreamClientIntercepto
 			if err := waitRetryBackoff(attempt, parentCtx, callOpts); err != nil {
 				return nil, err
 			}
-			callCtx, cancel := perCallContext(parentCtx, callOpts, 0)
-			defer cancel() // Clean up potential resources.
 			var newStreamer grpc.ClientStream
-			newStreamer, lastErr = streamer(callCtx, desc, cc, method, grpcOpts...)
+			newStreamer, lastErr = streamer(parentCtx, desc, cc, method, grpcOpts...)
 			if lastErr == nil {
 				retryingStreamer := &serverStreamingRetryingStream{
 					ClientStream: newStreamer,
@@ -188,9 +186,7 @@ func (s *serverStreamingRetryingStream) RecvMsg(m any) error {
 			return err
 		}
 		s.callOpts.onRetryCallback(s.parentCtx, attempt, lastErr)
-		// TODO(bwplotka): Close cancel as it might leak some resources.
-		callCtx, _ := perCallContext(s.parentCtx, s.callOpts, attempt) //nolint
-		newStream, err := s.reestablishStreamAndResendBuffer(callCtx)
+		newStream, err := s.reestablishStreamAndResendBuffer(s.parentCtx)
 		if err != nil {
 			// Retry dial and transport errors of establishing stream as grpc doesn't retry.
 			if isRetriable(err, s.callOpts) {
diff --git a/providers/prometheus/options.go b/providers/prometheus/options.go
index 2a47d4b..bdd171e 100644
--- a/providers/prometheus/options.go
+++ b/providers/prometheus/options.go
@@ -39,6 +39,13 @@ func WithConstLabels(labels prometheus.Labels) CounterOption {
 	}
 }
 
+// WithSubsystem allows you to add a Subsystem to Counter metrics.
+func WithSubsystem(subsystem string) CounterOption {
+	return func(o *prometheus.CounterOpts) {
+		o.Subsystem = subsystem
+	}
+}
+
 // A HistogramOption lets you add options to Histogram metrics using With*
 // funcs.
 type HistogramOption func(*prometheus.HistogramOpts)
@@ -81,6 +88,13 @@ func WithHistogramConstLabels(labels prometheus.Labels) HistogramOption {
 	}
 }
 
+// WithHistogramSubsystem allows you to add a Subsystem to histograms metrics.
+func WithHistogramSubsystem(subsystem string) HistogramOption {
+	return func(o *prometheus.HistogramOpts) {
+		o.Subsystem = subsystem
+	}
+}
+
 func typeFromMethodInfo(mInfo *grpc.MethodInfo) grpcType {
 	if !mInfo.IsClientStream && !mInfo.IsServerStream {
 		return Unary


Test patch:
diff --git a/interceptors/logging/interceptors_test.go b/interceptors/logging/interceptors_test.go
index 3dbc693..85eb8a6 100644
--- a/interceptors/logging/interceptors_test.go
+++ b/interceptors/logging/interceptors_test.go
@@ -630,6 +630,149 @@ func (s *loggingPayloadSuite) assertPayloadLogLinesForMessage(lines LogLines, me
 	}
 }
 
+type loggingCustomGrpcLogFieldsSuite struct {
+	*baseLoggingSuite
+}
+
+func TestCustomGrpcLogFieldsSuite(t *testing.T) {
+	if strings.HasPrefix(runtime.Version(), "go1.7") {
+		t.Skipf("Skipping due to json.RawMessage incompatibility with go1.7")
+		return
+	}
+	s := &loggingCustomGrpcLogFieldsSuite{
+		baseLoggingSuite: &baseLoggingSuite{
+			logger: newMockLogger(),
+			InterceptorTestSuite: &testpb.InterceptorTestSuite{
+				TestService: &testpb.TestPingService{},
+			},
+		},
+	}
+	s.InterceptorTestSuite.ClientOpts = []grpc.DialOption{
+		grpc.WithUnaryInterceptor(logging.UnaryClientInterceptor(s.logger, logging.WithDisableLoggingFields(logging.ComponentFieldKey, logging.MethodTypeFieldKey, logging.SystemTag[0], "custom-field-should-be-ignored"))),
+		grpc.WithStreamInterceptor(logging.StreamClientInterceptor(s.logger, logging.WithDisableLoggingFields(logging.ComponentFieldKey, logging.MethodTypeFieldKey, logging.SystemTag[0], "custom-field-should-be-ignored"))),
+	}
+	s.InterceptorTestSuite.ServerOpts = []grpc.ServerOption{
+		grpc.StreamInterceptor(logging.StreamServerInterceptor(s.logger, logging.WithDisableLoggingFields(logging.ComponentFieldKey, logging.MethodTypeFieldKey, logging.SystemTag[0], "custom-field-should-be-ignored"))),
+		grpc.UnaryInterceptor(logging.UnaryServerInterceptor(s.logger, logging.WithDisableLoggingFields(logging.ComponentFieldKey, logging.MethodTypeFieldKey, logging.SystemTag[0], "custom-field-should-be-ignore"))),
+	}
+	suite.Run(t, s)
+}
+
+// Test that fields are added to logs using withGrpcLogFields.
+func (s *loggingCustomGrpcLogFieldsSuite) TestCustomGrpcLogFieldsWithPing() {
+	_, err := s.Client.Ping(s.SimpleCtx(), testpb.GoodPing)
+	assert.NoError(s.T(), err, "there must be not be an on a successful call")
+
+	lines := s.logger.o.Lines()
+	sort.Sort(lines)
+	require.Len(s.T(), lines, 4)
+
+	clientStartCallLogLine := lines[2]
+	assert.Equal(s.T(), logging.LevelDebug, clientStartCallLogLine.lvl)
+	assert.Equal(s.T(), "started call", clientStartCallLogLine.msg)
+	clientStartCallFields := clientStartCallLogLine.fields
+	clientStartCallFields.AssertFieldNotEmpty(s.T(), "grpc.start_time").
+		AssertFieldNotEmpty(s.T(), "grpc.request.deadline").
+		AssertFieldNotEmpty(s.T(), "grpc.time_ms").
+		AssertField(s.T(), logging.MethodFieldKey, "Ping").
+		AssertField(s.T(), logging.ServiceFieldKey, testpb.TestServiceFullName).AssertNoMoreTags(s.T())
+
+	serverStartCallLogLine := lines[3]
+	assert.Equal(s.T(), logging.LevelInfo, serverStartCallLogLine.lvl)
+	assert.Equal(s.T(), "started call", serverStartCallLogLine.msg)
+	serverStartCallFields := serverStartCallLogLine.fields
+	serverStartCallFields.AssertFieldNotEmpty(s.T(), "peer.address").
+		AssertFieldNotEmpty(s.T(), "grpc.start_time").
+		AssertFieldNotEmpty(s.T(), "grpc.request.deadline").
+		AssertFieldNotEmpty(s.T(), "grpc.time_ms").
+		AssertField(s.T(), logging.MethodFieldKey, "Ping").
+		AssertField(s.T(), logging.ServiceFieldKey, testpb.TestServiceFullName).AssertNoMoreTags(s.T())
+
+	serverFinishCallLogLine := lines[0]
+	assert.Equal(s.T(), logging.LevelInfo, serverFinishCallLogLine.lvl)
+	assert.Equal(s.T(), "finished call", serverFinishCallLogLine.msg)
+	serverFinishCallFields := serverFinishCallLogLine.fields
+	serverFinishCallFields.AssertFieldNotEmpty(s.T(), "peer.address").
+		AssertFieldNotEmpty(s.T(), "grpc.start_time").
+		AssertFieldNotEmpty(s.T(), "grpc.request.deadline").
+		AssertField(s.T(), "grpc.code", "OK").
+		AssertFieldNotEmpty(s.T(), "grpc.time_ms").
+		AssertField(s.T(), logging.MethodFieldKey, "Ping").
+		AssertField(s.T(), logging.ServiceFieldKey, testpb.TestServiceFullName).AssertNoMoreTags(s.T())
+
+	clientFinishCallLogLine := lines[1]
+	assert.Equal(s.T(), logging.LevelDebug, clientFinishCallLogLine.lvl)
+	assert.Equal(s.T(), "finished call", clientFinishCallLogLine.msg)
+
+	clientFinishCallFields := clientFinishCallLogLine.fields
+	clientFinishCallFields.AssertFieldNotEmpty(s.T(), "grpc.start_time").
+		AssertFieldNotEmpty(s.T(), "grpc.request.deadline").
+		AssertField(s.T(), "grpc.code", "OK").
+		AssertFieldNotEmpty(s.T(), "grpc.time_ms").
+		AssertField(s.T(), logging.MethodFieldKey, "Ping").
+		AssertField(s.T(), logging.ServiceFieldKey, testpb.TestServiceFullName).AssertNoMoreTags(s.T())
+}
+
+func (s *loggingCustomGrpcLogFieldsSuite) TestCustomGrpcLogFieldsWithPingList() {
+	stream, err := s.Client.PingList(s.SimpleCtx(), testpb.GoodPingList)
+	require.NoError(s.T(), err, "should not fail on establishing the stream")
+	for {
+		_, err := stream.Recv()
+		if err == io.EOF {
+			break
+		}
+		require.NoError(s.T(), err, "reading stream should not fail")
+	}
+	lines := s.logger.o.Lines()
+	sort.Sort(lines)
+	require.Len(s.T(), lines, 4)
+
+	clientStartCallLogLine := lines[2]
+	assert.Equal(s.T(), logging.LevelDebug, clientStartCallLogLine.lvl)
+	assert.Equal(s.T(), "started call", clientStartCallLogLine.msg)
+	clientStartCallFields := clientStartCallLogLine.fields
+	clientStartCallFields.AssertFieldNotEmpty(s.T(), "grpc.start_time").
+		AssertFieldNotEmpty(s.T(), "grpc.request.deadline").
+		AssertFieldNotEmpty(s.T(), "grpc.time_ms").
+		AssertField(s.T(), logging.MethodFieldKey, "PingList").
+		AssertField(s.T(), logging.ServiceFieldKey, testpb.TestServiceFullName).AssertNoMoreTags(s.T())
+
+	serverStartCallLogLine := lines[3]
+	assert.Equal(s.T(), logging.LevelInfo, serverStartCallLogLine.lvl)
+	assert.Equal(s.T(), "started call", serverStartCallLogLine.msg)
+	serverStartCallFields := serverStartCallLogLine.fields
+	serverStartCallFields.AssertFieldNotEmpty(s.T(), "peer.address").
+		AssertFieldNotEmpty(s.T(), "grpc.start_time").
+		AssertFieldNotEmpty(s.T(), "grpc.request.deadline").
+		AssertFieldNotEmpty(s.T(), "grpc.time_ms").
+		AssertField(s.T(), logging.MethodFieldKey, "PingList").
+		AssertField(s.T(), logging.ServiceFieldKey, testpb.TestServiceFullName).AssertNoMoreTags(s.T())
+
+	serverFinishCallLogLine := lines[0]
+	assert.Equal(s.T(), logging.LevelInfo, serverFinishCallLogLine.lvl)
+	assert.Equal(s.T(), "finished call", serverFinishCallLogLine.msg)
+	serverFinishCallFields := serverFinishCallLogLine.fields
+	serverFinishCallFields.AssertFieldNotEmpty(s.T(), "peer.address").
+		AssertFieldNotEmpty(s.T(), "grpc.start_time").
+		AssertFieldNotEmpty(s.T(), "grpc.request.deadline").
+		AssertField(s.T(), "grpc.code", "OK").
+		AssertFieldNotEmpty(s.T(), "grpc.time_ms").
+		AssertField(s.T(), logging.MethodFieldKey, "PingList").
+		AssertField(s.T(), logging.ServiceFieldKey, testpb.TestServiceFullName).AssertNoMoreTags(s.T())
+
+	clientFinishCallLogLine := lines[1]
+	assert.Equal(s.T(), logging.LevelDebug, clientFinishCallLogLine.lvl)
+	assert.Equal(s.T(), "finished call", clientFinishCallLogLine.msg)
+
+	clientFinishCallFields := clientFinishCallLogLine.fields
+	clientFinishCallFields.AssertFieldNotEmpty(s.T(), "grpc.start_time").
+		AssertFieldNotEmpty(s.T(), "grpc.request.deadline").
+		AssertField(s.T(), "grpc.code", "OK").
+		AssertFieldNotEmpty(s.T(), "grpc.time_ms").
+		AssertField(s.T(), logging.MethodFieldKey, "PingList").
+		AssertField(s.T(), logging.ServiceFieldKey, testpb.TestServiceFullName).AssertNoMoreTags(s.T())
+}
+
 // waitUntil executes f every interval seconds until timeout or no error is returned from f.
 func waitUntil(interval time.Duration, stopc <-chan struct{}, f func() error) error {
 	tick := time.NewTicker(interval)
diff --git a/interceptors/logging/logging_test.go b/interceptors/logging/logging_test.go
index 2a6cfa4..150186f 100644
--- a/interceptors/logging/logging_test.go
+++ b/interceptors/logging/logging_test.go
@@ -39,3 +39,13 @@ func TestFieldsInjectExtractFromContext(t *testing.T) {
 	f = ExtractFields(c3)
 	require.Equal(t, Fields{"a", "changed", "b", "2"}, f)
 }
+
+func TestFieldsDelete(t *testing.T) {
+	f := Fields{"a", "1", "b", "2"}
+	f.Delete("a")
+	require.Equal(t, Fields{"b", "2"}, f)
+	f.Delete("b")
+	require.Equal(t, Fields{}, f)
+	f.Delete("c")
+	require.Equal(t, Fields{}, f)
+}
diff --git a/providers/prometheus/client_test.go b/providers/prometheus/client_test.go
index 4e5b9b0..76dbd43 100644
--- a/providers/prometheus/client_test.go
+++ b/providers/prometheus/client_test.go
@@ -100,3 +100,17 @@ func (s *ClientInterceptorTestSuite) TestStreamingIncrementsMetrics() {
 	requireValue(s.T(), 1, s.clientMetrics.clientHandledCounter.WithLabelValues("server_stream", testpb.TestServiceFullName, "PingList", "FailedPrecondition"))
 	requireValueHistCount(s.T(), 2, s.clientMetrics.clientHandledHistogram.WithLabelValues("server_stream", testpb.TestServiceFullName, "PingList"))
 }
+
+func (s *ClientInterceptorTestSuite) TestWithSubsystem() {
+	counterOpts := []CounterOption{
+		WithSubsystem("subsystem1"),
+	}
+	histOpts := []HistogramOption{
+		WithHistogramSubsystem("subsystem1"),
+	}
+	clientCounterOpts := WithClientCounterOptions(counterOpts...)
+	clientMetrics := NewClientMetrics(clientCounterOpts, WithClientHandlingTimeHistogram(histOpts...))
+
+	requireSubsystemName(s.T(), "subsystem1", clientMetrics.clientStartedCounter.WithLabelValues("unary", testpb.TestServiceFullName, "dummy"))
+	requireHistSubsystemName(s.T(), "subsystem1", clientMetrics.clientHandledHistogram.WithLabelValues("unary", testpb.TestServiceFullName, "dummy"))
+}
diff --git a/providers/prometheus/server_test.go b/providers/prometheus/server_test.go
index f64812e..422b2cf 100644
--- a/providers/prometheus/server_test.go
+++ b/providers/prometheus/server_test.go
@@ -56,6 +56,20 @@ func (s *ServerInterceptorTestSuite) SetupTest() {
 	s.serverMetrics.InitializeMetrics(s.Server)
 }
 
+func (s *ServerInterceptorTestSuite) TestWithSubsystem() {
+	counterOpts := []CounterOption{
+		WithSubsystem("subsystem1"),
+	}
+	histOpts := []HistogramOption{
+		WithHistogramSubsystem("subsystem1"),
+	}
+	serverCounterOpts := WithServerCounterOptions(counterOpts...)
+	serverMetrics := NewServerMetrics(serverCounterOpts, WithServerHandlingTimeHistogram(histOpts...))
+
+	requireSubsystemName(s.T(), "subsystem1", serverMetrics.serverStartedCounter.WithLabelValues("unary", testpb.TestServiceFullName, "dummy"))
+	requireHistSubsystemName(s.T(), "subsystem1", serverMetrics.serverHandledHistogram.WithLabelValues("unary", testpb.TestServiceFullName, "dummy"))
+}
+
 func (s *ServerInterceptorTestSuite) TestRegisterPresetsStuff() {
 	registry := prometheus.NewPedanticRegistry()
 	s.Require().NoError(registry.Register(s.serverMetrics))
@@ -233,6 +247,18 @@ func toFloat64HistCount(h prometheus.Observer) uint64 {
 	panic(fmt.Errorf("collected a non-histogram metric: %s", pb))
 }
 
+func requireSubsystemName(t *testing.T, expect string, c prometheus.Collector) {
+	t.Helper()
+	metricFullName := reflect.ValueOf(*c.(prometheus.Metric).Desc()).FieldByName("fqName").String()
+
+	if strings.Split(metricFullName, "_")[0] == expect {
+		return
+	}
+
+	t.Errorf("expected %s value to start with %s; ", metricFullName, expect)
+	t.Fail()
+}
+
 func requireValue(t *testing.T, expect int, c prometheus.Collector) {
 	t.Helper()
 	v := int(testutil.ToFloat64(c))
@@ -245,6 +271,18 @@ func requireValue(t *testing.T, expect int, c prometheus.Collector) {
 	t.Fail()
 }
 
+func requireHistSubsystemName(t *testing.T, expect string, o prometheus.Observer) {
+	t.Helper()
+	metricFullName := reflect.ValueOf(*o.(prometheus.Metric).Desc()).FieldByName("fqName").String()
+
+	if strings.Split(metricFullName, "_")[0] == expect {
+		return
+	}
+
+	t.Errorf("expected %s value to start with %s; ", metricFullName, expect)
+	t.Fail()
+}
+
 func requireValueHistCount(t *testing.T, expect int, o prometheus.Observer) {
 	t.Helper()
 	v := int(toFloat64HistCount(o))