Repository: grpc/grpc-go

Issue:
credentials/google/google: missing OAuth scopes for ADC
### What version of gRPC are you using?

v1.68.0, with and without cherry-picked #7845

### What version of Go are you using (`go version`)?

go version go1.23.2 linux/amd64

### What operating system (Linux, Windows, …) and version?

Debian GNU/Linux 12 (bookworm)

### What did you do?

1.  Deployed gRPC apps in a Kubernetes cluster running off Google Cloud.

2.  Configured gRPC xDS bootstrap with `server_uri` value `dns:///trafficdirector.googleapis.com:443` and `channel_creds.type` value `google_default`.

3.  Created a workload identity pool and provider by following the guide [Configure Workload Identity Federation with Kubernetes](https://cloud.google.com/iam/docs/workload-identity-federation-with-kubernetes). Used [direct resource access](https://cloud.google.com/iam/docs/workload-identity-federation-with-kubernetes#use-wlif), _not_ [IAM service account impersonation](https://cloud.google.com/iam/docs/workload-identity-federation-with-kubernetes#use-service-account-impersonation).

5.  Configured Application Default Credentials (ADC) on the Pods by setting the `GOOGLE_APPLICATION_CREDENTIALS` environment variable pointing to a [credentials configuration file](https://cloud.google.com/iam/docs/workload-download-cred-and-grant-access#direct-resource) and mounting a [`serviceAccountToken` projected volume](https://kubernetes.io/docs/concepts/storage/projected-volumes/#serviceaccounttoken) with an `audience` value referencing the workload identity pool provider.

6.  Verified the ADC setup by creating the `example` Pod from the guide [Configure Workload Identity Federation with Kubernetes](https://cloud.google.com/iam/docs/workload-identity-federation-with-kubernetes#deploy), section "Deploy the Kubernetes workload", and successfully obtained a token.

### What did you expect to see?

The gRPC applications should successfully authenticate to Traffic Director using Application Default Credentials (ADC) and Workload Identity Federation, and start the ADS stream.

### What did you see instead?

1.  Using grpc-go `v1.68.0` (so before #7845 was merged to fix #6285): `context canceled` error when obtaining a token from `sts.googleapis.com` as part of setting up the ADS stream:

    `transport/transport.go:336] "[xds][xds-client 0xc0000a8540] [trafficdirector.googleapis.com:443] Creating new ADS stream failed: rpc error: code = Unauthenticated desc = transport: per-RPC creds failed due to error: oauth2/google: invalid response from Secure Token Server: Post \"https://sts.googleapis.com/v1/token\": context canceled"`

2.  After cherry-picking PR #7845 to fix #6285: Error when authenticating to Traffic Director because no OAuth scopes were provided:

    `transport/transport.go:336] "[xds][xds-client 0xc000400360] [trafficdirector.googleapis.com:443] Creating new ADS stream failed: rpc error: code = Unauthenticated desc = transport: per-RPC creds failed due to error: oauth2/google: status code 400: {\"error\":\"invalid_request\",\"error_description\":\"Scope(s) must be provided.\"}"`

### Related issues and PRs

#6285 credentials/google/google not working with oauth2 and ADC
#7845 credentials: remove the context timeout to fix token request failure with non-GCE ADC


Discussion:
@arjan-bal: Thanks for reporting this issue and the fix (https://github.com/grpc/grpc-go/pull/7887). I've added people to review the fix.

Solution patch:
diff --git a/credentials/google/google.go b/credentials/google/google.go
index d18e370d..5a9c9461 100644
--- a/credentials/google/google.go
+++ b/credentials/google/google.go
@@ -30,6 +30,8 @@ import (
 	"google.golang.org/grpc/internal"
 )
 
+const defaultCloudPlatformScope = "https://www.googleapis.com/auth/cloud-platform"
+
 var logger = grpclog.Component("credentials")
 
 // DefaultCredentialsOptions constructs options to build DefaultCredentials.
@@ -120,7 +122,7 @@ var (
 		return alts.NewClientCreds(alts.DefaultClientOptions())
 	}
 	newADC = func(ctx context.Context) (credentials.PerRPCCredentials, error) {
-		return oauth.NewApplicationDefault(ctx)
+		return oauth.NewApplicationDefault(ctx, defaultCloudPlatformScope)
 	}
 )
 
diff --git a/experimental/stats/metricregistry.go b/experimental/stats/metricregistry.go
index 1d827dd5..ad75313a 100644
--- a/experimental/stats/metricregistry.go
+++ b/experimental/stats/metricregistry.go
@@ -23,6 +23,7 @@ import (
 
 	"google.golang.org/grpc/grpclog"
 	"google.golang.org/grpc/internal"
+	"google.golang.org/grpc/stats"
 )
 
 func init() {
@@ -34,7 +35,7 @@ var logger = grpclog.Component("metrics-registry")
 // DefaultMetrics are the default metrics registered through global metrics
 // registry. This is written to at initialization time only, and is read only
 // after initialization.
-var DefaultMetrics = NewMetrics()
+var DefaultMetrics = stats.NewMetricSet()
 
 // MetricDescriptor is the data for a registered metric.
 type MetricDescriptor struct {
@@ -42,7 +43,7 @@ type MetricDescriptor struct {
 	// (including any per call metrics). See
 	// https://github.com/grpc/proposal/blob/master/A79-non-per-call-metrics-architecture.md#metric-instrument-naming-conventions
 	// for metric naming conventions.
-	Name Metric
+	Name string
 	// The description of this metric.
 	Description string
 	// The unit (e.g. entries, seconds) of this metric.
@@ -154,27 +155,27 @@ func (h *Int64GaugeHandle) Record(recorder MetricsRecorder, incr int64, labels .
 }
 
 // registeredMetrics are the registered metric descriptor names.
-var registeredMetrics = make(map[Metric]bool)
+var registeredMetrics = make(map[string]bool)
 
 // metricsRegistry contains all of the registered metrics.
 //
 // This is written to only at init time, and read only after that.
-var metricsRegistry = make(map[Metric]*MetricDescriptor)
+var metricsRegistry = make(map[string]*MetricDescriptor)
 
 // DescriptorForMetric returns the MetricDescriptor from the global registry.
 //
 // Returns nil if MetricDescriptor not present.
-func DescriptorForMetric(metric Metric) *MetricDescriptor {
-	return metricsRegistry[metric]
+func DescriptorForMetric(metricName string) *MetricDescriptor {
+	return metricsRegistry[metricName]
 }
 
-func registerMetric(name Metric, def bool) {
-	if registeredMetrics[name] {
-		logger.Fatalf("metric %v already registered", name)
+func registerMetric(metricName string, def bool) {
+	if registeredMetrics[metricName] {
+		logger.Fatalf("metric %v already registered", metricName)
 	}
-	registeredMetrics[name] = true
+	registeredMetrics[metricName] = true
 	if def {
-		DefaultMetrics = DefaultMetrics.Add(name)
+		DefaultMetrics = DefaultMetrics.Add(metricName)
 	}
 }
 
@@ -256,8 +257,8 @@ func snapshotMetricsRegistryForTesting() func() {
 	oldRegisteredMetrics := registeredMetrics
 	oldMetricsRegistry := metricsRegistry
 
-	registeredMetrics = make(map[Metric]bool)
-	metricsRegistry = make(map[Metric]*MetricDescriptor)
+	registeredMetrics = make(map[string]bool)
+	metricsRegistry = make(map[string]*MetricDescriptor)
 	maps.Copy(registeredMetrics, registeredMetrics)
 	maps.Copy(metricsRegistry, metricsRegistry)
 
diff --git a/experimental/stats/metrics.go b/experimental/stats/metrics.go
index 3221f7a6..bf9e7f98 100644
--- a/experimental/stats/metrics.go
+++ b/experimental/stats/metrics.go
@@ -19,8 +19,6 @@
 // Package stats contains experimental metrics/stats API's.
 package stats
 
-import "maps"
-
 // MetricsRecorder records on metrics derived from metric registry.
 type MetricsRecorder interface {
 	// RecordInt64Count records the measurement alongside labels on the int
@@ -39,76 +37,3 @@ type MetricsRecorder interface {
 	// gauge associated with the provided handle.
 	RecordInt64Gauge(handle *Int64GaugeHandle, incr int64, labels ...string)
 }
-
-// Metric is an identifier for a metric.
-type Metric string
-
-// Metrics is a set of metrics to record. Once created, Metrics is immutable,
-// however Add and Remove can make copies with specific metrics added or
-// removed, respectively.
-//
-// Do not construct directly; use NewMetrics instead.
-type Metrics struct {
-	// metrics are the set of metrics to initialize.
-	metrics map[Metric]bool
-}
-
-// NewMetrics returns a Metrics containing Metrics.
-func NewMetrics(metrics ...Metric) *Metrics {
-	newMetrics := make(map[Metric]bool)
-	for _, metric := range metrics {
-		newMetrics[metric] = true
-	}
-	return &Metrics{
-		metrics: newMetrics,
-	}
-}
-
-// Metrics returns the metrics set. The returned map is read-only and must not
-// be modified.
-func (m *Metrics) Metrics() map[Metric]bool {
-	return m.metrics
-}
-
-// Add adds the metrics to the metrics set and returns a new copy with the
-// additional metrics.
-func (m *Metrics) Add(metrics ...Metric) *Metrics {
-	newMetrics := make(map[Metric]bool)
-	for metric := range m.metrics {
-		newMetrics[metric] = true
-	}
-
-	for _, metric := range metrics {
-		newMetrics[metric] = true
-	}
-	return &Metrics{
-		metrics: newMetrics,
-	}
-}
-
-// Join joins the metrics passed in with the metrics set, and returns a new copy
-// with the merged metrics.
-func (m *Metrics) Join(metrics *Metrics) *Metrics {
-	newMetrics := make(map[Metric]bool)
-	maps.Copy(newMetrics, m.metrics)
-	maps.Copy(newMetrics, metrics.metrics)
-	return &Metrics{
-		metrics: newMetrics,
-	}
-}
-
-// Remove removes the metrics from the metrics set and returns a new copy with
-// the metrics removed.
-func (m *Metrics) Remove(metrics ...Metric) *Metrics {
-	newMetrics := make(map[Metric]bool)
-	for metric := range m.metrics {
-		newMetrics[metric] = true
-	}
-
-	for _, metric := range metrics {
-		delete(newMetrics, metric)
-	}
-	return &Metrics{
-		metrics: newMetrics,
-	}
-}
diff --git a/stats/metrics.go b/stats/metrics.go
new file mode 100644
index 00000000..641c8e97
--- /dev/null
+++ b/stats/metrics.go
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2024 gRPC 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 stats
+
+import "maps"
+
+// MetricSet is a set of metrics to record. Once created, MetricSet is immutable,
+// however Add and Remove can make copies with specific metrics added or
+// removed, respectively.
+//
+// Do not construct directly; use NewMetricSet instead.
+type MetricSet struct {
+	// metrics are the set of metrics to initialize.
+	metrics map[string]bool
+}
+
+// NewMetricSet returns a MetricSet containing metricNames.
+func NewMetricSet(metricNames ...string) *MetricSet {
+	newMetrics := make(map[string]bool)
+	for _, metric := range metricNames {
+		newMetrics[metric] = true
+	}
+	return &MetricSet{metrics: newMetrics}
+}
+
+// Metrics returns the metrics set. The returned map is read-only and must not
+// be modified.
+func (m *MetricSet) Metrics() map[string]bool {
+	return m.metrics
+}
+
+// Add adds the metricNames to the metrics set and returns a new copy with the
+// additional metrics.
+func (m *MetricSet) Add(metricNames ...string) *MetricSet {
+	newMetrics := make(map[string]bool)
+	for metric := range m.metrics {
+		newMetrics[metric] = true
+	}
+
+	for _, metric := range metricNames {
+		newMetrics[metric] = true
+	}
+	return &MetricSet{metrics: newMetrics}
+}
+
+// Join joins the metrics passed in with the metrics set, and returns a new copy
+// with the merged metrics.
+func (m *MetricSet) Join(metrics *MetricSet) *MetricSet {
+	newMetrics := make(map[string]bool)
+	maps.Copy(newMetrics, m.metrics)
+	maps.Copy(newMetrics, metrics.metrics)
+	return &MetricSet{metrics: newMetrics}
+}
+
+// Remove removes the metricNames from the metrics set and returns a new copy
+// with the metrics removed.
+func (m *MetricSet) Remove(metricNames ...string) *MetricSet {
+	newMetrics := make(map[string]bool)
+	for metric := range m.metrics {
+		newMetrics[metric] = true
+	}
+
+	for _, metric := range metricNames {
+		delete(newMetrics, metric)
+	}
+	return &MetricSet{metrics: newMetrics}
+}
diff --git a/stats/opentelemetry/client_metrics.go b/stats/opentelemetry/client_metrics.go
index 4af7f933..265791e5 100644
--- a/stats/opentelemetry/client_metrics.go
+++ b/stats/opentelemetry/client_metrics.go
@@ -260,18 +260,19 @@ func (h *clientStatsHandler) processRPCEnd(ctx context.Context, ai *attemptInfo,
 }
 
 const (
-	// ClientAttemptStarted is the number of client call attempts started.
-	ClientAttemptStarted estats.Metric = "grpc.client.attempt.started"
-	// ClientAttemptDuration is the end-to-end time taken to complete a client
-	// call attempt.
-	ClientAttemptDuration estats.Metric = "grpc.client.attempt.duration"
-	// ClientAttemptSentCompressedTotalMessageSize is the compressed message
-	// bytes sent per client call attempt.
-	ClientAttemptSentCompressedTotalMessageSize estats.Metric = "grpc.client.attempt.sent_total_compressed_message_size"
-	// ClientAttemptRcvdCompressedTotalMessageSize is the compressed message
-	// bytes received per call attempt.
-	ClientAttemptRcvdCompressedTotalMessageSize estats.Metric = "grpc.client.attempt.rcvd_total_compressed_message_size"
-	// ClientCallDuration is the time taken by gRPC to complete an RPC from
-	// application's perspective.
-	ClientCallDuration estats.Metric = "grpc.client.call.duration"
+	// ClientAttemptStartedMetricName is the number of client call attempts
+	// started.
+	ClientAttemptStartedMetricName string = "grpc.client.attempt.started"
+	// ClientAttemptDurationMetricName is the end-to-end time taken to complete
+	// a client call attempt.
+	ClientAttemptDurationMetricName string = "grpc.client.attempt.duration"
+	// ClientAttemptSentCompressedTotalMessageSizeMetricName is the compressed
+	// message bytes sent per client call attempt.
+	ClientAttemptSentCompressedTotalMessageSizeMetricName string = "grpc.client.attempt.sent_total_compressed_message_size"
+	// ClientAttemptRcvdCompressedTotalMessageSizeMetricName is the compressed
+	// message bytes received per call attempt.
+	ClientAttemptRcvdCompressedTotalMessageSizeMetricName string = "grpc.client.attempt.rcvd_total_compressed_message_size"
+	// ClientCallDurationMetricName is the time taken by gRPC to complete an RPC
+	// from application's perspective.
+	ClientCallDurationMetricName string = "grpc.client.call.duration"
 )
diff --git a/stats/opentelemetry/opentelemetry.go b/stats/opentelemetry/opentelemetry.go
index cc5ad387..dcc42477 100644
--- a/stats/opentelemetry/opentelemetry.go
+++ b/stats/opentelemetry/opentelemetry.go
@@ -16,6 +16,10 @@
 
 // Package opentelemetry implements opentelemetry instrumentation code for
 // gRPC-Go clients and servers.
+//
+// For details on configuring opentelemetry and various instruments that this
+// package creates, see
+// [gRPC OpenTelemetry Metrics](https://grpc.io/docs/guides/opentelemetry-metrics/).
 package opentelemetry
 
 import (
@@ -28,6 +32,7 @@ import (
 	estats "google.golang.org/grpc/experimental/stats"
 	"google.golang.org/grpc/grpclog"
 	"google.golang.org/grpc/internal"
+	"google.golang.org/grpc/stats"
 	otelinternal "google.golang.org/grpc/stats/opentelemetry/internal"
 
 	otelattribute "go.opentelemetry.io/otel/attribute"
@@ -57,27 +62,28 @@ type Options struct {
 type MetricsOptions struct {
 	// MeterProvider is the MeterProvider instance that will be used to create
 	// instruments. To enable metrics collection, set a meter provider. If
-	// unset, no metrics will be recorded. Any implementation knobs (i.e. views,
-	// bounds) set in the MeterProvider take precedence over the API calls from
-	// this interface. (i.e. it will create default views for unset views).
+	// unset, no metrics will be recorded.
 	MeterProvider otelmetric.MeterProvider
 
 	// Metrics are the metrics to instrument. Will create instrument and record telemetry
 	// for corresponding metric supported by the client and server
 	// instrumentation components if applicable. If not set, the default metrics
 	// will be recorded.
-	Metrics *estats.Metrics
-
-	// MethodAttributeFilter is to record the method name of RPCs handled by
-	// grpc.UnknownServiceHandler, but take care to limit the values allowed, as
-	// allowing too many will increase cardinality and could cause severe memory
-	// or performance problems. On Client Side, pass a
-	// grpc.StaticMethodCallOption as a call option into Invoke or NewStream.
-	// This only applies for server side metrics.
+	Metrics *stats.MetricSet
+
+	// MethodAttributeFilter is a function that determines whether to record the
+	// method name of RPCs as an attribute, or to bucket into "other". Take care
+	// to limit the values allowed, as allowing too many will increase
+	// cardinality and could cause severe memory or performance problems.
+	//
+	// This only applies for server-side metrics.  For clients, to record the
+	// method name in the attributes, pass grpc.StaticMethodCallOption to Invoke
+	// or NewStream. Note that when using protobuf generated clients, this
+	// CallOption is included automatically.
 	MethodAttributeFilter func(string) bool
 
-	// OptionalLabels are labels received from LB Policies that this component
-	// should add to metrics that record after receiving incoming metadata.
+	// OptionalLabels specifies a list of optional labels to enable on any
+	// metrics that support them.
 	OptionalLabels []string
 
 	// pluginOption is used to get labels to attach to certain metrics, if set.
@@ -207,7 +213,7 @@ type serverMetrics struct {
 	callDuration otelmetric.Float64Histogram
 }
 
-func createInt64Counter(setOfMetrics map[estats.Metric]bool, metricName estats.Metric, meter otelmetric.Meter, options ...otelmetric.Int64CounterOption) otelmetric.Int64Counter {
+func createInt64Counter(setOfMetrics map[string]bool, metricName string, meter otelmetric.Meter, options ...otelmetric.Int64CounterOption) otelmetric.Int64Counter {
 	if _, ok := setOfMetrics[metricName]; !ok {
 		return noop.Int64Counter{}
 	}
@@ -219,7 +225,7 @@ func createInt64Counter(setOfMetrics map[estats.Metric]bool, metricName estats.M
 	return ret
 }
 
-func createFloat64Counter(setOfMetrics map[estats.Metric]bool, metricName estats.Metric, meter otelmetric.Meter, options ...otelmetric.Float64CounterOption) otelmetric.Float64Counter {
+func createFloat64Counter(setOfMetrics map[string]bool, metricName string, meter otelmetric.Meter, options ...otelmetric.Float64CounterOption) otelmetric.Float64Counter {
 	if _, ok := setOfMetrics[metricName]; !ok {
 		return noop.Float64Counter{}
 	}
@@ -231,7 +237,7 @@ func createFloat64Counter(setOfMetrics map[estats.Metric]bool, metricName estats
 	return ret
 }
 
-func createInt64Histogram(setOfMetrics map[estats.Metric]bool, metricName estats.Metric, meter otelmetric.Meter, options ...otelmetric.Int64HistogramOption) otelmetric.Int64Histogram {
+func createInt64Histogram(setOfMetrics map[string]bool, metricName string, meter otelmetric.Meter, options ...otelmetric.Int64HistogramOption) otelmetric.Int64Histogram {
 	if _, ok := setOfMetrics[metricName]; !ok {
 		return noop.Int64Histogram{}
 	}
@@ -243,7 +249,7 @@ func createInt64Histogram(setOfMetrics map[estats.Metric]bool, metricName estats
 	return ret
 }
 
-func createFloat64Histogram(setOfMetrics map[estats.Metric]bool, metricName estats.Metric, meter otelmetric.Meter, options ...otelmetric.Float64HistogramOption) otelmetric.Float64Histogram {
+func createFloat64Histogram(setOfMetrics map[string]bool, metricName string, meter otelmetric.Meter, options ...otelmetric.Float64HistogramOption) otelmetric.Float64Histogram {
 	if _, ok := setOfMetrics[metricName]; !ok {
 		return noop.Float64Histogram{}
 	}
@@ -255,7 +261,7 @@ func createFloat64Histogram(setOfMetrics map[estats.Metric]bool, metricName esta
 	return ret
 }
 
-func createInt64Gauge(setOfMetrics map[estats.Metric]bool, metricName estats.Metric, meter otelmetric.Meter, options ...otelmetric.Int64GaugeOption) otelmetric.Int64Gauge {
+func createInt64Gauge(setOfMetrics map[string]bool, metricName string, meter otelmetric.Meter, options ...otelmetric.Int64GaugeOption) otelmetric.Int64Gauge {
 	if _, ok := setOfMetrics[metricName]; !ok {
 		return noop.Int64Gauge{}
 	}
@@ -298,7 +304,7 @@ type registryMetrics struct {
 	optionalLabels []string
 }
 
-func (rm *registryMetrics) registerMetrics(metrics *estats.Metrics, meter otelmetric.Meter) {
+func (rm *registryMetrics) registerMetrics(metrics *stats.MetricSet, meter otelmetric.Meter) {
 	rm.intCounts = make(map[*estats.MetricDescriptor]otelmetric.Int64Counter)
 	rm.floatCounts = make(map[*estats.MetricDescriptor]otelmetric.Float64Counter)
 	rm.intHistos = make(map[*estats.MetricDescriptor]otelmetric.Int64Histogram)
@@ -379,12 +385,12 @@ var (
 	// DefaultSizeBounds are the default bounds for metrics which record size.
 	DefaultSizeBounds = []float64{0, 1024, 2048, 4096, 16384, 65536, 262144, 1048576, 4194304, 16777216, 67108864, 268435456, 1073741824, 4294967296}
 	// defaultPerCallMetrics are the default metrics provided by this module.
-	defaultPerCallMetrics = estats.NewMetrics(ClientAttemptStarted, ClientAttemptDuration, ClientAttemptSentCompressedTotalMessageSize, ClientAttemptRcvdCompressedTotalMessageSize, ClientCallDuration, ServerCallStarted, ServerCallSentCompressedTotalMessageSize, ServerCallRcvdCompressedTotalMessageSize, ServerCallDuration)
+	defaultPerCallMetrics = stats.NewMetricSet(ClientAttemptStartedMetricName, ClientAttemptDurationMetricName, ClientAttemptSentCompressedTotalMessageSizeMetricName, ClientAttemptRcvdCompressedTotalMessageSizeMetricName, ClientCallDurationMetricName, ServerCallStartedMetricName, ServerCallSentCompressedTotalMessageSizeMetricName, ServerCallRcvdCompressedTotalMessageSizeMetricName, ServerCallDurationMetricName)
 )
 
 // DefaultMetrics returns a set of default OpenTelemetry metrics.
 //
 // This should only be invoked after init time.
-func DefaultMetrics() *estats.Metrics {
+func DefaultMetrics() *stats.MetricSet {
 	return defaultPerCallMetrics.Join(estats.DefaultMetrics)
 }
diff --git a/stats/opentelemetry/server_metrics.go b/stats/opentelemetry/server_metrics.go
index eaea559b..4765afa8 100644
--- a/stats/opentelemetry/server_metrics.go
+++ b/stats/opentelemetry/server_metrics.go
@@ -264,15 +264,15 @@ func (h *serverStatsHandler) processRPCEnd(ctx context.Context, ai *attemptInfo,
 }
 
 const (
-	// ServerCallStarted is the number of server calls started.
-	ServerCallStarted estats.Metric = "grpc.server.call.started"
-	// ServerCallSentCompressedTotalMessageSize is the compressed message bytes
-	// sent per server call.
-	ServerCallSentCompressedTotalMessageSize estats.Metric = "grpc.server.call.sent_total_compressed_message_size"
-	// ServerCallRcvdCompressedTotalMessageSize is the compressed message bytes
-	// received per server call.
-	ServerCallRcvdCompressedTotalMessageSize estats.Metric = "grpc.server.call.rcvd_total_compressed_message_size"
-	// ServerCallDuration is the end-to-end time taken to complete a call from
-	// server transport's perspective.
-	ServerCallDuration estats.Metric = "grpc.server.call.duration"
+	// ServerCallStartedMetricName is the number of server calls started.
+	ServerCallStartedMetricName string = "grpc.server.call.started"
+	// ServerCallSentCompressedTotalMessageSizeMetricName is the compressed
+	// message bytes sent per server call.
+	ServerCallSentCompressedTotalMessageSizeMetricName string = "grpc.server.call.sent_total_compressed_message_size"
+	// ServerCallRcvdCompressedTotalMessageSizeMetricName is the compressed
+	// message bytes received per server call.
+	ServerCallRcvdCompressedTotalMessageSizeMetricName string = "grpc.server.call.rcvd_total_compressed_message_size"
+	// ServerCallDurationMetricName is the end-to-end time taken to complete a
+	// call from server transport's perspective.
+	ServerCallDurationMetricName string = "grpc.server.call.duration"
 )



Test patch:
diff --git a/internal/testutils/stats/test_metrics_recorder.go b/internal/testutils/stats/test_metrics_recorder.go
index e13013e3..e1a03b8d 100644
--- a/internal/testutils/stats/test_metrics_recorder.go
+++ b/internal/testutils/stats/test_metrics_recorder.go
@@ -44,7 +44,7 @@ type TestMetricsRecorder struct {
 	// mu protects data.
 	mu sync.Mutex
 	// data is the most recent update for each metric name.
-	data map[estats.Metric]float64
+	data map[string]float64
 }
 
 // NewTestMetricsRecorder returns a new TestMetricsRecorder.
@@ -56,7 +56,7 @@ func NewTestMetricsRecorder() *TestMetricsRecorder {
 		floatHistoCh: testutils.NewChannelWithSize(10),
 		intGaugeCh:   testutils.NewChannelWithSize(10),
 
-		data: make(map[estats.Metric]float64),
+		data: make(map[string]float64),
 	}
 }
 
@@ -65,7 +65,7 @@ func NewTestMetricsRecorder() *TestMetricsRecorder {
 func (r *TestMetricsRecorder) Metric(name string) (float64, bool) {
 	r.mu.Lock()
 	defer r.mu.Unlock()
-	data, ok := r.data[estats.Metric(name)]
+	data, ok := r.data[name]
 	return data, ok
 }
 
@@ -73,7 +73,7 @@ func (r *TestMetricsRecorder) Metric(name string) (float64, bool) {
 func (r *TestMetricsRecorder) ClearMetrics() {
 	r.mu.Lock()
 	defer r.mu.Unlock()
-	r.data = make(map[estats.Metric]float64)
+	r.data = make(map[string]float64)
 }
 
 // MetricsData represents data associated with a metric.
diff --git a/stats/opentelemetry/example_test.go b/stats/opentelemetry/example_test.go
index 66676f3c..e87e4ebb 100644
--- a/stats/opentelemetry/example_test.go
+++ b/stats/opentelemetry/example_test.go
@@ -19,7 +19,7 @@ package opentelemetry_test
 import (
 	"google.golang.org/grpc"
 	"google.golang.org/grpc/credentials/insecure"
-	estats "google.golang.org/grpc/experimental/stats"
+	"google.golang.org/grpc/stats"
 	"google.golang.org/grpc/stats/opentelemetry"
 
 	"go.opentelemetry.io/otel/sdk/metric"
@@ -88,7 +88,7 @@ func ExampleMetrics_excludeSome() {
 	// To exclude specific metrics, initialize Options as follows:
 	opts := opentelemetry.Options{
 		MetricsOptions: opentelemetry.MetricsOptions{
-			Metrics: opentelemetry.DefaultMetrics().Remove(opentelemetry.ClientAttemptDuration, opentelemetry.ClientAttemptRcvdCompressedTotalMessageSize),
+			Metrics: opentelemetry.DefaultMetrics().Remove(opentelemetry.ClientAttemptDurationMetricName, opentelemetry.ClientAttemptRcvdCompressedTotalMessageSizeMetricName),
 		},
 	}
 	do := opentelemetry.DialOption(opts)
@@ -103,7 +103,7 @@ func ExampleMetrics_disableAll() {
 	// To disable all metrics, initialize Options as follows:
 	opts := opentelemetry.Options{
 		MetricsOptions: opentelemetry.MetricsOptions{
-			Metrics: estats.NewMetrics(), // Distinct to nil, which creates default metrics. This empty set creates no metrics.
+			Metrics: stats.NewMetricSet(), // Distinct to nil, which creates default metrics. This empty set creates no metrics.
 		},
 	}
 	do := opentelemetry.DialOption(opts)
@@ -118,7 +118,7 @@ func ExampleMetrics_enableSome() {
 	// To only create specific metrics, initialize Options as follows:
 	opts := opentelemetry.Options{
 		MetricsOptions: opentelemetry.MetricsOptions{
-			Metrics: estats.NewMetrics(opentelemetry.ClientAttemptDuration, opentelemetry.ClientAttemptRcvdCompressedTotalMessageSize), // only create these metrics
+			Metrics: stats.NewMetricSet(opentelemetry.ClientAttemptDurationMetricName, opentelemetry.ClientAttemptRcvdCompressedTotalMessageSizeMetricName), // only create these metrics
 		},
 	}
 	do := opentelemetry.DialOption(opts)