google.golang.org/grpc@v1.72.2/stats/opentelemetry/csm/pluginoption.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 contains utilities for Google Cloud Service Mesh observability. 20 package csm 21 22 import ( 23 "context" 24 "encoding/base64" 25 "net/url" 26 "os" 27 28 "google.golang.org/grpc/grpclog" 29 "google.golang.org/grpc/metadata" 30 "google.golang.org/grpc/stats/opentelemetry/internal" 31 "google.golang.org/protobuf/proto" 32 "google.golang.org/protobuf/types/known/structpb" 33 34 "go.opentelemetry.io/contrib/detectors/gcp" 35 "go.opentelemetry.io/otel/attribute" 36 "go.opentelemetry.io/otel/sdk/resource" 37 ) 38 39 var logger = grpclog.Component("csm-observability-plugin") 40 41 // pluginOption emits CSM Labels from the environment and metadata exchange 42 // for csm channels and all servers. 43 // 44 // Do not use this directly; use newPluginOption instead. 45 type pluginOption struct { 46 // localLabels are the labels that identify the local environment a binary 47 // is run in, and will be emitted from the CSM Plugin Option. 48 localLabels map[string]string 49 // metadataExchangeLabelsEncoded are the metadata exchange labels to be sent 50 // as the value of metadata key "x-envoy-peer-metadata" in proto wire format 51 // and base 64 encoded. This gets sent out from all the servers running in 52 // this process and for csm channels. 53 metadataExchangeLabelsEncoded string 54 } 55 56 // newPluginOption returns a new pluginOption with local labels and metadata 57 // exchange labels derived from the environment. 58 func newPluginOption(ctx context.Context) internal.PluginOption { 59 localLabels, metadataExchangeLabelsEncoded := constructMetadataFromEnv(ctx) 60 61 return &pluginOption{ 62 localLabels: localLabels, 63 metadataExchangeLabelsEncoded: metadataExchangeLabelsEncoded, 64 } 65 } 66 67 // NewLabelsMD returns a metadata.MD with the CSM labels as an encoded protobuf 68 // Struct as the value of "x-envoy-peer-metadata". 69 func (cpo *pluginOption) GetMetadata() metadata.MD { 70 return metadata.New(map[string]string{ 71 metadataExchangeKey: cpo.metadataExchangeLabelsEncoded, 72 }) 73 } 74 75 // GetLabels gets the CSM peer labels from the metadata provided. It returns 76 // "unknown" for labels not found. Labels returned depend on the remote type. 77 // Additionally, local labels determined at initialization time are appended to 78 // labels returned, in addition to the optionalLabels provided. 79 func (cpo *pluginOption) GetLabels(md metadata.MD) map[string]string { 80 labels := map[string]string{ // Remote labels if type is unknown (i.e. unset or error processing x-envoy-peer-metadata) 81 "csm.remote_workload_type": "unknown", 82 "csm.remote_workload_canonical_service": "unknown", 83 } 84 // Append the local labels. 85 for k, v := range cpo.localLabels { 86 labels[k] = v 87 } 88 89 val := md.Get("x-envoy-peer-metadata") 90 // This can't happen if corresponding csm client because of proto wire 91 // format encoding, but since it is arbitrary off the wire be safe. 92 if len(val) != 1 { 93 logger.Warningf("length of md values of \"x-envoy-peer-metadata\" is not 1, is %v", len(val)) 94 return labels 95 } 96 97 protoWireFormat, err := base64.RawStdEncoding.DecodeString(val[0]) 98 if err != nil { 99 logger.Warningf("error base 64 decoding value of \"x-envoy-peer-metadata\": %v", err) 100 return labels 101 } 102 103 spb := &structpb.Struct{} 104 if err := proto.Unmarshal(protoWireFormat, spb); err != nil { 105 logger.Warningf("error unmarshalling value of \"x-envoy-peer-metadata\" into proto: %v", err) 106 return labels 107 } 108 109 fields := spb.GetFields() 110 111 labels["csm.remote_workload_type"] = getFromMetadata("type", fields) 112 // The value of “csm.remote_workload_canonical_service” comes from 113 // MetadataExchange with the key “canonical_service”. (Note that this should 114 // be read even if the remote type is unknown.) 115 labels["csm.remote_workload_canonical_service"] = getFromMetadata("canonical_service", fields) 116 117 // Unset/unknown types, and types that aren't GKE or GCP return early with 118 // just local labels, remote_workload_type and 119 // remote_workload_canonical_service labels. 120 workloadType := labels["csm.remote_workload_type"] 121 if workloadType != "gcp_kubernetes_engine" && workloadType != "gcp_compute_engine" { 122 return labels 123 } 124 // GKE and GCE labels. 125 labels["csm.remote_workload_project_id"] = getFromMetadata("project_id", fields) 126 labels["csm.remote_workload_location"] = getFromMetadata("location", fields) 127 labels["csm.remote_workload_name"] = getFromMetadata("workload_name", fields) 128 if workloadType == "gcp_compute_engine" { 129 return labels 130 } 131 132 // GKE only labels. 133 labels["csm.remote_workload_cluster_name"] = getFromMetadata("cluster_name", fields) 134 labels["csm.remote_workload_namespace_name"] = getFromMetadata("namespace_name", fields) 135 return labels 136 } 137 138 // getFromMetadata gets the value for the metadata key from the protobuf 139 // metadata. Returns "unknown" if the metadata is not found in the protobuf 140 // metadata, or if the value is not a string value. Returns the string value 141 // otherwise. 142 func getFromMetadata(metadataKey string, metadata map[string]*structpb.Value) string { 143 if metadata != nil { 144 if metadataVal, ok := metadata[metadataKey]; ok { 145 if _, ok := metadataVal.GetKind().(*structpb.Value_StringValue); ok { 146 return metadataVal.GetStringValue() 147 } 148 } 149 } 150 return "unknown" 151 } 152 153 // getFromResource gets the value for the resource key from the attribute set. 154 // Returns "unknown" if the resourceKey is not found in the attribute set or is 155 // not a string value, the string value otherwise. 156 func getFromResource(resourceKey attribute.Key, set *attribute.Set) string { 157 if set != nil { 158 if resourceVal, ok := set.Value(resourceKey); ok && resourceVal.Type() == attribute.STRING { 159 return resourceVal.AsString() 160 } 161 } 162 return "unknown" 163 } 164 165 // getEnv returns "unknown" if environment variable is unset, the environment 166 // variable otherwise. 167 func getEnv(name string) string { 168 if val, ok := os.LookupEnv(name); ok { 169 return val 170 } 171 return "unknown" 172 } 173 174 var ( 175 // This function will be overridden in unit tests. 176 getAttrSetFromResourceDetector = func(ctx context.Context) *attribute.Set { 177 r, err := resource.New(ctx, resource.WithFromEnv(), resource.WithDetectors(gcp.NewDetector())) 178 if err != nil { 179 logger.Warningf("error reading OpenTelemetry resource: %v", err) 180 } 181 if r != nil { 182 // It's possible for resource.New to return partial data alongside 183 // an error. In this case, use partial data and set "unknown" for 184 // labels missing. 185 return r.Set() 186 } 187 return nil 188 } 189 ) 190 191 // constructMetadataFromEnv creates local labels and labels to send to the peer 192 // using metadata exchange based off resource detection and environment 193 // variables. 194 // 195 // Returns local labels, and base 64 encoded protobuf.Struct containing metadata 196 // exchange labels. 197 func constructMetadataFromEnv(ctx context.Context) (map[string]string, string) { 198 set := getAttrSetFromResourceDetector(ctx) 199 200 labels := make(map[string]string) 201 labels["type"] = getFromResource("cloud.platform", set) 202 labels["canonical_service"] = getEnv("CSM_CANONICAL_SERVICE_NAME") 203 204 // If type is not GCE or GKE only metadata exchange labels are "type" and 205 // "canonical_service". 206 cloudPlatformVal := labels["type"] 207 if cloudPlatformVal != "gcp_kubernetes_engine" && cloudPlatformVal != "gcp_compute_engine" { 208 return initializeLocalAndMetadataLabels(labels) 209 } 210 211 // GCE and GKE labels: 212 labels["workload_name"] = getEnv("CSM_WORKLOAD_NAME") 213 214 locationVal := "unknown" 215 if resourceVal, ok := set.Value("cloud.availability_zone"); ok && resourceVal.Type() == attribute.STRING { 216 locationVal = resourceVal.AsString() 217 } else if resourceVal, ok = set.Value("cloud.region"); ok && resourceVal.Type() == attribute.STRING { 218 locationVal = resourceVal.AsString() 219 } 220 labels["location"] = locationVal 221 222 labels["project_id"] = getFromResource("cloud.account.id", set) 223 if cloudPlatformVal == "gcp_compute_engine" { 224 return initializeLocalAndMetadataLabels(labels) 225 } 226 227 // GKE specific labels: 228 labels["namespace_name"] = getFromResource("k8s.namespace.name", set) 229 labels["cluster_name"] = getFromResource("k8s.cluster.name", set) 230 return initializeLocalAndMetadataLabels(labels) 231 } 232 233 // initializeLocalAndMetadataLabels csm local labels for a CSM Plugin Option to 234 // record. It also builds out a base 64 encoded protobuf.Struct containing the 235 // metadata exchange labels to be sent as part of metadata exchange from a CSM 236 // Plugin Option. 237 func initializeLocalAndMetadataLabels(labels map[string]string) (map[string]string, string) { 238 // The value of “csm.workload_canonical_service” comes from 239 // “CSM_CANONICAL_SERVICE_NAME” env var, “unknown” if unset. 240 val := labels["canonical_service"] 241 localLabels := make(map[string]string) 242 localLabels["csm.workload_canonical_service"] = val 243 localLabels["csm.mesh_id"] = getEnv("CSM_MESH_ID") 244 245 // Metadata exchange labels - can go ahead and encode into proto, and then 246 // base64. 247 pbLabels := &structpb.Struct{ 248 Fields: map[string]*structpb.Value{}, 249 } 250 for k, v := range labels { 251 pbLabels.Fields[k] = structpb.NewStringValue(v) 252 } 253 protoWireFormat, err := proto.Marshal(pbLabels) 254 metadataExchangeLabelsEncoded := "" 255 if err == nil { 256 metadataExchangeLabelsEncoded = base64.RawStdEncoding.EncodeToString(protoWireFormat) 257 } 258 // else - This behavior triggers server side to reply (if sent from a gRPC 259 // Client within this binary) with the metadata exchange labels. Even if 260 // client side has a problem marshaling proto into wire format, it can 261 // still use server labels so send an empty string as the value of 262 // x-envoy-peer-metadata. The presence of this metadata exchange header 263 // will cause server side to respond with metadata exchange labels. 264 265 return localLabels, metadataExchangeLabelsEncoded 266 } 267 268 // metadataExchangeKey is the key for HTTP metadata exchange. 269 const metadataExchangeKey = "x-envoy-peer-metadata" 270 271 func determineTargetCSM(parsedTarget *url.URL) bool { 272 // On the client-side, the channel target is used to determine if a channel is a 273 // CSM channel or not. CSM channels need to have an “xds” scheme and a 274 // "traffic-director-global.xds.googleapis.com" authority. In the cases where no 275 // authority is mentioned, the authority is assumed to be CSM. MetadataExchange 276 // is performed only for CSM channels. Non-metadata exchange labels are detected 277 // as described below. 278 return parsedTarget.Scheme == "xds" && (parsedTarget.Host == "" || parsedTarget.Host == "traffic-director-global.xds.googleapis.com") 279 }