diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 648712f4698c..4c5aba8d6db8 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -105,8 +105,15 @@ jobs: cd "${GITHUB_WORKSPACE}" for MOD_FILE in $(find . -name 'go.mod' | grep -Ev '^\./go\.mod'); do pushd "$(dirname ${MOD_FILE})" - go test ${{ matrix.testflags }} -cpu 1,4 -timeout 2m ./... + # Skip OpenTelemetry module if 3 releases ago, Go OpenTelemetry only supports + # previous two releases. + if [[ "${{ matrix.goversion }}" == "1.20" && "${PWD}" =~ /stats/opentelemetry$ ]]; then + echo "Skipping ${MOD_FILE}" + else + go test ${{ matrix.testflags }} -cpu 1,4 -timeout 2m ./... + fi popd + done # Non-core gRPC tests (examples, interop, etc) diff --git a/stats/opentelemetry/csm/pluginoption.go b/stats/opentelemetry/csm/pluginoption.go new file mode 100644 index 000000000000..8214a82bc21d --- /dev/null +++ b/stats/opentelemetry/csm/pluginoption.go @@ -0,0 +1,309 @@ +/* + * + * 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 csm contains utilities for Google Cloud Service Mesh observability. +package csm + +import ( + "context" + "encoding/base64" + "net/url" + "os" + "strings" + + "google.golang.org/grpc/grpclog" + "google.golang.org/grpc/internal/xds/bootstrap" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/stats/opentelemetry/internal" + + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/structpb" + + "go.opentelemetry.io/contrib/detectors/gcp" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/sdk/resource" +) + +var logger = grpclog.Component("csm-observability-plugin") + +// pluginOption emits CSM Labels from the environment and metadata exchange +// for csm channels and all servers. +// +// Do not use this directly; use newPluginOption instead. +type pluginOption struct { + // localLabels are the labels that identify the local environment a binary + // is run in, and will be emitted from the CSM Plugin Option. + localLabels map[string]string + // metadataExchangeLabelsEncoded are the metadata exchange labels to be sent + // as the value of metadata key "x-envoy-peer-metadata" in proto wire format + // and base 64 encoded. This gets sent out from all the servers running in + // this process and for csm channels. + metadataExchangeLabelsEncoded string +} + +// newPluginOption returns a new pluginOption with local labels and metadata +// exchange labels derived from the environment. +func newPluginOption(ctx context.Context) internal.PluginOption { + localLabels, metadataExchangeLabelsEncoded := constructMetadataFromEnv(ctx) + + return &pluginOption{ + localLabels: localLabels, + metadataExchangeLabelsEncoded: metadataExchangeLabelsEncoded, + } +} + +// NewLabelsMD returns a metadata.MD with the CSM labels as an encoded protobuf +// Struct as the value of "x-envoy-peer-metadata". +func (cpo *pluginOption) GetMetadata() metadata.MD { + return metadata.New(map[string]string{ + metadataExchangeKey: cpo.metadataExchangeLabelsEncoded, + }) +} + +// GetLabels gets the CSM peer labels from the metadata provided. It returns +// "unknown" for labels not found. Labels returned depend on the remote type. +// Additionally, local labels determined at initialization time are appended to +// labels returned, in addition to the optionalLabels provided. +func (cpo *pluginOption) GetLabels(md metadata.MD) map[string]string { + labels := map[string]string{ // Remote labels if type is unknown (i.e. unset or error processing x-envoy-peer-metadata) + "csm.remote_workload_type": "unknown", + "csm.remote_workload_canonical_service": "unknown", + } + // Append the local labels. + for k, v := range cpo.localLabels { + labels[k] = v + } + + val := md.Get("x-envoy-peer-metadata") + // This can't happen if corresponding csm client because of proto wire + // format encoding, but since it is arbitrary off the wire be safe. + if len(val) != 1 { + logger.Warningf("length of md values of \"x-envoy-peer-metadata\" is not 1, is %v", len(val)) + return labels + } + + protoWireFormat, err := base64.RawStdEncoding.DecodeString(val[0]) + if err != nil { + logger.Warningf("error base 64 decoding value of \"x-envoy-peer-metadata\": %v", err) + return labels + } + + spb := &structpb.Struct{} + if err := proto.Unmarshal(protoWireFormat, spb); err != nil { + logger.Warningf("error unmarshalling value of \"x-envoy-peer-metadata\" into proto: %v", err) + return labels + } + + fields := spb.GetFields() + + labels["csm.remote_workload_type"] = getFromMetadata("type", fields) + // The value of “csm.remote_workload_canonical_service” comes from + // MetadataExchange with the key “canonical_service”. (Note that this should + // be read even if the remote type is unknown.) + labels["csm.remote_workload_canonical_service"] = getFromMetadata("canonical_service", fields) + + // Unset/unknown types, and types that aren't GKE or GCP return early with + // just local labels, remote_workload_type and + // remote_workload_canonical_service labels. + workloadType := labels["csm.remote_workload_type"] + if workloadType != "gcp_kubernetes_engine" && workloadType != "gcp_compute_engine" { + return labels + } + // GKE and GCE labels. + labels["csm.remote_workload_project_id"] = getFromMetadata("project_id", fields) + labels["csm.remote_workload_location"] = getFromMetadata("location", fields) + labels["csm.remote_workload_name"] = getFromMetadata("workload_name", fields) + if workloadType == "gcp_compute_engine" { + return labels + } + + // GKE only labels. + labels["csm.remote_workload_cluster_name"] = getFromMetadata("cluster_name", fields) + labels["csm.remote_workload_namespace_name"] = getFromMetadata("namespace_name", fields) + return labels +} + +// getFromMetadata gets the value for the metadata key from the protobuf +// metadata. Returns "unknown" if the metadata is not found in the protobuf +// metadata, or if the value is not a string value. Returns the string value +// otherwise. +func getFromMetadata(metadataKey string, metadata map[string]*structpb.Value) string { + if metadata != nil { + if metadataVal, ok := metadata[metadataKey]; ok { + if _, ok := metadataVal.GetKind().(*structpb.Value_StringValue); ok { + return metadataVal.GetStringValue() + } + } + } + return "unknown" +} + +// getFromResource gets the value for the resource key from the attribute set. +// Returns "unknown" if the resourceKey is not found in the attribute set or is +// not a string value, the string value otherwise. +func getFromResource(resourceKey attribute.Key, set *attribute.Set) string { + if set != nil { + if resourceVal, ok := set.Value(resourceKey); ok && resourceVal.Type() == attribute.STRING { + return resourceVal.AsString() + } + } + return "unknown" +} + +// getEnv returns "unknown" if environment variable is unset, the environment +// variable otherwise. +func getEnv(name string) string { + if val, ok := os.LookupEnv(name); ok { + return val + } + return "unknown" +} + +var ( + // This function will be overridden in unit tests. + getAttrSetFromResourceDetector = func(ctx context.Context) *attribute.Set { + r, err := resource.New(ctx, resource.WithDetectors(gcp.NewDetector())) + if err != nil { + logger.Errorf("error reading OpenTelemetry resource: %v", err) + return nil + } + return r.Set() + } +) + +// constructMetadataFromEnv creates local labels and labels to send to the peer +// using metadata exchange based off resource detection and environment +// variables. +// +// Returns local labels, and base 64 encoded protobuf.Struct containing metadata +// exchange labels. +func constructMetadataFromEnv(ctx context.Context) (map[string]string, string) { + set := getAttrSetFromResourceDetector(ctx) + + labels := make(map[string]string) + labels["type"] = getFromResource("cloud.platform", set) + labels["canonical_service"] = getEnv("CSM_CANONICAL_SERVICE_NAME") + + // If type is not GCE or GKE only metadata exchange labels are "type" and + // "canonical_service". + cloudPlatformVal := labels["type"] + if cloudPlatformVal != "gcp_kubernetes_engine" && cloudPlatformVal != "gcp_compute_engine" { + return initializeLocalAndMetadataLabels(labels) + } + + // GCE and GKE labels: + labels["workload_name"] = getEnv("CSM_WORKLOAD_NAME") + + locationVal := "unknown" + if resourceVal, ok := set.Value("cloud.availability_zone"); ok && resourceVal.Type() == attribute.STRING { + locationVal = resourceVal.AsString() + } else if resourceVal, ok = set.Value("cloud.region"); ok && resourceVal.Type() == attribute.STRING { + locationVal = resourceVal.AsString() + } + labels["location"] = locationVal + + labels["project_id"] = getFromResource("cloud.account.id", set) + if cloudPlatformVal == "gcp_compute_engine" { + return initializeLocalAndMetadataLabels(labels) + } + + // GKE specific labels: + labels["namespace_name"] = getFromResource("k8s.namespace.name", set) + labels["cluster_name"] = getFromResource("k8s.cluster.name", set) + return initializeLocalAndMetadataLabels(labels) +} + +// parseMeshIDString parses the mesh id from the node id according to the format +// "projects/[GCP Project number]/networks/mesh:[Mesh ID]/nodes/[UUID]". Returns +// "unknown" if there is a syntax error in the node ID. +func parseMeshIDFromNodeID(nodeID string) string { + meshSplit := strings.Split(nodeID, "/") + if len(meshSplit) != 6 { + return "unknown" + } + if meshSplit[0] != "projects" || meshSplit[2] != "networks" || meshSplit[4] != "nodes" { + return "unknown" + } + meshID, ok := strings.CutPrefix(meshSplit[3], "mesh:") + if !ok { // errors become "unknown" + return "unknown" + } + return meshID +} + +// initializeLocalAndMetadataLabels csm local labels for a CSM Plugin Option to +// record. It also builds out a base 64 encoded protobuf.Struct containing the +// metadata exchange labels to be sent as part of metadata exchange from a CSM +// Plugin Option. +func initializeLocalAndMetadataLabels(labels map[string]string) (map[string]string, string) { + // The value of “csm.workload_canonical_service” comes from + // “CSM_CANONICAL_SERVICE_NAME” env var, “unknown” if unset. + val := labels["canonical_service"] + localLabels := make(map[string]string) + localLabels["csm.workload_canonical_service"] = val + // Get the CSM Mesh ID from the bootstrap file. + nodeID := getNodeID() + localLabels["csm.mesh_id"] = parseMeshIDFromNodeID(nodeID) + + // Metadata exchange labels - can go ahead and encode into proto, and then + // base64. + pbLabels := &structpb.Struct{ + Fields: map[string]*structpb.Value{}, + } + for k, v := range labels { + pbLabels.Fields[k] = structpb.NewStringValue(v) + } + protoWireFormat, err := proto.Marshal(pbLabels) + metadataExchangeLabelsEncoded := "" + if err == nil { + metadataExchangeLabelsEncoded = base64.RawStdEncoding.EncodeToString(protoWireFormat) + } + // else - This behavior triggers server side to reply (if sent from a gRPC + // Client within this binary) with the metadata exchange labels. Even if + // client side has a problem marshaling proto into wire format, it can + // still use server labels so send an empty string as the value of + // x-envoy-peer-metadata. The presence of this metadata exchange header + // will cause server side to respond with metadata exchange labels. + + return localLabels, metadataExchangeLabelsEncoded +} + +// getNodeID gets the Node ID from the bootstrap data. +func getNodeID() string { + cfg, err := bootstrap.NewConfig() + if err != nil { + return "" // will become "unknown" + } + if cfg.NodeProto == nil { + return "" + } + return cfg.NodeProto.GetId() +} + +// metadataExchangeKey is the key for HTTP metadata exchange. +const metadataExchangeKey = "x-envoy-peer-metadata" + +func determineTargetCSM(parsedTarget *url.URL) bool { + // On the client-side, the channel target is used to determine if a channel is a + // CSM channel or not. CSM channels need to have an “xds” scheme and a + // "traffic-director-global.xds.googleapis.com" authority. In the cases where no + // authority is mentioned, the authority is assumed to be CSM. MetadataExchange + // is performed only for CSM channels. Non-metadata exchange labels are detected + // as described below. + return parsedTarget.Scheme == "xds" && (parsedTarget.Host == "" || parsedTarget.Host == "traffic-director-global.xds.googleapis.com") +} diff --git a/stats/opentelemetry/csm/pluginoption_test.go b/stats/opentelemetry/csm/pluginoption_test.go new file mode 100644 index 000000000000..7bddcfad37f8 --- /dev/null +++ b/stats/opentelemetry/csm/pluginoption_test.go @@ -0,0 +1,548 @@ +/* + * + * 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 csm + +import ( + "context" + "encoding/base64" + "fmt" + "net/url" + "os" + "testing" + "time" + + "google.golang.org/grpc/internal/envconfig" + "google.golang.org/grpc/internal/grpctest" + "google.golang.org/grpc/internal/testutils/xds/bootstrap" + "google.golang.org/grpc/metadata" + + "github.com/google/go-cmp/cmp" + "go.opentelemetry.io/otel/attribute" + + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/structpb" +) + +type s struct { + grpctest.Tester +} + +func Test(t *testing.T) { + grpctest.RunSubTests(t, s{}) +} + +var defaultTestTimeout = 5 * time.Second + +// clearEnv unsets all the environment variables relevant to the csm +// pluginOption. +func clearEnv() { + os.Unsetenv(envconfig.XDSBootstrapFileContentEnv) + os.Unsetenv(envconfig.XDSBootstrapFileNameEnv) + + os.Unsetenv("CSM_CANONICAL_SERVICE_NAME") + os.Unsetenv("CSM_WORKLOAD_NAME") +} + +func (s) TestGetLabels(t *testing.T) { + clearEnv() + ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) + defer cancel() + cpo := newPluginOption(ctx) + + tests := []struct { + name string + unsetHeader bool // Should trigger "unknown" labels + twoValues bool // Should trigger "unknown" labels + metadataExchangeLabels map[string]string + labelsWant map[string]string + }{ + { + name: "unset-labels", + metadataExchangeLabels: nil, + labelsWant: map[string]string{ + "csm.workload_canonical_service": "unknown", + "csm.mesh_id": "unknown", + + "csm.remote_workload_type": "unknown", + "csm.remote_workload_canonical_service": "unknown", + }, + }, + { + name: "metadata-partially-set", + metadataExchangeLabels: map[string]string{ + "type": "not-gce-or-gke", + "ignore-this": "ignore-this", + }, + labelsWant: map[string]string{ + "csm.workload_canonical_service": "unknown", + "csm.mesh_id": "unknown", + + "csm.remote_workload_type": "not-gce-or-gke", + "csm.remote_workload_canonical_service": "unknown", + }, + }, + { + name: "google-compute-engine", + metadataExchangeLabels: map[string]string{ // All of these labels get emitted when type is "gcp_compute_engine". + "type": "gcp_compute_engine", + "canonical_service": "canonical_service_val", + "project_id": "unique-id", + "location": "us-east", + "workload_name": "workload_name_val", + }, + labelsWant: map[string]string{ + "csm.workload_canonical_service": "unknown", + "csm.mesh_id": "unknown", + + "csm.remote_workload_type": "gcp_compute_engine", + "csm.remote_workload_canonical_service": "canonical_service_val", + "csm.remote_workload_project_id": "unique-id", + "csm.remote_workload_location": "us-east", + "csm.remote_workload_name": "workload_name_val", + }, + }, + // unset should go to unknown, ignore GKE labels that are not relevant + // to GCE. + { + name: "google-compute-engine-labels-partially-set-with-extra", + metadataExchangeLabels: map[string]string{ + "type": "gcp_compute_engine", + "canonical_service": "canonical_service_val", + "project_id": "unique-id", + "location": "us-east", + // "workload_name": "", unset workload name - should become "unknown" + "namespace_name": "should-be-ignored", + "cluster_name": "should-be-ignored", + }, + labelsWant: map[string]string{ + "csm.workload_canonical_service": "unknown", + "csm.mesh_id": "unknown", + + "csm.remote_workload_type": "gcp_compute_engine", + "csm.remote_workload_canonical_service": "canonical_service_val", + "csm.remote_workload_project_id": "unique-id", + "csm.remote_workload_location": "us-east", + "csm.remote_workload_name": "unknown", + }, + }, + { + name: "google-kubernetes-engine", + metadataExchangeLabels: map[string]string{ + "type": "gcp_kubernetes_engine", + "canonical_service": "canonical_service_val", + "project_id": "unique-id", + "namespace_name": "namespace_name_val", + "cluster_name": "cluster_name_val", + "location": "us-east", + "workload_name": "workload_name_val", + }, + labelsWant: map[string]string{ + "csm.workload_canonical_service": "unknown", + "csm.mesh_id": "unknown", + + "csm.remote_workload_type": "gcp_kubernetes_engine", + "csm.remote_workload_canonical_service": "canonical_service_val", + "csm.remote_workload_project_id": "unique-id", + "csm.remote_workload_cluster_name": "cluster_name_val", + "csm.remote_workload_namespace_name": "namespace_name_val", + "csm.remote_workload_location": "us-east", + "csm.remote_workload_name": "workload_name_val", + }, + }, + { + name: "google-kubernetes-engine-labels-partially-set", + metadataExchangeLabels: map[string]string{ + "type": "gcp_kubernetes_engine", + "canonical_service": "canonical_service_val", + "project_id": "unique-id", + "namespace_name": "namespace_name_val", + // "cluster_name": "", cluster_name unset, should become "unknown" + "location": "us-east", + // "workload_name": "", workload_name unset, should become "unknown" + }, + labelsWant: map[string]string{ + "csm.workload_canonical_service": "unknown", + "csm.mesh_id": "unknown", + + "csm.remote_workload_type": "gcp_kubernetes_engine", + "csm.remote_workload_canonical_service": "canonical_service_val", + "csm.remote_workload_project_id": "unique-id", + "csm.remote_workload_cluster_name": "unknown", + "csm.remote_workload_namespace_name": "namespace_name_val", + "csm.remote_workload_location": "us-east", + "csm.remote_workload_name": "unknown", + }, + }, + { + name: "unset-header", + metadataExchangeLabels: map[string]string{ + "type": "gcp_kubernetes_engine", + "canonical_service": "canonical_service_val", + "project_id": "unique-id", + "namespace_name": "namespace_name_val", + "cluster_name": "cluster_name_val", + "location": "us-east", + "workload_name": "workload_name_val", + }, + unsetHeader: true, + labelsWant: map[string]string{ + "csm.workload_canonical_service": "unknown", + "csm.mesh_id": "unknown", + + "csm.remote_workload_type": "unknown", + "csm.remote_workload_canonical_service": "unknown", + }, + }, + { + name: "two-header-values", + metadataExchangeLabels: map[string]string{ + "type": "gcp_kubernetes_engine", + "canonical_service": "canonical_service_val", + "project_id": "unique-id", + "namespace_name": "namespace_name_val", + "cluster_name": "cluster_name_val", + "location": "us-east", + "workload_name": "workload_name_val", + }, + twoValues: true, + labelsWant: map[string]string{ + "csm.workload_canonical_service": "unknown", + "csm.mesh_id": "unknown", + + "csm.remote_workload_type": "unknown", + "csm.remote_workload_canonical_service": "unknown", + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + pbLabels := &structpb.Struct{ + Fields: map[string]*structpb.Value{}, + } + for k, v := range test.metadataExchangeLabels { + pbLabels.Fields[k] = structpb.NewStringValue(v) + } + protoWireFormat, err := proto.Marshal(pbLabels) + if err != nil { + t.Fatalf("Error marshaling proto: %v", err) + } + metadataExchangeLabelsEncoded := base64.RawStdEncoding.EncodeToString(protoWireFormat) + md := metadata.New(map[string]string{ + metadataExchangeKey: metadataExchangeLabelsEncoded, + }) + if test.unsetHeader { + md.Delete(metadataExchangeKey) + } + if test.twoValues { + md.Append(metadataExchangeKey, "extra-val") + } + + labelsGot := cpo.GetLabels(md) + if diff := cmp.Diff(labelsGot, test.labelsWant); diff != "" { + t.Fatalf("cpo.GetLabels returned unexpected value (-got, +want): %v", diff) + } + }) + } +} + +// TestDetermineTargetCSM tests the helper function that determines whether a +// target is relevant to CSM or not, based off the rules outlined in design. +func (s) TestDetermineTargetCSM(t *testing.T) { + tests := []struct { + name string + target string + targetCSM bool + }{ + { + name: "dns:///localhost", + target: "normal-target-here", + targetCSM: false, + }, + { + name: "xds-no-authority", + target: "xds:///localhost", + targetCSM: true, + }, + { + name: "xds-traffic-director-authority", + target: "xds://traffic-director-global.xds.googleapis.com/localhost", + targetCSM: true, + }, + { + name: "xds-not-traffic-director-authority", + target: "xds://not-traffic-director-authority/localhost", + targetCSM: false, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + parsedTarget, err := url.Parse(test.target) + if err != nil { + t.Fatalf("test target %v failed to parse: %v", test.target, err) + } + if got := determineTargetCSM(parsedTarget); got != test.targetCSM { + t.Fatalf("cpo.determineTargetCSM(%v): got %v, want %v", test.target, got, test.targetCSM) + } + }) + } +} + +func (s) TestBootstrap(t *testing.T) { + tests := []struct { + name string + nodeID string + meshIDWant string + }{ + { + name: "malformed-node-id-unknown", + nodeID: "malformed", + meshIDWant: "unknown", + }, + { + name: "node-id-parsed", + nodeID: "projects/12345/networks/mesh:mesh_id/nodes/aaaa-aaaa-aaaa-aaaa", + meshIDWant: "mesh_id", + }, + { + name: "wrong-syntax-unknown", + nodeID: "wrong-syntax/12345/networks/mesh:mesh_id/nodes/aaaa-aaaa-aaaa-aaaa", + meshIDWant: "unknown", + }, + { + name: "node-id-parsed", + nodeID: "projects/12345/networks/mesh:/nodes/aaaa-aaaa-aaaa-aaaa", + meshIDWant: "", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + cleanup, err := bootstrap.CreateFile(bootstrap.Options{ + NodeID: test.nodeID, + ServerURI: "xds_server_uri", + }) + if err != nil { + t.Fatalf("failed to create bootstrap: %v", err) + } + defer cleanup() + nodeIDGot := getNodeID() // this should return the node ID plumbed into bootstrap above + if nodeIDGot != test.nodeID { + t.Fatalf("getNodeID: got %v, want %v", nodeIDGot, test.nodeID) + } + + meshIDGot := parseMeshIDFromNodeID(nodeIDGot) + if meshIDGot != test.meshIDWant { + t.Fatalf("parseMeshIDFromNodeID(%v): got %v, want %v", nodeIDGot, meshIDGot, test.meshIDWant) + } + }) + } +} + +// TestSetLabels tests the setting of labels, which snapshots the resource and +// environment. It mocks the resource and environment, and then calls into +// labels creation. It verifies to local labels created and metadata exchange +// labels emitted from the setLabels function. +func (s) TestSetLabels(t *testing.T) { + clearEnv() + tests := []struct { + name string + resourceKeyValues map[string]string + csmCanonicalServiceNamePopulated bool + csmWorkloadNamePopulated bool + bootstrapGeneratorPopulated bool + localLabelsWant map[string]string + metadataExchangeLabelsWant map[string]string + }{ + { + name: "no-type", + csmCanonicalServiceNamePopulated: true, + bootstrapGeneratorPopulated: true, + resourceKeyValues: map[string]string{}, + localLabelsWant: map[string]string{ + "csm.workload_canonical_service": "canonical_service_name_val", // env var populated so should be set. + "csm.mesh_id": "mesh_id", // env var populated so should be set. + }, + metadataExchangeLabelsWant: map[string]string{ + "type": "unknown", + "canonical_service": "canonical_service_name_val", // env var populated so should be set. + }, + }, + { + name: "gce", + csmWorkloadNamePopulated: true, + resourceKeyValues: map[string]string{ + "cloud.platform": "gcp_compute_engine", + // csm workload name is an env var + "cloud.availability_zone": "cloud_availability_zone_val", + "cloud.region": "should-be-ignored", // cloud.availability_zone takes precedence + "cloud.account.id": "cloud_account_id_val", + }, + localLabelsWant: map[string]string{ + "csm.workload_canonical_service": "unknown", + "csm.mesh_id": "unknown", + }, + metadataExchangeLabelsWant: map[string]string{ + "type": "gcp_compute_engine", + "canonical_service": "unknown", + "workload_name": "workload_name_val", + "location": "cloud_availability_zone_val", + "project_id": "cloud_account_id_val", + }, + }, + { + name: "gce-half-unset", + resourceKeyValues: map[string]string{ + "cloud.platform": "gcp_compute_engine", + // csm workload name is an env var + "cloud.availability_zone": "cloud_availability_zone_val", + "cloud.region": "should-be-ignored", // cloud.availability_zone takes precedence + }, + localLabelsWant: map[string]string{ + "csm.workload_canonical_service": "unknown", + "csm.mesh_id": "unknown", + }, + metadataExchangeLabelsWant: map[string]string{ + "type": "gcp_compute_engine", + "canonical_service": "unknown", + "workload_name": "unknown", + "location": "cloud_availability_zone_val", + "project_id": "unknown", + }, + }, + { + name: "gke", + resourceKeyValues: map[string]string{ + "cloud.platform": "gcp_kubernetes_engine", + // csm workload name is an env var + "cloud.region": "cloud_region_val", // availability_zone isn't present, so this should become location + "cloud.account.id": "cloud_account_id_val", + "k8s.namespace.name": "k8s_namespace_name_val", + "k8s.cluster.name": "k8s_cluster_name_val", + }, + localLabelsWant: map[string]string{ + "csm.workload_canonical_service": "unknown", + "csm.mesh_id": "unknown", + }, + metadataExchangeLabelsWant: map[string]string{ + "type": "gcp_kubernetes_engine", + "canonical_service": "unknown", + "workload_name": "unknown", + "location": "cloud_region_val", + "project_id": "cloud_account_id_val", + "namespace_name": "k8s_namespace_name_val", + "cluster_name": "k8s_cluster_name_val", + }, + }, + { + name: "gke-half-unset", + resourceKeyValues: map[string]string{ // unset should become unknown + "cloud.platform": "gcp_kubernetes_engine", + // csm workload name is an env var + "cloud.region": "cloud_region_val", // availability_zone isn't present, so this should become location + // "cloud.account.id": "", // unset - should become unknown + "k8s.namespace.name": "k8s_namespace_name_val", + // "k8s.cluster.name": "", // unset - should become unknown + }, + localLabelsWant: map[string]string{ + "csm.workload_canonical_service": "unknown", + "csm.mesh_id": "unknown", + }, + metadataExchangeLabelsWant: map[string]string{ + "type": "gcp_kubernetes_engine", + "canonical_service": "unknown", + "workload_name": "unknown", + "location": "cloud_region_val", + "project_id": "unknown", + "namespace_name": "k8s_namespace_name_val", + "cluster_name": "unknown", + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + func() { + if test.csmCanonicalServiceNamePopulated { + os.Setenv("CSM_CANONICAL_SERVICE_NAME", "canonical_service_name_val") + defer os.Unsetenv("CSM_CANONICAL_SERVICE_NAME") + } + if test.csmWorkloadNamePopulated { + os.Setenv("CSM_WORKLOAD_NAME", "workload_name_val") + defer os.Unsetenv("CSM_WORKLOAD_NAME") + } + if test.bootstrapGeneratorPopulated { + cleanup, err := bootstrap.CreateFile(bootstrap.Options{ + NodeID: "projects/12345/networks/mesh:mesh_id/nodes/aaaa-aaaa-aaaa-aaaa", + ServerURI: "xds_server_uri", + }) + if err != nil { + t.Fatalf("failed to create bootstrap: %v", err) + } + defer cleanup() + } + var attributes []attribute.KeyValue + for k, v := range test.resourceKeyValues { + attributes = append(attributes, attribute.String(k, v)) + } + // Return the attributes configured as part of the test in place + // of reading from resource. + attrSet := attribute.NewSet(attributes...) + origGetAttrSet := getAttrSetFromResourceDetector + getAttrSetFromResourceDetector = func(context.Context) *attribute.Set { + return &attrSet + } + defer func() { getAttrSetFromResourceDetector = origGetAttrSet }() + + ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) + defer cancel() + localLabelsGot, mdEncoded := constructMetadataFromEnv(ctx) + if diff := cmp.Diff(localLabelsGot, test.localLabelsWant); diff != "" { + t.Fatalf("constructMetadataFromEnv() want: %v, got %v", test.localLabelsWant, localLabelsGot) + } + + verifyMetadataExchangeLabels(mdEncoded, test.metadataExchangeLabelsWant) + }() + }) + } +} + +func verifyMetadataExchangeLabels(mdEncoded string, mdLabelsWant map[string]string) error { + protoWireFormat, err := base64.RawStdEncoding.DecodeString(mdEncoded) + if err != nil { + return fmt.Errorf("error base 64 decoding metadata val: %v", err) + } + spb := &structpb.Struct{} + if err := proto.Unmarshal(protoWireFormat, spb); err != nil { + return fmt.Errorf("error unmarshaling proto wire format: %v", err) + } + fields := spb.GetFields() + for k, v := range mdLabelsWant { + if val, ok := fields[k]; !ok { + if _, ok := val.GetKind().(*structpb.Value_StringValue); !ok { + return fmt.Errorf("struct value for key %v should be string type", k) + } + if val.GetStringValue() != v { + return fmt.Errorf("struct value for key %v got: %v, want %v", k, val.GetStringValue(), v) + } + } + } + if len(mdLabelsWant) != len(fields) { + return fmt.Errorf("len(mdLabelsWant) = %v, len(mdLabelsGot) = %v", len(mdLabelsWant), len(fields)) + } + return nil +} diff --git a/stats/opentelemetry/go.mod b/stats/opentelemetry/go.mod index 1573ad9b4a88..a43d693fd5b9 100644 --- a/stats/opentelemetry/go.mod +++ b/stats/opentelemetry/go.mod @@ -1,24 +1,35 @@ module google.golang.org/grpc/stats/opentelemetry -go 1.20 +go 1.21 replace google.golang.org/grpc => ../.. require ( - go.opentelemetry.io/otel v1.24.0 - go.opentelemetry.io/otel/metric v1.24.0 + github.com/google/go-cmp v0.6.0 + go.opentelemetry.io/contrib/detectors/gcp v1.26.0 + go.opentelemetry.io/otel v1.26.0 + go.opentelemetry.io/otel/metric v1.26.0 + go.opentelemetry.io/otel/sdk v1.26.0 go.opentelemetry.io/otel/sdk/metric v1.24.0 google.golang.org/grpc v1.62.1 + google.golang.org/protobuf v1.33.0 ) require ( + cloud.google.com/go/compute/metadata v0.3.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.22.0 // indirect + github.com/cncf/xds/go v0.0.0-20240318125728-8a4994d93e50 // indirect + github.com/envoyproxy/go-control-plane v0.12.0 // indirect + github.com/envoyproxy/protoc-gen-validate v1.0.4 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect - go.opentelemetry.io/otel/sdk v1.24.0 // indirect - go.opentelemetry.io/otel/trace v1.24.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect + go.opentelemetry.io/otel/trace v1.26.0 // indirect golang.org/x/net v0.22.0 // indirect - golang.org/x/sys v0.18.0 // indirect + golang.org/x/oauth2 v0.18.0 // indirect + golang.org/x/sync v0.6.0 // indirect + golang.org/x/sys v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect + google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect - google.golang.org/protobuf v1.33.0 // indirect ) diff --git a/stats/opentelemetry/go.sum b/stats/opentelemetry/go.sum index a2ec727fbf3e..2424e7378b01 100644 --- a/stats/opentelemetry/go.sum +++ b/stats/opentelemetry/go.sum @@ -1,30 +1,92 @@ +cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= +cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.22.0 h1:PWcDbDjrcT/ZHLn4Bc/FuglaZZVPP8bWO/YRmJBbe38= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.22.0/go.mod h1:XEK/YHYsi+Wk2Bk1+zi/he+gjRfDWtoIZEZwuwcYjhk= +github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= +github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= +github.com/cncf/xds/go v0.0.0-20240318125728-8a4994d93e50 h1:DBmgJDC9dTfkVyGgipamEh2BpGYxScCH1TOF1LL1cXc= +github.com/cncf/xds/go v0.0.0-20240318125728-8a4994d93e50/go.mod h1:5e1+Vvlzido69INQaVO6d87Qn543Xr6nooe9Kz7oBFM= 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.12.0 h1:4X+VP1GHd1Mhj6IB5mMeGbLCleqxjletLK6K0rbxyZI= +github.com/envoyproxy/go-control-plane v0.12.0/go.mod h1:ZBTaoJ23lqITozF0M6G4/IragXCQKCnYbmlmtHvwRG0= +github.com/envoyproxy/protoc-gen-validate v1.0.4 h1:gVPz/FMfvh57HdSJQyvBtF00j8JU4zdyUgIUNhlgg0A= +github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= -go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= -go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= -go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= -go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= -go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.opentelemetry.io/contrib/detectors/gcp v1.26.0 h1:NeL09fPJQ4C350Q4TAoBCfLMeUmwbsRdrWAIQhPLKtA= +go.opentelemetry.io/contrib/detectors/gcp v1.26.0/go.mod h1:F5oCMdHOftNwFDC0lUS+c5q6tvyb8C/hwdL9yP+AkXk= +go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs= +go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4= +go.opentelemetry.io/otel/metric v1.26.0 h1:7S39CLuY5Jgg9CrnA9HHiEjGMF/X2VHvoXGgSllRz30= +go.opentelemetry.io/otel/metric v1.26.0/go.mod h1:SY+rHOI4cEawI9a7N1A4nIg/nTQXe1ccCNWYOJUrpX4= +go.opentelemetry.io/otel/sdk v1.26.0 h1:Y7bumHf5tAiDlRYFmGqetNcLaVUZmh4iYfmGxtmz7F8= +go.opentelemetry.io/otel/sdk v1.26.0/go.mod h1:0p8MXpqLeJ0pzcszQQN4F0S5FVjBLgypeGSngLsmirs= go.opentelemetry.io/otel/sdk/metric v1.24.0 h1:yyMQrPzF+k88/DbH7o4FMAs80puqd+9osbiBrJrz/w8= go.opentelemetry.io/otel/sdk/metric v1.24.0/go.mod h1:I6Y5FjH6rvEnTTAYQz3Mmv2kl6Ek5IIrmwTLqMrrOE0= -go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= -go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= +go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA= +go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI= +golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= +google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98 h1:Z0hjGZePRE0ZBWotvtrwxFNrNE9CUAGtplaDK5NNI/g= +google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 h1:RFiFrvy37/mpSpdySBDrUdipW/dHwsRwh3J3+A9VgT4= +google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237/go.mod h1:Z5Iiy3jtmioajWHDGFk7CeugTyHtPvMHA4UTmUkyalE= google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc= google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/stats/opentelemetry/internal/pluginoption.go b/stats/opentelemetry/internal/pluginoption.go new file mode 100644 index 000000000000..efbcb0679395 --- /dev/null +++ b/stats/opentelemetry/internal/pluginoption.go @@ -0,0 +1,38 @@ +/* + * 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 internal defines the PluginOption interface. +package internal + +import ( + "google.golang.org/grpc/metadata" +) + +// PluginOption is the interface which represents a plugin option for the +// OpenTelemetry instrumentation component. This plugin option emits labels from +// metadata and also creates metadata containing labels. These labels are +// intended to be added to applicable OpenTelemetry metrics recorded in the +// OpenTelemetry instrumentation component. +// +// In the future, we hope to stabilize and expose this API to allow plugins to +// inject labels of their choosing into metrics recorded. +type PluginOption interface { + // GetMetadata creates a MD with metadata exchange labels. + GetMetadata() metadata.MD + // GetLabels emits labels to be attached to metrics for the RPC that + // contains the provided incomingMetadata. + GetLabels(incomingMetadata metadata.MD) map[string]string +} diff --git a/test/xds/xds_telemetry_labels_test.go b/test/xds/xds_telemetry_labels_test.go index 8607b6179816..11858d8fbce4 100644 --- a/test/xds/xds_telemetry_labels_test.go +++ b/test/xds/xds_telemetry_labels_test.go @@ -37,7 +37,9 @@ import ( ) const serviceNameKey = "service_name" +const serviceNameKeyCSM = "csm.service_name" const serviceNamespaceKey = "service_namespace" +const serviceNamespaceKeyCSM = "csm.service_namespace" const serviceNameValue = "grpc-service" const serviceNamespaceValue = "grpc-service-namespace" @@ -125,11 +127,11 @@ func (fsh *fakeStatsHandler) HandleRPC(ctx context.Context, rs stats.RPCStats) { // aren't started. All of these should have access to the desired telemetry // labels. case *stats.OutPayload, *stats.InPayload, *stats.End: - if label, ok := fsh.labels.TelemetryLabels[serviceNameKey]; !ok || label != serviceNameValue { - fsh.t.Fatalf("for telemetry label %v, want: %v, got: %v", serviceNameKey, serviceNameValue, label) + if label, ok := fsh.labels.TelemetryLabels[serviceNameKeyCSM]; !ok || label != serviceNameValue { + fsh.t.Fatalf("for telemetry label %v, want: %v, got: %v", serviceNameKeyCSM, serviceNameValue, label) } - if label, ok := fsh.labels.TelemetryLabels[serviceNamespaceKey]; !ok || label != serviceNamespaceValue { - fsh.t.Fatalf("for telemetry label %v, want: %v, got: %v", serviceNamespaceKey, serviceNamespaceValue, label) + if label, ok := fsh.labels.TelemetryLabels[serviceNamespaceKeyCSM]; !ok || label != serviceNamespaceValue { + fsh.t.Fatalf("for telemetry label %v, want: %v, got: %v", serviceNamespaceKeyCSM, serviceNamespaceValue, label) } default: diff --git a/xds/internal/xdsclient/xdsresource/unmarshal_cds.go b/xds/internal/xdsclient/xdsresource/unmarshal_cds.go index 276bf405e330..b6b22592cef2 100644 --- a/xds/internal/xdsclient/xdsresource/unmarshal_cds.go +++ b/xds/internal/xdsclient/xdsresource/unmarshal_cds.go @@ -88,12 +88,12 @@ func validateClusterAndConstructClusterUpdate(cluster *v3clusterpb.Cluster) (Clu if fields := val.GetFields(); fields != nil { if val, ok := fields["service_name"]; ok { if _, ok := val.GetKind().(*structpb.Value_StringValue); ok { - telemetryLabels["service_name"] = val.GetStringValue() + telemetryLabels["csm.service_name"] = val.GetStringValue() } } if val, ok := fields["service_namespace"]; ok { if _, ok := val.GetKind().(*structpb.Value_StringValue); ok { - telemetryLabels["service_namespace"] = val.GetStringValue() + telemetryLabels["csm.service_namespace"] = val.GetStringValue() } } } diff --git a/xds/internal/xdsclient/xdsresource/unmarshal_cds_test.go b/xds/internal/xdsclient/xdsresource/unmarshal_cds_test.go index a345466961c2..983e7e890943 100644 --- a/xds/internal/xdsclient/xdsresource/unmarshal_cds_test.go +++ b/xds/internal/xdsclient/xdsresource/unmarshal_cds_test.go @@ -1376,8 +1376,8 @@ func (s) TestUnmarshalCluster(t *testing.T) { LRSServerConfig: ClusterLRSServerSelf, Raw: v3ClusterAnyWithTelemetryLabels, TelemetryLabels: map[string]string{ - "service_name": "grpc-service", - "service_namespace": "grpc-service-namespace", + "csm.service_name": "grpc-service", + "csm.service_namespace": "grpc-service-namespace", }, }, }, @@ -1391,7 +1391,7 @@ func (s) TestUnmarshalCluster(t *testing.T) { LRSServerConfig: ClusterLRSServerSelf, Raw: v3ClusterAnyWithTelemetryLabelsIgnoreSome, TelemetryLabels: map[string]string{ - "service_name": "grpc-service", + "csm.service_name": "grpc-service", }, }, },