google.golang.org/grpc@v1.72.2/stats/opentelemetry/csm/observability_test.go (about)

     1  /*
     2   *
     3   * Copyright 2024 gRPC authors.
     4   *
     5   * Licensed under the Apache License, Version 2.0 (the "License");
     6   * you may not use this file except in compliance with the License.
     7   * You may obtain a copy of the License at
     8   *
     9   *     http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the License for the specific language governing permissions and
    15   * limitations under the License.
    16   *
    17   */
    18  
    19  package csm
    20  
    21  import (
    22  	"context"
    23  	"errors"
    24  	"io"
    25  	"os"
    26  	"testing"
    27  
    28  	"go.opentelemetry.io/otel/attribute"
    29  	"go.opentelemetry.io/otel/sdk/metric"
    30  	"go.opentelemetry.io/otel/sdk/metric/metricdata"
    31  	"google.golang.org/grpc"
    32  	"google.golang.org/grpc/encoding/gzip"
    33  	istats "google.golang.org/grpc/internal/stats"
    34  	"google.golang.org/grpc/internal/stubserver"
    35  	testgrpc "google.golang.org/grpc/interop/grpc_testing"
    36  	testpb "google.golang.org/grpc/interop/grpc_testing"
    37  	"google.golang.org/grpc/metadata"
    38  	"google.golang.org/grpc/stats/opentelemetry"
    39  	itestutils "google.golang.org/grpc/stats/opentelemetry/internal/testutils"
    40  )
    41  
    42  // setupEnv configures the environment for CSM Observability Testing. It sets
    43  // the bootstrap env var to a bootstrap file with a nodeID provided. It sets CSM
    44  // Env Vars as well, and mocks the resource detector's returned attribute set to
    45  // simulate the environment. It registers a cleanup function on the provided t
    46  // to restore the environment to its original state.
    47  func setupEnv(t *testing.T, resourceDetectorEmissions map[string]string, meshID, csmCanonicalServiceName, csmWorkloadName string) {
    48  	oldCSMMeshID, csmMeshIDPresent := os.LookupEnv("CSM_MESH_ID")
    49  	oldCSMCanonicalServiceName, csmCanonicalServiceNamePresent := os.LookupEnv("CSM_CANONICAL_SERVICE_NAME")
    50  	oldCSMWorkloadName, csmWorkloadNamePresent := os.LookupEnv("CSM_WORKLOAD_NAME")
    51  	os.Setenv("CSM_MESH_ID", meshID)
    52  	os.Setenv("CSM_CANONICAL_SERVICE_NAME", csmCanonicalServiceName)
    53  	os.Setenv("CSM_WORKLOAD_NAME", csmWorkloadName)
    54  
    55  	var attributes []attribute.KeyValue
    56  	for k, v := range resourceDetectorEmissions {
    57  		attributes = append(attributes, attribute.String(k, v))
    58  	}
    59  	// Return the attributes configured as part of the test in place
    60  	// of reading from resource.
    61  	attrSet := attribute.NewSet(attributes...)
    62  	origGetAttrSet := getAttrSetFromResourceDetector
    63  	getAttrSetFromResourceDetector = func(context.Context) *attribute.Set {
    64  		return &attrSet
    65  	}
    66  	t.Cleanup(func() {
    67  		if csmMeshIDPresent {
    68  			os.Setenv("CSM_MESH_ID", oldCSMMeshID)
    69  		} else {
    70  			os.Unsetenv("CSM_MESH_ID")
    71  		}
    72  		if csmCanonicalServiceNamePresent {
    73  			os.Setenv("CSM_CANONICAL_SERVICE_NAME", oldCSMCanonicalServiceName)
    74  		} else {
    75  			os.Unsetenv("CSM_CANONICAL_SERVICE_NAME")
    76  		}
    77  		if csmWorkloadNamePresent {
    78  			os.Setenv("CSM_WORKLOAD_NAME", oldCSMWorkloadName)
    79  		} else {
    80  			os.Unsetenv("CSM_WORKLOAD_NAME")
    81  		}
    82  
    83  		getAttrSetFromResourceDetector = origGetAttrSet
    84  	})
    85  }
    86  
    87  // TestCSMPluginOptionUnary tests the CSM Plugin Option and labels. It
    88  // configures the environment for the CSM Plugin Option to read from. It then
    89  // configures a system with a gRPC Client and gRPC server with the OpenTelemetry
    90  // Dial and Server Option configured with a CSM Plugin Option with a certain
    91  // unary handler set to induce different ways of setting metadata exchange
    92  // labels, and makes a Unary RPC. This RPC should cause certain recording for
    93  // each registered metric observed through a Manual Metrics Reader on the
    94  // provided OpenTelemetry SDK's Meter Provider. The CSM Labels emitted from the
    95  // plugin option should be attached to the relevant metrics.
    96  func (s) TestCSMPluginOptionUnary(t *testing.T) {
    97  	resourceDetectorEmissions := map[string]string{
    98  		"cloud.platform":     "gcp_kubernetes_engine",
    99  		"cloud.region":       "cloud_region_val", // availability_zone isn't present, so this should become location
   100  		"cloud.account.id":   "cloud_account_id_val",
   101  		"k8s.namespace.name": "k8s_namespace_name_val",
   102  		"k8s.cluster.name":   "k8s_cluster_name_val",
   103  	}
   104  	const meshID = "mesh_id"
   105  	const csmCanonicalServiceName = "csm_canonical_service_name"
   106  	const csmWorkloadName = "csm_workload_name"
   107  	setupEnv(t, resourceDetectorEmissions, meshID, csmCanonicalServiceName, csmWorkloadName)
   108  
   109  	attributesWant := map[string]string{
   110  		"csm.workload_canonical_service": csmCanonicalServiceName, // from env
   111  		"csm.mesh_id":                    "mesh_id",               // from bootstrap env var
   112  
   113  		// No xDS Labels - this happens in a test below.
   114  
   115  		"csm.remote_workload_type":              "gcp_kubernetes_engine",
   116  		"csm.remote_workload_canonical_service": csmCanonicalServiceName,
   117  		"csm.remote_workload_project_id":        "cloud_account_id_val",
   118  		"csm.remote_workload_cluster_name":      "k8s_cluster_name_val",
   119  		"csm.remote_workload_namespace_name":    "k8s_namespace_name_val",
   120  		"csm.remote_workload_location":          "cloud_region_val",
   121  		"csm.remote_workload_name":              csmWorkloadName,
   122  	}
   123  
   124  	var csmLabels []attribute.KeyValue
   125  	for k, v := range attributesWant {
   126  		csmLabels = append(csmLabels, attribute.String(k, v))
   127  	}
   128  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   129  	defer cancel()
   130  	tests := []struct {
   131  		name string
   132  		// To test the different operations for Unary RPC's from the interceptor
   133  		// level that can plumb metadata exchange header in.
   134  		unaryCallFunc func(ctx context.Context, in *testpb.SimpleRequest) (*testpb.SimpleResponse, error)
   135  		opts          itestutils.MetricDataOptions
   136  	}{
   137  		{
   138  			name: "normal-flow",
   139  			unaryCallFunc: func(_ context.Context, in *testpb.SimpleRequest) (*testpb.SimpleResponse, error) {
   140  				return &testpb.SimpleResponse{Payload: &testpb.Payload{
   141  					Body: make([]byte, len(in.GetPayload().GetBody())),
   142  				}}, nil
   143  			},
   144  			opts: itestutils.MetricDataOptions{
   145  				CSMLabels:                  csmLabels,
   146  				UnaryCompressedMessageSize: float64(57),
   147  			},
   148  		},
   149  		{
   150  			name: "trailers-only",
   151  			unaryCallFunc: func(context.Context, *testpb.SimpleRequest) (*testpb.SimpleResponse, error) {
   152  				return nil, errors.New("some error") // return an error and no message - this triggers trailers only - no messages or headers sent
   153  			},
   154  			opts: itestutils.MetricDataOptions{
   155  				CSMLabels:       csmLabels,
   156  				UnaryCallFailed: true,
   157  			},
   158  		},
   159  		{
   160  			name: "set-header",
   161  			unaryCallFunc: func(ctx context.Context, in *testpb.SimpleRequest) (*testpb.SimpleResponse, error) {
   162  				grpc.SetHeader(ctx, metadata.New(map[string]string{"some-metadata": "some-metadata-val"}))
   163  
   164  				return &testpb.SimpleResponse{Payload: &testpb.Payload{
   165  					Body: make([]byte, len(in.GetPayload().GetBody())),
   166  				}}, nil
   167  			},
   168  			opts: itestutils.MetricDataOptions{
   169  				CSMLabels:                  csmLabels,
   170  				UnaryCompressedMessageSize: float64(57),
   171  			},
   172  		},
   173  		{
   174  			name: "send-header",
   175  			unaryCallFunc: func(ctx context.Context, in *testpb.SimpleRequest) (*testpb.SimpleResponse, error) {
   176  				grpc.SendHeader(ctx, metadata.New(map[string]string{"some-metadata": "some-metadata-val"}))
   177  
   178  				return &testpb.SimpleResponse{Payload: &testpb.Payload{
   179  					Body: make([]byte, len(in.GetPayload().GetBody())),
   180  				}}, nil
   181  			},
   182  			opts: itestutils.MetricDataOptions{
   183  				CSMLabels:                  csmLabels,
   184  				UnaryCompressedMessageSize: float64(57),
   185  			},
   186  		},
   187  		{
   188  			name: "send-msg",
   189  			unaryCallFunc: func(_ context.Context, in *testpb.SimpleRequest) (*testpb.SimpleResponse, error) {
   190  				return &testpb.SimpleResponse{Payload: &testpb.Payload{
   191  					Body: make([]byte, len(in.GetPayload().GetBody())),
   192  				}}, nil
   193  			},
   194  			opts: itestutils.MetricDataOptions{
   195  				CSMLabels:                  csmLabels,
   196  				UnaryCompressedMessageSize: float64(57),
   197  			},
   198  		},
   199  	}
   200  
   201  	for _, test := range tests {
   202  		t.Run(test.name, func(t *testing.T) {
   203  			reader := metric.NewManualReader()
   204  			provider := metric.NewMeterProvider(metric.WithReader(reader))
   205  			ss := &stubserver.StubServer{UnaryCallF: test.unaryCallFunc}
   206  			po := newPluginOption(ctx)
   207  			sopts := []grpc.ServerOption{
   208  				serverOptionWithCSMPluginOption(opentelemetry.Options{
   209  					MetricsOptions: opentelemetry.MetricsOptions{
   210  						MeterProvider: provider,
   211  						Metrics:       opentelemetry.DefaultMetrics(),
   212  					}}, po),
   213  			}
   214  			dopts := []grpc.DialOption{dialOptionWithCSMPluginOption(opentelemetry.Options{
   215  				MetricsOptions: opentelemetry.MetricsOptions{
   216  					MeterProvider:  provider,
   217  					Metrics:        opentelemetry.DefaultMetrics(),
   218  					OptionalLabels: []string{"csm.service_name", "csm.service_namespace_name"},
   219  				},
   220  			}, po)}
   221  			if err := ss.Start(sopts, dopts...); err != nil {
   222  				t.Fatalf("Error starting endpoint server: %v", err)
   223  			}
   224  			defer ss.Stop()
   225  
   226  			var request *testpb.SimpleRequest
   227  			if test.opts.UnaryCompressedMessageSize != 0 {
   228  				request = &testpb.SimpleRequest{Payload: &testpb.Payload{
   229  					Body: make([]byte, 10000),
   230  				}}
   231  			}
   232  			// Make a Unary RPC. These should cause certain metrics to be
   233  			// emitted, which should be able to be observed through the Metric
   234  			// Reader.
   235  			ss.Client.UnaryCall(ctx, request, grpc.UseCompressor(gzip.Name))
   236  			rm := &metricdata.ResourceMetrics{}
   237  			reader.Collect(ctx, rm)
   238  
   239  			gotMetrics := map[string]metricdata.Metrics{}
   240  			for _, sm := range rm.ScopeMetrics {
   241  				for _, m := range sm.Metrics {
   242  					gotMetrics[m.Name] = m
   243  				}
   244  			}
   245  
   246  			opts := test.opts
   247  			opts.Target = ss.Target
   248  			wantMetrics := itestutils.MetricDataUnary(opts)
   249  			gotMetrics = itestutils.WaitForServerMetrics(ctx, t, reader, gotMetrics, wantMetrics)
   250  			itestutils.CompareMetrics(t, gotMetrics, wantMetrics)
   251  		})
   252  	}
   253  }
   254  
   255  // TestCSMPluginOptionStreaming tests the CSM Plugin Option and labels. It
   256  // configures the environment for the CSM Plugin Option to read from. It then
   257  // configures a system with a gRPC Client and gRPC server with the OpenTelemetry
   258  // Dial and Server Option configured with a CSM Plugin Option with a certain
   259  // streaming handler set to induce different ways of setting metadata exchange
   260  // labels, and makes a Streaming RPC. This RPC should cause certain recording
   261  // for each registered metric observed through a Manual Metrics Reader on the
   262  // provided OpenTelemetry SDK's Meter Provider. The CSM Labels emitted from the
   263  // plugin option should be attached to the relevant metrics.
   264  func (s) TestCSMPluginOptionStreaming(t *testing.T) {
   265  	resourceDetectorEmissions := map[string]string{
   266  		"cloud.platform":     "gcp_kubernetes_engine",
   267  		"cloud.region":       "cloud_region_val", // availability_zone isn't present, so this should become location
   268  		"cloud.account.id":   "cloud_account_id_val",
   269  		"k8s.namespace.name": "k8s_namespace_name_val",
   270  		"k8s.cluster.name":   "k8s_cluster_name_val",
   271  	}
   272  	const meshID = "mesh_id"
   273  	const csmCanonicalServiceName = "csm_canonical_service_name"
   274  	const csmWorkloadName = "csm_workload_name"
   275  	setupEnv(t, resourceDetectorEmissions, meshID, csmCanonicalServiceName, csmWorkloadName)
   276  
   277  	attributesWant := map[string]string{
   278  		"csm.workload_canonical_service": csmCanonicalServiceName, // from env
   279  		"csm.mesh_id":                    "mesh_id",               // from bootstrap env var
   280  
   281  		// No xDS Labels - this happens in a test below.
   282  
   283  		"csm.remote_workload_type":              "gcp_kubernetes_engine",
   284  		"csm.remote_workload_canonical_service": csmCanonicalServiceName,
   285  		"csm.remote_workload_project_id":        "cloud_account_id_val",
   286  		"csm.remote_workload_cluster_name":      "k8s_cluster_name_val",
   287  		"csm.remote_workload_namespace_name":    "k8s_namespace_name_val",
   288  		"csm.remote_workload_location":          "cloud_region_val",
   289  		"csm.remote_workload_name":              csmWorkloadName,
   290  	}
   291  
   292  	var csmLabels []attribute.KeyValue
   293  	for k, v := range attributesWant {
   294  		csmLabels = append(csmLabels, attribute.String(k, v))
   295  	}
   296  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   297  	defer cancel()
   298  	tests := []struct {
   299  		name string
   300  		// To test the different operations for Streaming RPC's from the
   301  		// interceptor level that can plumb metadata exchange header in.
   302  		streamingCallFunc func(stream testgrpc.TestService_FullDuplexCallServer) error
   303  		opts              itestutils.MetricDataOptions
   304  	}{
   305  		{
   306  			name: "trailers-only",
   307  			streamingCallFunc: func(stream testgrpc.TestService_FullDuplexCallServer) error {
   308  				for {
   309  					if _, err := stream.Recv(); err == io.EOF {
   310  						return nil
   311  					}
   312  				}
   313  			},
   314  			opts: itestutils.MetricDataOptions{
   315  				CSMLabels: csmLabels,
   316  			},
   317  		},
   318  		{
   319  			name: "set-header",
   320  			streamingCallFunc: func(stream testgrpc.TestService_FullDuplexCallServer) error {
   321  				stream.SetHeader(metadata.New(map[string]string{"some-metadata": "some-metadata-val"}))
   322  				for {
   323  					if _, err := stream.Recv(); err == io.EOF {
   324  						return nil
   325  					}
   326  				}
   327  			},
   328  			opts: itestutils.MetricDataOptions{
   329  				CSMLabels: csmLabels,
   330  			},
   331  		},
   332  		{
   333  			name: "send-header",
   334  			streamingCallFunc: func(stream testgrpc.TestService_FullDuplexCallServer) error {
   335  				stream.SendHeader(metadata.New(map[string]string{"some-metadata": "some-metadata-val"}))
   336  				for {
   337  					if _, err := stream.Recv(); err == io.EOF {
   338  						return nil
   339  					}
   340  				}
   341  			},
   342  			opts: itestutils.MetricDataOptions{
   343  				CSMLabels: csmLabels,
   344  			},
   345  		},
   346  		{
   347  			name: "send-msg",
   348  			streamingCallFunc: func(stream testgrpc.TestService_FullDuplexCallServer) error {
   349  				stream.Send(&testpb.StreamingOutputCallResponse{Payload: &testpb.Payload{
   350  					Body: make([]byte, 10000),
   351  				}})
   352  				for {
   353  					if _, err := stream.Recv(); err == io.EOF {
   354  						return nil
   355  					}
   356  				}
   357  			},
   358  			opts: itestutils.MetricDataOptions{
   359  				CSMLabels:                      csmLabels,
   360  				StreamingCompressedMessageSize: float64(57),
   361  			},
   362  		},
   363  	}
   364  	for _, test := range tests {
   365  		t.Run(test.name, func(t *testing.T) {
   366  			reader := metric.NewManualReader()
   367  			provider := metric.NewMeterProvider(metric.WithReader(reader))
   368  			ss := &stubserver.StubServer{FullDuplexCallF: test.streamingCallFunc}
   369  			po := newPluginOption(ctx)
   370  			sopts := []grpc.ServerOption{
   371  				serverOptionWithCSMPluginOption(opentelemetry.Options{
   372  					MetricsOptions: opentelemetry.MetricsOptions{
   373  						MeterProvider: provider,
   374  						Metrics:       opentelemetry.DefaultMetrics(),
   375  					}}, po),
   376  			}
   377  			dopts := []grpc.DialOption{dialOptionWithCSMPluginOption(opentelemetry.Options{
   378  				MetricsOptions: opentelemetry.MetricsOptions{
   379  					MeterProvider:  provider,
   380  					Metrics:        opentelemetry.DefaultMetrics(),
   381  					OptionalLabels: []string{"csm.service_name", "csm.service_namespace_name"},
   382  				},
   383  			}, po)}
   384  			if err := ss.Start(sopts, dopts...); err != nil {
   385  				t.Fatalf("Error starting endpoint server: %v", err)
   386  			}
   387  			defer ss.Stop()
   388  
   389  			stream, err := ss.Client.FullDuplexCall(ctx, grpc.UseCompressor(gzip.Name))
   390  			if err != nil {
   391  				t.Fatalf("ss.Client.FullDuplexCall failed: %f", err)
   392  			}
   393  
   394  			if test.opts.StreamingCompressedMessageSize != 0 {
   395  				if err := stream.Send(&testpb.StreamingOutputCallRequest{Payload: &testpb.Payload{
   396  					Body: make([]byte, 10000),
   397  				}}); err != nil {
   398  					t.Fatalf("stream.Send failed")
   399  				}
   400  				if _, err := stream.Recv(); err != nil {
   401  					t.Fatalf("stream.Recv failed with error: %v", err)
   402  				}
   403  			}
   404  
   405  			stream.CloseSend()
   406  			if _, err = stream.Recv(); err != io.EOF {
   407  				t.Fatalf("stream.Recv received an unexpected error: %v, expected an EOF error", err)
   408  			}
   409  
   410  			rm := &metricdata.ResourceMetrics{}
   411  			reader.Collect(ctx, rm)
   412  
   413  			gotMetrics := map[string]metricdata.Metrics{}
   414  			for _, sm := range rm.ScopeMetrics {
   415  				for _, m := range sm.Metrics {
   416  					gotMetrics[m.Name] = m
   417  				}
   418  			}
   419  
   420  			opts := test.opts
   421  			opts.Target = ss.Target
   422  			wantMetrics := itestutils.MetricDataStreaming(opts)
   423  			gotMetrics = itestutils.WaitForServerMetrics(ctx, t, reader, gotMetrics, wantMetrics)
   424  			itestutils.CompareMetrics(t, gotMetrics, wantMetrics)
   425  		})
   426  	}
   427  }
   428  
   429  func unaryInterceptorAttachXDSLabels(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
   430  	ctx = istats.SetLabels(ctx, &istats.Labels{
   431  		TelemetryLabels: map[string]string{
   432  			// mock what the cluster impl would write here ("csm." xDS Labels
   433  			// and locality label)
   434  			"csm.service_name":           "service_name_val",
   435  			"csm.service_namespace_name": "service_namespace_val",
   436  
   437  			"grpc.lb.locality": "grpc.lb.locality_val",
   438  		},
   439  	})
   440  
   441  	// TagRPC will just see this in the context and set it's xDS Labels to point
   442  	// to this map on the heap.
   443  	return invoker(ctx, method, req, reply, cc, opts...)
   444  }
   445  
   446  // TestXDSLabels tests that xDS Labels get emitted from OpenTelemetry metrics.
   447  // This test configures OpenTelemetry with the CSM Plugin Option, and xDS
   448  // Optional Labels turned on. It then configures an interceptor to attach
   449  // labels, representing the cluster_impl picker. It then makes a unary RPC, and
   450  // expects xDS Labels labels to be attached to emitted relevant metrics. Full
   451  // xDS System alongside OpenTelemetry will be tested with interop. (there is a
   452  // test for xDS -> Stats handler and this tests -> OTel -> emission). It also
   453  // tests the optional per call locality label in the same manner.
   454  func (s) TestXDSLabels(t *testing.T) {
   455  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   456  	defer cancel()
   457  	reader := metric.NewManualReader()
   458  	provider := metric.NewMeterProvider(metric.WithReader(reader))
   459  	ss := &stubserver.StubServer{
   460  		UnaryCallF: func(_ context.Context, in *testpb.SimpleRequest) (*testpb.SimpleResponse, error) {
   461  			return &testpb.SimpleResponse{Payload: &testpb.Payload{
   462  				Body: make([]byte, len(in.GetPayload().GetBody())),
   463  			}}, nil
   464  		},
   465  	}
   466  
   467  	po := newPluginOption(ctx)
   468  	dopts := []grpc.DialOption{dialOptionSetCSM(opentelemetry.Options{
   469  		MetricsOptions: opentelemetry.MetricsOptions{
   470  			MeterProvider:  provider,
   471  			Metrics:        opentelemetry.DefaultMetrics(),
   472  			OptionalLabels: []string{"csm.service_name", "csm.service_namespace_name", "grpc.lb.locality"},
   473  		},
   474  	}, po), grpc.WithUnaryInterceptor(unaryInterceptorAttachXDSLabels)}
   475  	if err := ss.Start(nil, dopts...); err != nil {
   476  		t.Fatalf("Error starting endpoint server: %v", err)
   477  	}
   478  
   479  	defer ss.Stop()
   480  	ss.Client.UnaryCall(ctx, &testpb.SimpleRequest{Payload: &testpb.Payload{
   481  		Body: make([]byte, 10000),
   482  	}}, grpc.UseCompressor(gzip.Name))
   483  
   484  	rm := &metricdata.ResourceMetrics{}
   485  	reader.Collect(ctx, rm)
   486  
   487  	gotMetrics := map[string]metricdata.Metrics{}
   488  	for _, sm := range rm.ScopeMetrics {
   489  		for _, m := range sm.Metrics {
   490  			gotMetrics[m.Name] = m
   491  		}
   492  	}
   493  
   494  	unaryMethodAttr := attribute.String("grpc.method", "grpc.testing.TestService/UnaryCall")
   495  	targetAttr := attribute.String("grpc.target", ss.Target)
   496  	unaryStatusAttr := attribute.String("grpc.status", "OK")
   497  
   498  	serviceNameAttr := attribute.String("csm.service_name", "service_name_val")
   499  	serviceNamespaceAttr := attribute.String("csm.service_namespace_name", "service_namespace_val")
   500  	localityAttr := attribute.String("grpc.lb.locality", "grpc.lb.locality_val")
   501  	meshIDAttr := attribute.String("csm.mesh_id", "unknown")
   502  	workloadCanonicalServiceAttr := attribute.String("csm.workload_canonical_service", "unknown")
   503  	remoteWorkloadTypeAttr := attribute.String("csm.remote_workload_type", "unknown")
   504  	remoteWorkloadCanonicalServiceAttr := attribute.String("csm.remote_workload_canonical_service", "unknown")
   505  
   506  	unaryMethodClientSideEnd := []attribute.KeyValue{
   507  		unaryMethodAttr,
   508  		targetAttr,
   509  		unaryStatusAttr,
   510  		serviceNameAttr,
   511  		serviceNamespaceAttr,
   512  		localityAttr,
   513  		meshIDAttr,
   514  		workloadCanonicalServiceAttr,
   515  		remoteWorkloadTypeAttr,
   516  		remoteWorkloadCanonicalServiceAttr,
   517  	}
   518  
   519  	unaryCompressedBytesSentRecv := int64(57) // Fixed 10000 bytes with gzip assumption.
   520  	unaryBucketCounts := []uint64{0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}
   521  	unaryExtrema := metricdata.NewExtrema(int64(57))
   522  	wantMetrics := []metricdata.Metrics{
   523  		{
   524  			Name:        "grpc.client.attempt.started",
   525  			Description: "Number of client call attempts started.",
   526  			Unit:        "attempt",
   527  			Data: metricdata.Sum[int64]{
   528  				DataPoints: []metricdata.DataPoint[int64]{
   529  					{
   530  						Attributes: attribute.NewSet(unaryMethodAttr, targetAttr),
   531  						Value:      1,
   532  					},
   533  				},
   534  				Temporality: metricdata.CumulativeTemporality,
   535  				IsMonotonic: true,
   536  			},
   537  		}, // Doesn't have xDS Labels, CSM Labels start from header or trailer from server, whichever comes first, so this doesn't need it
   538  		{
   539  			Name:        "grpc.client.attempt.duration",
   540  			Description: "End-to-end time taken to complete a client call attempt.",
   541  			Unit:        "s",
   542  			Data: metricdata.Histogram[float64]{
   543  				DataPoints: []metricdata.HistogramDataPoint[float64]{
   544  					{
   545  						Attributes: attribute.NewSet(unaryMethodClientSideEnd...),
   546  						Count:      1,
   547  						Bounds:     itestutils.DefaultLatencyBounds,
   548  					},
   549  				},
   550  				Temporality: metricdata.CumulativeTemporality,
   551  			},
   552  		},
   553  		{
   554  			Name:        "grpc.client.attempt.sent_total_compressed_message_size",
   555  			Description: "Compressed message bytes sent per client call attempt.",
   556  			Unit:        "By",
   557  			Data: metricdata.Histogram[int64]{
   558  				DataPoints: []metricdata.HistogramDataPoint[int64]{
   559  					{
   560  						Attributes:   attribute.NewSet(unaryMethodClientSideEnd...),
   561  						Count:        1,
   562  						Bounds:       itestutils.DefaultSizeBounds,
   563  						BucketCounts: unaryBucketCounts,
   564  						Min:          unaryExtrema,
   565  						Max:          unaryExtrema,
   566  						Sum:          unaryCompressedBytesSentRecv,
   567  					},
   568  				},
   569  				Temporality: metricdata.CumulativeTemporality,
   570  			},
   571  		},
   572  		{
   573  			Name:        "grpc.client.attempt.rcvd_total_compressed_message_size",
   574  			Description: "Compressed message bytes received per call attempt.",
   575  			Unit:        "By",
   576  			Data: metricdata.Histogram[int64]{
   577  				DataPoints: []metricdata.HistogramDataPoint[int64]{
   578  					{
   579  						Attributes:   attribute.NewSet(unaryMethodClientSideEnd...),
   580  						Count:        1,
   581  						Bounds:       itestutils.DefaultSizeBounds,
   582  						BucketCounts: unaryBucketCounts,
   583  						Min:          unaryExtrema,
   584  						Max:          unaryExtrema,
   585  						Sum:          unaryCompressedBytesSentRecv,
   586  					},
   587  				},
   588  				Temporality: metricdata.CumulativeTemporality,
   589  			},
   590  		},
   591  		{
   592  			Name:        "grpc.client.call.duration",
   593  			Description: "Time taken by gRPC to complete an RPC from application's perspective.",
   594  			Unit:        "s",
   595  			Data: metricdata.Histogram[float64]{
   596  				DataPoints: []metricdata.HistogramDataPoint[float64]{
   597  					{
   598  						Attributes: attribute.NewSet(unaryMethodAttr, targetAttr, unaryStatusAttr),
   599  						Count:      1,
   600  						Bounds:     itestutils.DefaultLatencyBounds,
   601  					},
   602  				},
   603  				Temporality: metricdata.CumulativeTemporality,
   604  			},
   605  		},
   606  	}
   607  
   608  	gotMetrics = itestutils.WaitForServerMetrics(ctx, t, reader, gotMetrics, wantMetrics)
   609  	itestutils.CompareMetrics(t, gotMetrics, wantMetrics)
   610  }
   611  
   612  // TestObservability tests that Observability global function compiles and runs
   613  // without error. The actual functionality of this function will be verified in
   614  // interop tests.
   615  func (s) TestObservability(*testing.T) {
   616  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   617  	defer cancel()
   618  
   619  	cleanup := EnableObservability(ctx, opentelemetry.Options{})
   620  	cleanup()
   621  }