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 }