Repository: open-telemetry/opentelemetry-go-contrib
Pull request #2700 (2022-08-31 05:53:24)

[otelgrpc] add metric rpc.server.duration support 
Signed-off-by: Ziqi Zhao [zhaoziqi9146@gmail.com](mailto:zhaoziqi9146@gmail.com)

Enhance instrumentation library [otelgrpc](https://github.com/open-telemetry/opentelemetry-go-contrib/tree/main/instrumentation/google.golang.org/grpc/otelgrpc) for grpc to satisfy opentelemetry [grpc server metric semantic conventions](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/rpc.md#rpc-server).

This pr is meant to add metric `rpc.server.duration` for the first step.

I use example code to test, and produce the data for unary and stream request
```
{
    "Name": "rpc.server.duration{service.name=unknown_service:main,telemetry.sdk.language=go,telemetry.sdk.name=opentelemetry,telemetry.sdk.version=1.9.0,instrumentation.name=go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc,instrumentation.version=semver:0.34.0,net.peer.ip=127.0.0.1,net.peer.port=55809,rpc.grpc.status_code=0,rpc.method=SayHelloServerStream,rpc.service=api.HelloService,rpc.system=grpc}",
    "Sum": 502.366387
},
{
    "Name": "rpc.server.duration{service.name=unknown_service:main,telemetry.sdk.language=go,telemetry.sdk.name=opentelemetry,telemetry.sdk.version=1.9.0,instrumentation.name=go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc,instrumentation.version=semver:0.34.0,net.peer.ip=127.0.0.1,net.peer.port=55809,rpc.grpc.status_code=0,rpc.method=SayHelloBidiStream,rpc.service=api.HelloService,rpc.system=grpc}",
    "Sum": 253.394227
}
```

dmathieu: You need to run `go mod tidy` from the otelgrpc package to fix the tests.
Also, this will need a changelog entry.
codecov[bot]: # [Codecov](https://codecov.io/gh/open-telemetry/opentelemetry-go-contrib/pull/2700?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry) Report
> Merging [#2700](https://codecov.io/gh/open-telemetry/opentelemetry-go-contrib/pull/2700?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry) (a6e086f) into [main](https://codecov.io/gh/open-telemetry/opentelemetry-go-contrib/commit/df16f32df86b40077c9c90d06f33c4cdb6dd5afa?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry) (df16f32) will **decrease** coverage by `0.0%`.
> The diff coverage is `74.1%`.

<details><summary>Additional details and impacted files</summary>


[![Impacted file tree graph](https://codecov.io/gh/open-telemetry/opentelemetry-go-contrib/pull/2700/graphs/tree.svg?width=650&height=150&src=pr&token=P6F3W9WA7Q&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry)](https://codecov.io/gh/open-telemetry/opentelemetry-go-contrib/pull/2700?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry)

```diff
@@           Coverage Diff           @@
##            main   #2700     +/-   ##
=======================================
- Coverage   74.4%   74.4%   -0.1%     
=======================================
  Files        145     146      +1     
  Lines       6685    6715     +30     
=======================================
+ Hits        4974    4996     +22     
- Misses      1564    1572      +8     
  Partials     147     147             
```


| [Impacted Files](https://codecov.io/gh/open-telemetry/opentelemetry-go-contrib/pull/2700?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry) | Coverage Δ | |
|---|---|---|
| [...tion/google.golang.org/grpc/otelgrpc/grpcmetric.go](https://codecov.io/gh/open-telemetry/opentelemetry-go-contrib/pull/2700/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry#diff-aW5zdHJ1bWVudGF0aW9uL2dvb2dsZS5nb2xhbmcub3JnL2dycGMvb3RlbGdycGMvZ3JwY21ldHJpYy5nbw==) | `0.0% <0.0%> (ø)` | |
| [...ion/google.golang.org/grpc/otelgrpc/interceptor.go](https://codecov.io/gh/open-telemetry/opentelemetry-go-contrib/pull/2700/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry#diff-aW5zdHJ1bWVudGF0aW9uL2dvb2dsZS5nb2xhbmcub3JnL2dycGMvb3RlbGdycGMvaW50ZXJjZXB0b3IuZ28=) | `84.7% <91.6%> (+0.4%)` | :arrow_up: |
| [...ation/google.golang.org/grpc/otelgrpc/grpctrace.go](https://codecov.io/gh/open-telemetry/opentelemetry-go-contrib/pull/2700/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry#diff-aW5zdHJ1bWVudGF0aW9uL2dvb2dsZS5nb2xhbmcub3JnL2dycGMvb3RlbGdycGMvZ3JwY3RyYWNlLmdv) | `50.0% <100.0%> (+0.8%)` | :arrow_up: |

</details>
fatsheep9146: @dmathieu @Aneurysm9 All checks are passed, could you help review this again?
fatsheep9146: Could you help review this again? @dmathieu 
fatsheep9146: @Aneurysm9 @jmacd @MrAlias @hanyuancheung 

Could you help review this again?
devnev: Hi, stumbled across this, and had two questions about the units

- Why milliseconds? Looks like its a float64 anyway, so the position of the decimal makes no technical difference, so seconds would seem like a better choice simply for being the SI standard.
- Does the unit need to be indicated somewhere? I had to dig into the recording code to find it.

fatsheep9146: > Signed-off-by: Ziqi Zhao [zhaoziqi9146@gmail.com](mailto:zhaoziqi9146@gmail.com)
> 
> Enhance instrumentation library [otelgrpc](https://github.com/open-telemetry/opentelemetry-go-contrib/tree/main/instrumentation/google.golang.org/grpc/otelgrpc) for grpc to satisfy opentelemetry [grpc server metric semantic conventions](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/rpc.md#rpc-server).
> 
> This pr is meant to add metric `rpc.server.duration` for the first step.
> 
> I use example code to test, and produce the data for unary and stream request
> 
> ```
> {
>     "Name": "rpc.server.duration{service.name=unknown_service:main,telemetry.sdk.language=go,telemetry.sdk.name=opentelemetry,telemetry.sdk.version=1.9.0,instrumentation.name=go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc,instrumentation.version=semver:0.34.0,net.peer.ip=127.0.0.1,net.peer.port=55809,rpc.grpc.status_code=0,rpc.method=SayHelloServerStream,rpc.service=api.HelloService,rpc.system=grpc}",
>     "Sum": 502.366387
> },
> {
>     "Name": "rpc.server.duration{service.name=unknown_service:main,telemetry.sdk.language=go,telemetry.sdk.name=opentelemetry,telemetry.sdk.version=1.9.0,instrumentation.name=go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc,instrumentation.version=semver:0.34.0,net.peer.ip=127.0.0.1,net.peer.port=55809,rpc.grpc.status_code=0,rpc.method=SayHelloBidiStream,rpc.service=api.HelloService,rpc.system=grpc}",
>     "Sum": 253.394227
> }
> ```
@devnev 
I think you can visit here to find the info about all metrics.
https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/rpc.md#rpc-server
fatsheep9146: ping @pellared @Aneurysm9 @MrAlias @jmacd @dashpole @MadVikingGod @evantorrie @XSAM , could you help review this again?
fatsheep9146: ping  @pellared @Aneurysm9 @MrAlias @jmacd @dashpole @MadVikingGod @evantorrie @XSAM could you help review this?
MrAlias: Is the plan to add the other specified metrics (https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/rpc.md#rpc-server) in follow on PRs? If so, can you open an issue to track the work?
fatsheep9146: > Is the plan to add the other specified metrics (https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/rpc.md#rpc-server) in follow on PRs? If so, can you open an issue to track the work?

Absolutely, I already open an issue #2840 to track, and I will add the metric one by one. @MrAlias 
fatsheep9146: Could you help review this again? @MrAlias 
fatsheep9146: @jmacd @pellared @Aneurysm9 Could you help review this again?

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d1ce0928d..87140b977 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,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 `WithMeterProvider` function to enable metric and add metric `rpc.server.duration` to otelgrpc instrumentation library. (#2700) 
 
 ### Changed
 
diff --git a/instrumentation/README.md b/instrumentation/README.md
index 76b34325b..f645f7c2d 100644
--- a/instrumentation/README.md
+++ b/instrumentation/README.md
@@ -53,7 +53,7 @@ The following instrumentation packages are provided for popular Go packages and
 | [github.com/labstack/echo](./github.com/labstack/echo/otelecho) |  | ✓ |
 | [github.com/Shopify/sarama](./github.com/Shopify/sarama/otelsarama) |  | ✓ |
 | [go.mongodb.org/mongo-driver](./go.mongodb.org/mongo-driver/mongo/otelmongo) |  | ✓ |
-| [google.golang.org/grpc](./google.golang.org/grpc/otelgrpc) |  | ✓ |
+| [google.golang.org/grpc](./google.golang.org/grpc/otelgrpc) | ✓ | ✓ |
 | [gopkg.in/macaron.v1](./gopkg.in/macaron.v1/otelmacaron) |  | ✓ |
 | [host](./host) | ✓ |  |
 | [net/http](./net/http/otelhttp) | ✓ | ✓ |
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 712e0f362..369ebae24 100644
--- a/instrumentation/google.golang.org/grpc/otelgrpc/grpctrace.go
+++ b/instrumentation/google.golang.org/grpc/otelgrpc/config.go
@@ -15,14 +15,15 @@
 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"
+	"go.opentelemetry.io/otel/metric/instrument/syncint64"
+	"go.opentelemetry.io/otel/metric/unit"
 	"go.opentelemetry.io/otel/propagation"
+	semconv "go.opentelemetry.io/otel/semconv/v1.12.0"
 	"go.opentelemetry.io/otel/trace"
 )
 
@@ -43,6 +44,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 +60,22 @@ 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
+	if c.rpcServerDuration, err = c.meter.SyncInt64().Histogram("rpc.server.duration", instrument.WithUnit(unit.Milliseconds)); err != nil {
+		otel.Handle(err)
+	}
+
 	return c
 }
 
@@ -105,75 +122,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 8643b3e80..e7bf3df09 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.33.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 7299bd549..c3d594e8a 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.1 h1:4WLLAmcfkmDk2ukNXJyq3/kiz/3UzCaYq6PskJsaou4=
 go.opentelemetry.io/otel v1.11.1/go.mod h1:1nNhXBbWSD0nsL38H6btgnFN2k4i0sNLHNNMZMSbUGE=
 go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.11.1 h1:3Yvzs7lgOw8MmbxmLRsQGwYdCubFmUHSooKaEhQunFQ=
 go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.11.1/go.mod h1:pyHDt0YlyuENkD2VwHsiRDf+5DfI3EH7pfhUYW6sQUE=
+go.opentelemetry.io/otel/metric v0.33.0 h1:xQAyl7uGEYvrLAiV/09iTJlp1pZnQ9Wl793qbVvED1E=
+go.opentelemetry.io/otel/metric v0.33.0/go.mod h1:QlTYc+EnYNq/M2mNk1qDDMRLpqCOj2f/r5c7Fd5FYaI=
 go.opentelemetry.io/otel/sdk v1.11.1 h1:F7KmQgoHljhUuJyA+9BiU+EkJfyX5nVVF4wyzWZpKxs=
 go.opentelemetry.io/otel/sdk v1.11.1/go.mod h1:/l3FE4SupHJ12TduVjUkZtlfFqDCQJlOlithYrdktys=
 go.opentelemetry.io/otel/trace v1.11.1 h1:ofxdnzsNrGBYXbP7t7zpUK281+go5rF7dvdIZXF8gdQ=
diff --git a/instrumentation/google.golang.org/grpc/otelgrpc/go.mod b/instrumentation/google.golang.org/grpc/otelgrpc/go.mod
index 365b242aa..4d59221c8 100644
--- a/instrumentation/google.golang.org/grpc/otelgrpc/go.mod
+++ b/instrumentation/google.golang.org/grpc/otelgrpc/go.mod
@@ -3,7 +3,9 @@ module go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgr
 go 1.18
 
 require (
+	github.com/stretchr/testify v1.8.0
 	go.opentelemetry.io/otel v1.11.1
+	go.opentelemetry.io/otel/metric v0.33.0
 	go.opentelemetry.io/otel/trace v1.11.1
 	google.golang.org/grpc v1.50.1
 	google.golang.org/protobuf v1.28.1
@@ -11,13 +13,16 @@ require (
 
 require (
 	cloud.google.com/go v0.34.0 // indirect
+	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/go-logr/logr v1.2.3 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/golang/protobuf v1.5.2 // indirect
+	github.com/pmezard/go-difflib v1.0.0 // indirect
 	golang.org/x/net v0.0.0-20201021035429-f5854403a974 // indirect
 	golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d // indirect
 	golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 // indirect
 	golang.org/x/text v0.3.3 // indirect
 	google.golang.org/appengine v1.4.0 // indirect
 	google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/instrumentation/google.golang.org/grpc/otelgrpc/go.sum b/instrumentation/google.golang.org/grpc/otelgrpc/go.sum
index ba5618f55..ac7002db2 100644
--- a/instrumentation/google.golang.org/grpc/otelgrpc/go.sum
+++ b/instrumentation/google.golang.org/grpc/otelgrpc/go.sum
@@ -4,7 +4,9 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 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/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=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -32,10 +34,17 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 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_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 go.opentelemetry.io/otel v1.11.1 h1:4WLLAmcfkmDk2ukNXJyq3/kiz/3UzCaYq6PskJsaou4=
 go.opentelemetry.io/otel v1.11.1/go.mod h1:1nNhXBbWSD0nsL38H6btgnFN2k4i0sNLHNNMZMSbUGE=
+go.opentelemetry.io/otel/metric v0.33.0 h1:xQAyl7uGEYvrLAiV/09iTJlp1pZnQ9Wl793qbVvED1E=
+go.opentelemetry.io/otel/metric v0.33.0/go.mod h1:QlTYc+EnYNq/M2mNk1qDDMRLpqCOj2f/r5c7Fd5FYaI=
 go.opentelemetry.io/otel/trace v1.11.1 h1:ofxdnzsNrGBYXbP7t7zpUK281+go5rF7dvdIZXF8gdQ=
 go.opentelemetry.io/otel/trace v1.11.1/go.mod h1:f/Q9G7vzk5u91PhbmKbg1Qn0rzH1LJ4vbPHFGkTPtOk=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -97,6 +106,10 @@ 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.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
 google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+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=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/instrumentation/google.golang.org/grpc/otelgrpc/interceptor.go b/instrumentation/google.golang.org/grpc/otelgrpc/interceptor.go
index 2546e0f1b..aeee1d18d 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 000000000..d91c6df23
--- /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,
+	})
+}


Test patch:
diff --git a/instrumentation/google.golang.org/grpc/otelgrpc/metadata_supplier_test.go b/instrumentation/google.golang.org/grpc/otelgrpc/metadata_supplier_test.go
new file mode 100644
index 000000000..2217ecafa
--- /dev/null
+++ b/instrumentation/google.golang.org/grpc/otelgrpc/metadata_supplier_test.go
@@ -0,0 +1,39 @@
+// 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 (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"google.golang.org/grpc/metadata"
+)
+
+func TestMetadataSupplier(t *testing.T) {
+	md := metadata.New(map[string]string{
+		"k1": "v1",
+	})
+	ms := &metadataSupplier{&md}
+
+	v1 := ms.Get("k1")
+	assert.Equal(t, v1, "v1")
+
+	ms.Set("k2", "v2")
+
+	v1 = ms.Get("k1")
+	v2 := ms.Get("k2")
+	assert.Equal(t, v1, "v1")
+	assert.Equal(t, v2, "v2")
+}
diff --git a/instrumentation/google.golang.org/grpc/otelgrpc/test/go.mod b/instrumentation/google.golang.org/grpc/otelgrpc/test/go.mod
index 874a4aa54..2ebf1be39 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.4
 	go.opentelemetry.io/otel v1.11.1
 	go.opentelemetry.io/otel/sdk v1.11.1
+	go.opentelemetry.io/otel/sdk/metric v0.31.0
 	go.uber.org/goleak v1.2.0
 	google.golang.org/grpc v1.50.1
 	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.33.0 // indirect
 	go.opentelemetry.io/otel/trace v1.11.1 // 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 5184b3840..e00f5f459 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.1 h1:4WLLAmcfkmDk2ukNXJyq3/kiz/3UzCaYq6PskJsaou4=
 go.opentelemetry.io/otel v1.11.1/go.mod h1:1nNhXBbWSD0nsL38H6btgnFN2k4i0sNLHNNMZMSbUGE=
+go.opentelemetry.io/otel/metric v0.33.0 h1:xQAyl7uGEYvrLAiV/09iTJlp1pZnQ9Wl793qbVvED1E=
+go.opentelemetry.io/otel/metric v0.33.0/go.mod h1:QlTYc+EnYNq/M2mNk1qDDMRLpqCOj2f/r5c7Fd5FYaI=
 go.opentelemetry.io/otel/sdk v1.11.1 h1:F7KmQgoHljhUuJyA+9BiU+EkJfyX5nVVF4wyzWZpKxs=
 go.opentelemetry.io/otel/sdk v1.11.1/go.mod h1:/l3FE4SupHJ12TduVjUkZtlfFqDCQJlOlithYrdktys=
+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.1 h1:ofxdnzsNrGBYXbP7t7zpUK281+go5rF7dvdIZXF8gdQ=
 go.opentelemetry.io/otel/trace v1.11.1/go.mod h1:f/Q9G7vzk5u91PhbmKbg1Qn0rzH1LJ4vbPHFGkTPtOk=
 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 30fc9e788..c44c79c26 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 ""
+}