1 //
2 //
3 // Copyright 2023 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 #include "src/cpp/ext/csm/metadata_exchange.h"
20
21 #include <grpc/slice.h>
22 #include <grpc/support/port_platform.h>
23 #include <stddef.h>
24
25 #include <algorithm>
26 #include <array>
27 #include <cstdint>
28 #include <unordered_map>
29
30 #include "absl/log/check.h"
31 #include "absl/status/statusor.h"
32 #include "absl/strings/escaping.h"
33 #include "absl/strings/str_split.h"
34 #include "absl/strings/string_view.h"
35 #include "absl/strings/strip.h"
36 #include "absl/types/optional.h"
37 #include "absl/types/variant.h"
38 #include "opentelemetry/sdk/resource/semantic_conventions.h"
39 #include "src/core/lib/iomgr/error.h"
40 #include "src/core/lib/slice/slice_internal.h"
41 #include "src/core/telemetry/call_tracer.h"
42 #include "src/core/util/env.h"
43 #include "src/cpp/ext/otel/key_value_iterable.h"
44 #include "upb/base/string_view.h"
45
46 namespace grpc {
47 namespace internal {
48
49 using OptionalLabelKey =
50 grpc_core::ClientCallTracer::CallAttemptTracer::OptionalLabelKey;
51
52 namespace {
53
54 // The keys that will be used in the Metadata Exchange between local and remote.
55 constexpr absl::string_view kMetadataExchangeTypeKey = "type";
56 constexpr absl::string_view kMetadataExchangeWorkloadNameKey = "workload_name";
57 constexpr absl::string_view kMetadataExchangeNamespaceNameKey =
58 "namespace_name";
59 constexpr absl::string_view kMetadataExchangeClusterNameKey = "cluster_name";
60 constexpr absl::string_view kMetadataExchangeLocationKey = "location";
61 constexpr absl::string_view kMetadataExchangeProjectIdKey = "project_id";
62 constexpr absl::string_view kMetadataExchangeCanonicalServiceKey =
63 "canonical_service";
64 // The keys that will be used for the local attributes when recording metrics.
65 constexpr absl::string_view kCanonicalServiceAttribute =
66 "csm.workload_canonical_service";
67 constexpr absl::string_view kMeshIdAttribute = "csm.mesh_id";
68 // The keys that will be used for the peer attributes when recording metrics.
69 constexpr absl::string_view kPeerTypeAttribute = "csm.remote_workload_type";
70 constexpr absl::string_view kPeerWorkloadNameAttribute =
71 "csm.remote_workload_name";
72 constexpr absl::string_view kPeerNamespaceNameAttribute =
73 "csm.remote_workload_namespace_name";
74 constexpr absl::string_view kPeerClusterNameAttribute =
75 "csm.remote_workload_cluster_name";
76 constexpr absl::string_view kPeerLocationAttribute =
77 "csm.remote_workload_location";
78 constexpr absl::string_view kPeerProjectIdAttribute =
79 "csm.remote_workload_project_id";
80 constexpr absl::string_view kPeerCanonicalServiceAttribute =
81 "csm.remote_workload_canonical_service";
82 // Type values used by Google Cloud Resource Detector
83 constexpr absl::string_view kGkeType = "gcp_kubernetes_engine";
84 constexpr absl::string_view kGceType = "gcp_compute_engine";
85
86 // A helper method that decodes the remote metadata \a slice as a protobuf
87 // Struct allocated on \a arena.
DecodeMetadata(grpc_core::Slice slice,upb_Arena * arena)88 google_protobuf_Struct* DecodeMetadata(grpc_core::Slice slice,
89 upb_Arena* arena) {
90 // Treat an empty slice as an invalid metadata value.
91 if (slice.empty()) {
92 return nullptr;
93 }
94 // Decode the slice.
95 std::string decoded_metadata;
96 bool metadata_decoded =
97 absl::Base64Unescape(slice.as_string_view(), &decoded_metadata);
98 if (metadata_decoded) {
99 return google_protobuf_Struct_parse(decoded_metadata.c_str(),
100 decoded_metadata.size(), arena);
101 }
102 return nullptr;
103 }
104
StringToGcpResourceType(absl::string_view type)105 MeshLabelsIterable::GcpResourceType StringToGcpResourceType(
106 absl::string_view type) {
107 if (type == kGkeType) {
108 return MeshLabelsIterable::GcpResourceType::kGke;
109 } else if (type == kGceType) {
110 return MeshLabelsIterable::GcpResourceType::kGce;
111 }
112 return MeshLabelsIterable::GcpResourceType::kUnknown;
113 }
114
AbslStrToUpbStr(absl::string_view str)115 upb_StringView AbslStrToUpbStr(absl::string_view str) {
116 return upb_StringView_FromDataAndSize(str.data(), str.size());
117 }
118
UpbStrToAbslStr(upb_StringView str)119 absl::string_view UpbStrToAbslStr(upb_StringView str) {
120 return absl::string_view(str.data, str.size);
121 }
122
AddStringKeyValueToStructProto(google_protobuf_Struct * struct_pb,absl::string_view key,absl::string_view value,upb_Arena * arena)123 void AddStringKeyValueToStructProto(google_protobuf_Struct* struct_pb,
124 absl::string_view key,
125 absl::string_view value, upb_Arena* arena) {
126 google_protobuf_Value* value_pb = google_protobuf_Value_new(arena);
127 google_protobuf_Value_set_string_value(value_pb, AbslStrToUpbStr(value));
128 google_protobuf_Struct_fields_set(struct_pb, AbslStrToUpbStr(key), value_pb,
129 arena);
130 }
131
GetStringValueFromAttributeMap(const opentelemetry::sdk::common::AttributeMap & map,absl::string_view key)132 absl::string_view GetStringValueFromAttributeMap(
133 const opentelemetry::sdk::common::AttributeMap& map,
134 absl::string_view key) {
135 const auto& attributes = map.GetAttributes();
136 const auto it = attributes.find(std::string(key));
137 if (it == attributes.end()) {
138 return "unknown";
139 }
140 const auto* string_value = absl::get_if<std::string>(&it->second);
141 if (string_value == nullptr) {
142 return "unknown";
143 }
144 return *string_value;
145 }
146
GetStringValueFromUpbStruct(google_protobuf_Struct * struct_pb,absl::string_view key,upb_Arena * arena)147 absl::string_view GetStringValueFromUpbStruct(google_protobuf_Struct* struct_pb,
148 absl::string_view key,
149 upb_Arena* arena) {
150 if (struct_pb == nullptr) {
151 return "unknown";
152 }
153 google_protobuf_Value* value_pb = google_protobuf_Value_new(arena);
154 bool present = google_protobuf_Struct_fields_get(
155 struct_pb, AbslStrToUpbStr(key), &value_pb);
156 if (present) {
157 if (google_protobuf_Value_has_string_value(value_pb)) {
158 return UpbStrToAbslStr(google_protobuf_Value_string_value(value_pb));
159 }
160 }
161 return "unknown";
162 }
163
164 struct RemoteAttribute {
165 absl::string_view otel_attribute;
166 absl::string_view metadata_attribute;
167 };
168
169 constexpr std::array<RemoteAttribute, 2> kFixedAttributes = {
170 RemoteAttribute{kPeerTypeAttribute, kMetadataExchangeTypeKey},
171 RemoteAttribute{kPeerCanonicalServiceAttribute,
172 kMetadataExchangeCanonicalServiceKey},
173 };
174
175 constexpr std::array<RemoteAttribute, 5> kGkeAttributeList = {
176 RemoteAttribute{kPeerWorkloadNameAttribute,
177 kMetadataExchangeWorkloadNameKey},
178 RemoteAttribute{kPeerNamespaceNameAttribute,
179 kMetadataExchangeNamespaceNameKey},
180 RemoteAttribute{kPeerClusterNameAttribute, kMetadataExchangeClusterNameKey},
181 RemoteAttribute{kPeerLocationAttribute, kMetadataExchangeLocationKey},
182 RemoteAttribute{kPeerProjectIdAttribute, kMetadataExchangeProjectIdKey},
183 };
184
185 constexpr std::array<RemoteAttribute, 3> kGceAttributeList = {
186 RemoteAttribute{kPeerWorkloadNameAttribute,
187 kMetadataExchangeWorkloadNameKey},
188 RemoteAttribute{kPeerLocationAttribute, kMetadataExchangeLocationKey},
189 RemoteAttribute{kPeerProjectIdAttribute, kMetadataExchangeProjectIdKey},
190 };
191
GetAttributesForType(MeshLabelsIterable::GcpResourceType remote_type)192 absl::Span<const RemoteAttribute> GetAttributesForType(
193 MeshLabelsIterable::GcpResourceType remote_type) {
194 switch (remote_type) {
195 case MeshLabelsIterable::GcpResourceType::kGke:
196 return kGkeAttributeList;
197 case MeshLabelsIterable::GcpResourceType::kGce:
198 return kGceAttributeList;
199 default:
200 return {};
201 }
202 }
203
204 absl::optional<std::pair<absl::string_view, absl::string_view>>
NextFromAttributeList(absl::Span<const RemoteAttribute> attributes,size_t start_index,size_t curr,google_protobuf_Struct * decoded_metadata,upb_Arena * arena)205 NextFromAttributeList(absl::Span<const RemoteAttribute> attributes,
206 size_t start_index, size_t curr,
207 google_protobuf_Struct* decoded_metadata,
208 upb_Arena* arena) {
209 DCHECK_GE(curr, start_index);
210 const size_t index = curr - start_index;
211 if (index >= attributes.size()) return absl::nullopt;
212 const auto& attribute = attributes[index];
213 return std::make_pair(
214 attribute.otel_attribute,
215 GetStringValueFromUpbStruct(decoded_metadata,
216 attribute.metadata_attribute, arena));
217 }
218
219 } // namespace
220
221 //
222 // MeshLabelsIterable
223 //
224
MeshLabelsIterable(const std::vector<std::pair<absl::string_view,std::string>> & local_labels,grpc_core::Slice remote_metadata)225 MeshLabelsIterable::MeshLabelsIterable(
226 const std::vector<std::pair<absl::string_view, std::string>>& local_labels,
227 grpc_core::Slice remote_metadata)
228 : struct_pb_(DecodeMetadata(std::move(remote_metadata), arena_.ptr())),
229 local_labels_(local_labels),
230 remote_type_(StringToGcpResourceType(GetStringValueFromUpbStruct(
231 struct_pb_, kMetadataExchangeTypeKey, arena_.ptr()))) {}
232
233 absl::optional<std::pair<absl::string_view, absl::string_view>>
Next()234 MeshLabelsIterable::Next() {
235 size_t local_labels_size = local_labels_.size();
236 if (pos_ < local_labels_size) {
237 return local_labels_[pos_++];
238 }
239 const size_t fixed_attribute_end =
240 local_labels_size + kFixedAttributes.size();
241 if (pos_ < fixed_attribute_end) {
242 return NextFromAttributeList(kFixedAttributes, local_labels_size, pos_++,
243 struct_pb_, arena_.ptr());
244 }
245 return NextFromAttributeList(GetAttributesForType(remote_type_),
246 fixed_attribute_end, pos_++, struct_pb_,
247 arena_.ptr());
248 }
249
Size() const250 size_t MeshLabelsIterable::Size() const {
251 return local_labels_.size() + kFixedAttributes.size() +
252 GetAttributesForType(remote_type_).size();
253 }
254
255 //
256 // ServiceMeshLabelsInjector
257 //
258
ServiceMeshLabelsInjector(const opentelemetry::sdk::common::AttributeMap & map)259 ServiceMeshLabelsInjector::ServiceMeshLabelsInjector(
260 const opentelemetry::sdk::common::AttributeMap& map) {
261 upb::Arena arena;
262 auto* metadata = google_protobuf_Struct_new(arena.ptr());
263 // Assume kubernetes for now
264 absl::string_view type_value = GetStringValueFromAttributeMap(
265 map, opentelemetry::sdk::resource::SemanticConventions::kCloudPlatform);
266 std::string workload_name_value =
267 grpc_core::GetEnv("CSM_WORKLOAD_NAME").value_or("unknown");
268 absl::string_view namespace_value = GetStringValueFromAttributeMap(
269 map,
270 opentelemetry::sdk::resource::SemanticConventions::kK8sNamespaceName);
271 absl::string_view cluster_name_value = GetStringValueFromAttributeMap(
272 map, opentelemetry::sdk::resource::SemanticConventions::kK8sClusterName);
273 absl::string_view location_value = GetStringValueFromAttributeMap(
274 map, opentelemetry::sdk::resource::SemanticConventions::
275 kCloudAvailabilityZone); // if zonal
276 if (location_value == "unknown") {
277 location_value = GetStringValueFromAttributeMap(
278 map, opentelemetry::sdk::resource::SemanticConventions::
279 kCloudRegion); // if regional
280 }
281 absl::string_view project_id_value = GetStringValueFromAttributeMap(
282 map, opentelemetry::sdk::resource::SemanticConventions::kCloudAccountId);
283 std::string canonical_service_value =
284 grpc_core::GetEnv("CSM_CANONICAL_SERVICE_NAME").value_or("unknown");
285 // Create metadata to be sent over wire.
286 AddStringKeyValueToStructProto(metadata, kMetadataExchangeTypeKey, type_value,
287 arena.ptr());
288 AddStringKeyValueToStructProto(metadata, kMetadataExchangeCanonicalServiceKey,
289 canonical_service_value, arena.ptr());
290 if (type_value == kGkeType) {
291 AddStringKeyValueToStructProto(metadata, kMetadataExchangeWorkloadNameKey,
292 workload_name_value, arena.ptr());
293 AddStringKeyValueToStructProto(metadata, kMetadataExchangeNamespaceNameKey,
294 namespace_value, arena.ptr());
295 AddStringKeyValueToStructProto(metadata, kMetadataExchangeClusterNameKey,
296 cluster_name_value, arena.ptr());
297 AddStringKeyValueToStructProto(metadata, kMetadataExchangeLocationKey,
298 location_value, arena.ptr());
299 AddStringKeyValueToStructProto(metadata, kMetadataExchangeProjectIdKey,
300 project_id_value, arena.ptr());
301 } else if (type_value == kGceType) {
302 AddStringKeyValueToStructProto(metadata, kMetadataExchangeWorkloadNameKey,
303 workload_name_value, arena.ptr());
304 AddStringKeyValueToStructProto(metadata, kMetadataExchangeLocationKey,
305 location_value, arena.ptr());
306 AddStringKeyValueToStructProto(metadata, kMetadataExchangeProjectIdKey,
307 project_id_value, arena.ptr());
308 }
309
310 size_t output_length;
311 char* output =
312 google_protobuf_Struct_serialize(metadata, arena.ptr(), &output_length);
313 serialized_labels_to_send_ = grpc_core::Slice::FromCopiedString(
314 absl::Base64Escape(absl::string_view(output, output_length)));
315 // Fill up local labels map. The rest we get from the detected Resource and
316 // from the peer.
317 local_labels_.emplace_back(kCanonicalServiceAttribute,
318 canonical_service_value);
319 local_labels_.emplace_back(
320 kMeshIdAttribute, grpc_core::GetEnv("CSM_MESH_ID").value_or("unknown"));
321 }
322
GetLabels(grpc_metadata_batch * incoming_initial_metadata) const323 std::unique_ptr<LabelsIterable> ServiceMeshLabelsInjector::GetLabels(
324 grpc_metadata_batch* incoming_initial_metadata) const {
325 auto peer_metadata =
326 incoming_initial_metadata->Take(grpc_core::XEnvoyPeerMetadata());
327 return std::make_unique<MeshLabelsIterable>(
328 local_labels_, peer_metadata.has_value() ? *std::move(peer_metadata)
329 : grpc_core::Slice());
330 }
331
AddLabels(grpc_metadata_batch * outgoing_initial_metadata,LabelsIterable * labels_from_incoming_metadata) const332 void ServiceMeshLabelsInjector::AddLabels(
333 grpc_metadata_batch* outgoing_initial_metadata,
334 LabelsIterable* labels_from_incoming_metadata) const {
335 // On the server, if the labels from incoming metadata did not have a
336 // non-empty base64 encoded "x-envoy-peer-metadata", do not perform metadata
337 // exchange.
338 if (labels_from_incoming_metadata != nullptr &&
339 !static_cast<MeshLabelsIterable*>(labels_from_incoming_metadata)
340 ->GotRemoteLabels()) {
341 return;
342 }
343 outgoing_initial_metadata->Set(grpc_core::XEnvoyPeerMetadata(),
344 serialized_labels_to_send_.Ref());
345 }
346
AddOptionalLabels(bool is_client,absl::Span<const grpc_core::RefCountedStringValue> optional_labels,opentelemetry::nostd::function_ref<bool (opentelemetry::nostd::string_view,opentelemetry::common::AttributeValue)> callback) const347 bool ServiceMeshLabelsInjector::AddOptionalLabels(
348 bool is_client,
349 absl::Span<const grpc_core::RefCountedStringValue> optional_labels,
350 opentelemetry::nostd::function_ref<
351 bool(opentelemetry::nostd::string_view,
352 opentelemetry::common::AttributeValue)>
353 callback) const {
354 if (!is_client) {
355 // Currently the CSM optional labels are only set on client.
356 return true;
357 }
358 // Performs JSON label name format to CSM Observability Metric spec format
359 // conversion.
360 absl::string_view service_name =
361 optional_labels[static_cast<size_t>(
362 grpc_core::ClientCallTracer::CallAttemptTracer::
363 OptionalLabelKey::kXdsServiceName)]
364 .as_string_view();
365 absl::string_view service_namespace =
366 optional_labels[static_cast<size_t>(
367 grpc_core::ClientCallTracer::CallAttemptTracer::
368 OptionalLabelKey::kXdsServiceNamespace)]
369 .as_string_view();
370 return callback("csm.service_name",
371 service_name.empty()
372 ? "unknown"
373 : AbslStrViewToOpenTelemetryStrView(service_name)) &&
374 callback("csm.service_namespace_name",
375 service_namespace.empty()
376 ? "unknown"
377 : AbslStrViewToOpenTelemetryStrView(service_namespace));
378 }
379
380 } // namespace internal
381 } // namespace grpc
382