From 9e162469a2ae2575cd55129e8d7a8d6f4eea5515 Mon Sep 17 00:00:00 2001 From: Ziqi Zhao Date: Tue, 30 Aug 2022 13:39:13 +0800 Subject: [PATCH] add metric rpc.server.duration for unary request and stream request Signed-off-by: Ziqi Zhao --- CHANGELOG.md | 5 + .../grpc/otelgrpc/{grpctrace.go => config.go} | 101 +++++------------- .../grpc/otelgrpc/example/go.mod | 1 + .../grpc/otelgrpc/example/go.sum | 2 + .../google.golang.org/grpc/otelgrpc/go.mod | 1 + .../google.golang.org/grpc/otelgrpc/go.sum | 2 + .../grpc/otelgrpc/interceptor.go | 11 +- .../grpc/otelgrpc/metadata_supplier.go | 98 +++++++++++++++++ .../grpc/otelgrpc/test/go.mod | 2 + .../grpc/otelgrpc/test/go.sum | 5 + .../grpc/otelgrpc/test/grpc_test.go | 31 +++++- 11 files changed, 185 insertions(+), 74 deletions(-) rename instrumentation/google.golang.org/grpc/otelgrpc/{grpctrace.go => config.go} (59%) create mode 100644 instrumentation/google.golang.org/grpc/otelgrpc/metadata_supplier.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a6d3f6968f..cee9b8d9d1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Added - Add trace context propagation support to `instrumentation/github.com/aws/aws-sdk-go-v2/otelaws` (#2856). +- [otelgrpc] add metric `rpc.server.duration` to otelgrpc instrumentation library. (#2700) ## [1.11.0/0.36.3/0.5.1] @@ -31,6 +32,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - The `Inject` function in `go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc` is deprecated. (#2838) - The `Extract` function in `go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc` is deprecated. (#2838) +### Added + +- [otelgrpc] Add `WithMeterProvider` function to enable metric and add metric `rpc.server.duration` to otelgrpc instrumentation library. (#2700) + ## [0.36.1] ### Changed diff --git a/instrumentation/google.golang.org/grpc/otelgrpc/grpctrace.go b/instrumentation/google.golang.org/grpc/otelgrpc/config.go similarity index 59% rename from instrumentation/google.golang.org/grpc/otelgrpc/grpctrace.go rename to instrumentation/google.golang.org/grpc/otelgrpc/config.go index 712e0f3628b..478e006019f 100644 --- a/instrumentation/google.golang.org/grpc/otelgrpc/grpctrace.go +++ b/instrumentation/google.golang.org/grpc/otelgrpc/config.go @@ -15,14 +15,13 @@ package otelgrpc // import "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" import ( - "context" - - "google.golang.org/grpc/metadata" - "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/baggage" + "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/metric/global" + "go.opentelemetry.io/otel/metric/instrument/syncint64" "go.opentelemetry.io/otel/propagation" + semconv "go.opentelemetry.io/otel/semconv/v1.12.0" "go.opentelemetry.io/otel/trace" ) @@ -43,6 +42,10 @@ type config struct { Filter Filter Propagators propagation.TextMapPropagator TracerProvider trace.TracerProvider + MeterProvider metric.MeterProvider + + meter metric.Meter + rpcServerDuration syncint64.Histogram } // Option applies an option value for a config. @@ -55,10 +58,23 @@ func newConfig(opts []Option) *config { c := &config{ Propagators: otel.GetTextMapPropagator(), TracerProvider: otel.GetTracerProvider(), + MeterProvider: global.MeterProvider(), } for _, o := range opts { o.apply(c) } + + c.meter = c.MeterProvider.Meter( + instrumentationName, + metric.WithInstrumentationVersion(SemVersion()), + metric.WithSchemaURL(semconv.SchemaURL), + ) + var err error + // the unit of rpc.server.duration is milliseconds + if c.rpcServerDuration, err = c.meter.SyncInt64().Histogram("rpc.server.duration"); err != nil { + otel.Handle(err) + } + return c } @@ -105,75 +121,16 @@ func WithTracerProvider(tp trace.TracerProvider) Option { return tracerProviderOption{tp: tp} } -type metadataSupplier struct { - metadata *metadata.MD -} - -// assert that metadataSupplier implements the TextMapCarrier interface. -var _ propagation.TextMapCarrier = &metadataSupplier{} - -func (s *metadataSupplier) Get(key string) string { - values := s.metadata.Get(key) - if len(values) == 0 { - return "" - } - return values[0] -} - -func (s *metadataSupplier) Set(key string, value string) { - s.metadata.Set(key, value) -} +type meterProviderOption struct{ mp metric.MeterProvider } -func (s *metadataSupplier) Keys() []string { - out := make([]string, 0, len(*s.metadata)) - for key := range *s.metadata { - out = append(out, key) +func (o meterProviderOption) apply(c *config) { + if o.mp != nil { + c.MeterProvider = o.mp } - return out -} - -// Inject injects correlation context and span context into the gRPC -// metadata object. This function is meant to be used on outgoing -// requests. -// Deprecated: Unnecessary public func. -func Inject(ctx context.Context, md *metadata.MD, opts ...Option) { - c := newConfig(opts) - c.Propagators.Inject(ctx, &metadataSupplier{ - metadata: md, - }) } -func inject(ctx context.Context, propagators propagation.TextMapPropagator) context.Context { - md, ok := metadata.FromOutgoingContext(ctx) - if !ok { - md = metadata.MD{} - } - propagators.Inject(ctx, &metadataSupplier{ - metadata: &md, - }) - return metadata.NewOutgoingContext(ctx, md) -} - -// Extract returns the correlation context and span context that -// another service encoded in the gRPC metadata object with Inject. -// This function is meant to be used on incoming requests. -// Deprecated: Unnecessary public func. -func Extract(ctx context.Context, md *metadata.MD, opts ...Option) (baggage.Baggage, trace.SpanContext) { - c := newConfig(opts) - ctx = c.Propagators.Extract(ctx, &metadataSupplier{ - metadata: md, - }) - - return baggage.FromContext(ctx), trace.SpanContextFromContext(ctx) -} - -func extract(ctx context.Context, propagators propagation.TextMapPropagator) context.Context { - md, ok := metadata.FromIncomingContext(ctx) - if !ok { - md = metadata.MD{} - } - - return propagators.Extract(ctx, &metadataSupplier{ - metadata: &md, - }) +// WithMeterProvider returns an Option to use the MeterProvider when +// creating a Meter. If this option is not provide the global MeterProvider will be used. +func WithMeterProvider(mp metric.MeterProvider) Option { + return meterProviderOption{mp: mp} } diff --git a/instrumentation/google.golang.org/grpc/otelgrpc/example/go.mod b/instrumentation/google.golang.org/grpc/otelgrpc/example/go.mod index 558b4e683ba..5fdf25817ba 100644 --- a/instrumentation/google.golang.org/grpc/otelgrpc/example/go.mod +++ b/instrumentation/google.golang.org/grpc/otelgrpc/example/go.mod @@ -18,6 +18,7 @@ require ( require ( github.com/go-logr/logr v1.2.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect + go.opentelemetry.io/otel/metric v0.31.0 // indirect golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 // indirect golang.org/x/text v0.3.3 // indirect google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect diff --git a/instrumentation/google.golang.org/grpc/otelgrpc/example/go.sum b/instrumentation/google.golang.org/grpc/otelgrpc/example/go.sum index d8615910545..627cfd667c3 100644 --- a/instrumentation/google.golang.org/grpc/otelgrpc/example/go.sum +++ b/instrumentation/google.golang.org/grpc/otelgrpc/example/go.sum @@ -37,6 +37,8 @@ go.opentelemetry.io/otel v1.11.0 h1:kfToEGMDq6TrVrJ9Vht84Y8y9enykSZzDDZglV0kIEk= go.opentelemetry.io/otel v1.11.0/go.mod h1:H2KtuEphyMvlhZ+F7tg9GRhAOe60moNx61Ex+WmiKkk= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.11.0 h1:rzpQkvma82S+jQvJHqJaAGQdeRBtH6HASrgrZa45rx4= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.11.0/go.mod h1:nMt8nBu01qC+8LfJu4puk/OYHovohkISNuy/MMG8yRk= +go.opentelemetry.io/otel/metric v0.31.0 h1:6SiklT+gfWAwWUR0meEMxQBtihpiEs4c+vL9spDTqUs= +go.opentelemetry.io/otel/metric v0.31.0/go.mod h1:ohmwj9KTSIeBnDBm/ZwH2PSZxZzoOaG2xZeekTRzL5A= go.opentelemetry.io/otel/sdk v1.11.0 h1:ZnKIL9V9Ztaq+ME43IUi/eo22mNsb6a7tGfzaOWB5fo= go.opentelemetry.io/otel/sdk v1.11.0/go.mod h1:REusa8RsyKaq0OlyangWXaw97t2VogoO4SSEeKkSTAk= go.opentelemetry.io/otel/trace v1.11.0 h1:20U/Vj42SX+mASlXLmSGBg6jpI1jQtv682lZtTAOVFI= diff --git a/instrumentation/google.golang.org/grpc/otelgrpc/go.mod b/instrumentation/google.golang.org/grpc/otelgrpc/go.mod index 8933ebfa45b..2cfbd79d741 100644 --- a/instrumentation/google.golang.org/grpc/otelgrpc/go.mod +++ b/instrumentation/google.golang.org/grpc/otelgrpc/go.mod @@ -4,6 +4,7 @@ go 1.18 require ( go.opentelemetry.io/otel v1.11.0 + go.opentelemetry.io/otel/metric v0.31.0 go.opentelemetry.io/otel/trace v1.11.0 google.golang.org/grpc v1.49.0 google.golang.org/protobuf v1.28.1 diff --git a/instrumentation/google.golang.org/grpc/otelgrpc/go.sum b/instrumentation/google.golang.org/grpc/otelgrpc/go.sum index 156beae6389..d6c3bfe78d3 100644 --- a/instrumentation/google.golang.org/grpc/otelgrpc/go.sum +++ b/instrumentation/google.golang.org/grpc/otelgrpc/go.sum @@ -36,6 +36,8 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1: github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= go.opentelemetry.io/otel v1.11.0 h1:kfToEGMDq6TrVrJ9Vht84Y8y9enykSZzDDZglV0kIEk= go.opentelemetry.io/otel v1.11.0/go.mod h1:H2KtuEphyMvlhZ+F7tg9GRhAOe60moNx61Ex+WmiKkk= +go.opentelemetry.io/otel/metric v0.31.0 h1:6SiklT+gfWAwWUR0meEMxQBtihpiEs4c+vL9spDTqUs= +go.opentelemetry.io/otel/metric v0.31.0/go.mod h1:ohmwj9KTSIeBnDBm/ZwH2PSZxZzoOaG2xZeekTRzL5A= go.opentelemetry.io/otel/trace v1.11.0 h1:20U/Vj42SX+mASlXLmSGBg6jpI1jQtv682lZtTAOVFI= go.opentelemetry.io/otel/trace v1.11.0/go.mod h1:nyYjis9jy0gytE9LXGU+/m1sHTKbRY0fX0hulNNDP1U= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= diff --git a/instrumentation/google.golang.org/grpc/otelgrpc/interceptor.go b/instrumentation/google.golang.org/grpc/otelgrpc/interceptor.go index 2546e0f1b8a..aeee1d18dbd 100644 --- a/instrumentation/google.golang.org/grpc/otelgrpc/interceptor.go +++ b/instrumentation/google.golang.org/grpc/otelgrpc/interceptor.go @@ -20,6 +20,7 @@ import ( "context" "io" "net" + "time" "google.golang.org/grpc" grpc_codes "google.golang.org/grpc/codes" @@ -338,13 +339,22 @@ func UnaryServerInterceptor(opts ...Option) grpc.UnaryServerInterceptor { messageReceived.Event(ctx, 1, req) + var statusCode grpc_codes.Code + defer func(t time.Time) { + elapsedTime := time.Since(t) / time.Millisecond + attr = append(attr, semconv.RPCGRPCStatusCodeKey.Int64(int64(statusCode))) + cfg.rpcServerDuration.Record(ctx, int64(elapsedTime), attr...) + }(time.Now()) + resp, err := handler(ctx, req) if err != nil { s, _ := status.FromError(err) + statusCode = s.Code() span.SetStatus(codes.Error, s.Message()) span.SetAttributes(statusCodeAttr(s.Code())) messageSent.Event(ctx, 1, s.Proto()) } else { + statusCode = grpc_codes.OK span.SetAttributes(statusCodeAttr(grpc_codes.OK)) messageSent.Event(ctx, 1, resp) } @@ -430,7 +440,6 @@ func StreamServerInterceptor(opts ...Option) grpc.StreamServerInterceptor { defer span.End() err := handler(srv, wrapServerStream(ctx, ss)) - if err != nil { s, _ := status.FromError(err) span.SetStatus(codes.Error, s.Message()) diff --git a/instrumentation/google.golang.org/grpc/otelgrpc/metadata_supplier.go b/instrumentation/google.golang.org/grpc/otelgrpc/metadata_supplier.go new file mode 100644 index 00000000000..d91c6df2370 --- /dev/null +++ b/instrumentation/google.golang.org/grpc/otelgrpc/metadata_supplier.go @@ -0,0 +1,98 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package otelgrpc // import "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" + +import ( + "context" + + "google.golang.org/grpc/metadata" + + "go.opentelemetry.io/otel/baggage" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/trace" +) + +type metadataSupplier struct { + metadata *metadata.MD +} + +// assert that metadataSupplier implements the TextMapCarrier interface. +var _ propagation.TextMapCarrier = &metadataSupplier{} + +func (s *metadataSupplier) Get(key string) string { + values := s.metadata.Get(key) + if len(values) == 0 { + return "" + } + return values[0] +} + +func (s *metadataSupplier) Set(key string, value string) { + s.metadata.Set(key, value) +} + +func (s *metadataSupplier) Keys() []string { + out := make([]string, 0, len(*s.metadata)) + for key := range *s.metadata { + out = append(out, key) + } + return out +} + +// Inject injects correlation context and span context into the gRPC +// metadata object. This function is meant to be used on outgoing +// requests. +// Deprecated: Unnecessary public func. +func Inject(ctx context.Context, md *metadata.MD, opts ...Option) { + c := newConfig(opts) + c.Propagators.Inject(ctx, &metadataSupplier{ + metadata: md, + }) +} + +func inject(ctx context.Context, propagators propagation.TextMapPropagator) context.Context { + md, ok := metadata.FromOutgoingContext(ctx) + if !ok { + md = metadata.MD{} + } + propagators.Inject(ctx, &metadataSupplier{ + metadata: &md, + }) + return metadata.NewOutgoingContext(ctx, md) +} + +// Extract returns the correlation context and span context that +// another service encoded in the gRPC metadata object with Inject. +// This function is meant to be used on incoming requests. +// Deprecated: Unnecessary public func. +func Extract(ctx context.Context, md *metadata.MD, opts ...Option) (baggage.Baggage, trace.SpanContext) { + c := newConfig(opts) + ctx = c.Propagators.Extract(ctx, &metadataSupplier{ + metadata: md, + }) + + return baggage.FromContext(ctx), trace.SpanContextFromContext(ctx) +} + +func extract(ctx context.Context, propagators propagation.TextMapPropagator) context.Context { + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + md = metadata.MD{} + } + + return propagators.Extract(ctx, &metadataSupplier{ + metadata: &md, + }) +} diff --git a/instrumentation/google.golang.org/grpc/otelgrpc/test/go.mod b/instrumentation/google.golang.org/grpc/otelgrpc/test/go.mod index 57952aed518..c9b6eeed8ed 100644 --- a/instrumentation/google.golang.org/grpc/otelgrpc/test/go.mod +++ b/instrumentation/google.golang.org/grpc/otelgrpc/test/go.mod @@ -7,6 +7,7 @@ require ( go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.36.3 go.opentelemetry.io/otel v1.11.0 go.opentelemetry.io/otel/sdk v1.11.0 + go.opentelemetry.io/otel/sdk/metric v0.31.0 go.uber.org/goleak v1.2.0 google.golang.org/grpc v1.49.0 google.golang.org/protobuf v1.28.1 @@ -20,6 +21,7 @@ require ( github.com/golang/protobuf v1.5.2 // indirect github.com/kr/text v0.2.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + go.opentelemetry.io/otel/metric v0.31.0 // indirect go.opentelemetry.io/otel/trace v1.11.0 // indirect golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 // indirect golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d // indirect diff --git a/instrumentation/google.golang.org/grpc/otelgrpc/test/go.sum b/instrumentation/google.golang.org/grpc/otelgrpc/test/go.sum index 8f5060eff3f..ab3c69c6365 100644 --- a/instrumentation/google.golang.org/grpc/otelgrpc/test/go.sum +++ b/instrumentation/google.golang.org/grpc/otelgrpc/test/go.sum @@ -2,6 +2,7 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.34.0 h1:eOI3/cP2VTU6uZLDYAoic+eyzzB9YyGmJ7eIjl8rOPg= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -47,8 +48,12 @@ github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PK github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= go.opentelemetry.io/otel v1.11.0 h1:kfToEGMDq6TrVrJ9Vht84Y8y9enykSZzDDZglV0kIEk= go.opentelemetry.io/otel v1.11.0/go.mod h1:H2KtuEphyMvlhZ+F7tg9GRhAOe60moNx61Ex+WmiKkk= +go.opentelemetry.io/otel/metric v0.31.0 h1:6SiklT+gfWAwWUR0meEMxQBtihpiEs4c+vL9spDTqUs= +go.opentelemetry.io/otel/metric v0.31.0/go.mod h1:ohmwj9KTSIeBnDBm/ZwH2PSZxZzoOaG2xZeekTRzL5A= go.opentelemetry.io/otel/sdk v1.11.0 h1:ZnKIL9V9Ztaq+ME43IUi/eo22mNsb6a7tGfzaOWB5fo= go.opentelemetry.io/otel/sdk v1.11.0/go.mod h1:REusa8RsyKaq0OlyangWXaw97t2VogoO4SSEeKkSTAk= +go.opentelemetry.io/otel/sdk/metric v0.31.0 h1:2sZx4R43ZMhJdteKAlKoHvRgrMp53V1aRxvEf5lCq8Q= +go.opentelemetry.io/otel/sdk/metric v0.31.0/go.mod h1:fl0SmNnX9mN9xgU6OLYLMBMrNAsaZQi7qBwprwO3abk= go.opentelemetry.io/otel/trace v1.11.0 h1:20U/Vj42SX+mASlXLmSGBg6jpI1jQtv682lZtTAOVFI= go.opentelemetry.io/otel/trace v1.11.0/go.mod h1:nyYjis9jy0gytE9LXGU+/m1sHTKbRY0fX0hulNNDP1U= go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= diff --git a/instrumentation/google.golang.org/grpc/otelgrpc/test/grpc_test.go b/instrumentation/google.golang.org/grpc/otelgrpc/test/grpc_test.go index 30fc9e78880..c44c79c2699 100644 --- a/instrumentation/google.golang.org/grpc/otelgrpc/test/grpc_test.go +++ b/instrumentation/google.golang.org/grpc/otelgrpc/test/grpc_test.go @@ -30,6 +30,7 @@ import ( "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/sdk/metric/metrictest" "go.opentelemetry.io/otel/sdk/trace" "go.opentelemetry.io/otel/sdk/trace/tracetest" semconv "go.opentelemetry.io/otel/semconv/v1.12.0" @@ -84,6 +85,7 @@ func TestInterceptors(t *testing.T) { serverUnarySR := tracetest.NewSpanRecorder() serverUnaryTP := trace.NewTracerProvider(trace.WithSpanProcessor(serverUnarySR)) + serverUnaryMP, serverUnaryMetricExporter := metrictest.NewTestMeterProvider() serverStreamSR := tracetest.NewSpanRecorder() serverStreamTP := trace.NewTracerProvider(trace.WithSpanProcessor(serverStreamSR)) @@ -94,7 +96,7 @@ func TestInterceptors(t *testing.T) { grpc.WithStreamInterceptor(otelgrpc.StreamClientInterceptor(otelgrpc.WithTracerProvider(clientStreamTP))), }, []grpc.ServerOption{ - grpc.UnaryInterceptor(otelgrpc.UnaryServerInterceptor(otelgrpc.WithTracerProvider(serverUnaryTP))), + grpc.UnaryInterceptor(otelgrpc.UnaryServerInterceptor(otelgrpc.WithTracerProvider(serverUnaryTP), otelgrpc.WithMeterProvider(serverUnaryMP))), grpc.StreamInterceptor(otelgrpc.StreamServerInterceptor(otelgrpc.WithTracerProvider(serverStreamTP))), }, )) @@ -109,6 +111,7 @@ func TestInterceptors(t *testing.T) { t.Run("UnaryServerSpans", func(t *testing.T) { checkUnaryServerSpans(t, serverUnarySR.Ended()) + checkUnaryServerRecords(t, serverUnaryMetricExporter) }) t.Run("StreamServerSpans", func(t *testing.T) { @@ -622,3 +625,29 @@ func assertEvents(t *testing.T, expected, actual []trace.Event) bool { return !failed } + +func checkUnaryServerRecords(t *testing.T, exporter *metrictest.Exporter) { + assert.NoError(t, exporter.Collect(context.Background())) + records := exporter.GetRecords() + assert.Equal(t, 2, len(records)) + + for _, record := range records { + method := getRPCMethod(record.Attributes) + assert.NotEmpty(t, method) + assert.ElementsMatch(t, []attribute.KeyValue{ + semconv.RPCMethodKey.String(method), + semconv.RPCServiceKey.String("grpc.testing.TestService"), + otelgrpc.RPCSystemGRPC, + otelgrpc.GRPCStatusCodeKey.Int64(int64(codes.OK)), + }, record.Attributes) + } +} + +func getRPCMethod(attrs []attribute.KeyValue) string { + for _, kvs := range attrs { + if kvs.Key == semconv.RPCMethodKey { + return kvs.Value.AsString() + } + } + return "" +}