io.grpc.gcp.csm.observability.MetadataExchanger Maven / Gradle / Ivy
The newest version!
/*
* Copyright 2024 The 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 io.grpc.gcp.csm.observability;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.io.BaseEncoding;
import com.google.protobuf.Struct;
import com.google.protobuf.Value;
import io.grpc.CallOptions;
import io.grpc.ForwardingServerCall.SimpleForwardingServerCall;
import io.grpc.Metadata;
import io.grpc.ServerBuilder;
import io.grpc.ServerCall;
import io.grpc.ServerCallHandler;
import io.grpc.ServerInterceptor;
import io.grpc.Status;
import io.grpc.internal.JsonParser;
import io.grpc.internal.JsonUtil;
import io.grpc.opentelemetry.InternalOpenTelemetryPlugin;
import io.grpc.protobuf.ProtoUtils;
import io.grpc.xds.ClusterImplLoadBalancerProvider;
import io.grpc.xds.InternalGrpcBootstrapperImpl;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.common.AttributesBuilder;
import io.opentelemetry.contrib.gcp.resource.GCPResourceProvider;
import io.opentelemetry.sdk.autoconfigure.ResourceConfiguration;
import java.net.URI;
import java.util.Map;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* OpenTelemetryPlugin implementing metadata-based workload property exchange for both client and
* server. Is responsible for determining the metadata, communicating the metadata, and adding local
* and remote details to metrics.
*/
final class MetadataExchanger implements InternalOpenTelemetryPlugin {
private static final Logger logger = Logger.getLogger(MetadataExchanger.class.getName());
private static final AttributeKey CLOUD_PLATFORM =
AttributeKey.stringKey("cloud.platform");
private static final AttributeKey K8S_NAMESPACE_NAME =
AttributeKey.stringKey("k8s.namespace.name");
private static final AttributeKey K8S_CLUSTER_NAME =
AttributeKey.stringKey("k8s.cluster.name");
private static final AttributeKey CLOUD_AVAILABILITY_ZONE =
AttributeKey.stringKey("cloud.availability_zone");
private static final AttributeKey CLOUD_REGION =
AttributeKey.stringKey("cloud.region");
private static final AttributeKey CLOUD_ACCOUNT_ID =
AttributeKey.stringKey("cloud.account.id");
private static final Metadata.Key SEND_KEY =
Metadata.Key.of("x-envoy-peer-metadata", Metadata.ASCII_STRING_MARSHALLER);
private static final Metadata.Key RECV_KEY =
Metadata.Key.of("x-envoy-peer-metadata", new BinaryToAsciiMarshaller<>(
ProtoUtils.metadataMarshaller(Struct.getDefaultInstance())));
private static final String EXCHANGE_TYPE = "type";
private static final String EXCHANGE_CANONICAL_SERVICE = "canonical_service";
private static final String EXCHANGE_PROJECT_ID = "project_id";
private static final String EXCHANGE_LOCATION = "location";
private static final String EXCHANGE_CLUSTER_NAME = "cluster_name";
private static final String EXCHANGE_NAMESPACE_NAME = "namespace_name";
private static final String EXCHANGE_WORKLOAD_NAME = "workload_name";
private static final String TYPE_GKE = "gcp_kubernetes_engine";
private static final String TYPE_GCE = "gcp_compute_engine";
private final String localMetadata;
private final Attributes localAttributes;
public MetadataExchanger() {
this(
addOtelResourceAttributes(new GCPResourceProvider().getAttributes()),
System::getenv,
InternalGrpcBootstrapperImpl::getJsonContent);
}
MetadataExchanger(Attributes platformAttributes, Lookup env, Supplier xdsBootstrap) {
String type = platformAttributes.get(CLOUD_PLATFORM);
String canonicalService = env.get("CSM_CANONICAL_SERVICE_NAME");
Struct.Builder struct = Struct.newBuilder();
put(struct, EXCHANGE_TYPE, type);
put(struct, EXCHANGE_CANONICAL_SERVICE, canonicalService);
if (TYPE_GKE.equals(type)) {
String location = platformAttributes.get(CLOUD_AVAILABILITY_ZONE);
if (location == null) {
location = platformAttributes.get(CLOUD_REGION);
}
put(struct, EXCHANGE_WORKLOAD_NAME, env.get("CSM_WORKLOAD_NAME"));
put(struct, EXCHANGE_NAMESPACE_NAME, platformAttributes.get(K8S_NAMESPACE_NAME));
put(struct, EXCHANGE_CLUSTER_NAME, platformAttributes.get(K8S_CLUSTER_NAME));
put(struct, EXCHANGE_LOCATION, location);
put(struct, EXCHANGE_PROJECT_ID, platformAttributes.get(CLOUD_ACCOUNT_ID));
} else if (TYPE_GCE.equals(type)) {
String location = platformAttributes.get(CLOUD_AVAILABILITY_ZONE);
if (location == null) {
location = platformAttributes.get(CLOUD_REGION);
}
put(struct, EXCHANGE_WORKLOAD_NAME, env.get("CSM_WORKLOAD_NAME"));
put(struct, EXCHANGE_LOCATION, location);
put(struct, EXCHANGE_PROJECT_ID, platformAttributes.get(CLOUD_ACCOUNT_ID));
}
localMetadata = BaseEncoding.base64().encode(struct.build().toByteArray());
localAttributes = Attributes.builder()
.put("csm.mesh_id", nullIsUnknown(getMeshId(xdsBootstrap)))
.put("csm.workload_canonical_service", nullIsUnknown(canonicalService))
.build();
}
private static String nullIsUnknown(String value) {
return value == null ? "unknown" : value;
}
private static void put(Struct.Builder struct, String key, String value) {
value = nullIsUnknown(value);
struct.putFields(key, Value.newBuilder().setStringValue(value).build());
}
private static void put(AttributesBuilder attributes, String key, Value value) {
attributes.put(key, nullIsUnknown(fromValue(value)));
}
private static String fromValue(Value value) {
if (value == null) {
return null;
}
if (value.getKindCase() != Value.KindCase.STRING_VALUE) {
return null;
}
return value.getStringValue();
}
private static Attributes addOtelResourceAttributes(Attributes platformAttributes) {
// Can't inject env variables as ResourceConfiguration requires the large ConfigProperties API
// to inject our own values and a default implementation isn't provided. So this reads directly
// from System.getenv().
Attributes envAttributes = ResourceConfiguration
.createEnvironmentResource()
.getAttributes();
AttributesBuilder builder = platformAttributes.toBuilder();
builder.putAll(envAttributes);
return builder.build();
}
@VisibleForTesting
static String getMeshId(Supplier xdsBootstrap) {
try {
@SuppressWarnings("unchecked")
Map rawBootstrap = (Map) JsonParser.parse(xdsBootstrap.get());
Map node = JsonUtil.getObject(rawBootstrap, "node");
String id = JsonUtil.getString(node, "id");
Preconditions.checkNotNull(id, "id");
String[] parts = id.split("/", 6);
if (!(parts.length == 6
&& parts[0].equals("projects")
&& parts[2].equals("networks")
&& parts[3].startsWith("mesh:")
&& parts[4].equals("nodes"))) {
throw new Exception("node id didn't match mesh format: " + id);
}
return parts[3].substring("mesh:".length());
} catch (Exception e) {
logger.log(Level.INFO, "Failed to determine mesh ID for CSM", e);
return null;
}
}
private void addLabels(AttributesBuilder to, Struct struct) {
to.putAll(localAttributes);
Map remote = struct.getFieldsMap();
Value typeValue = remote.get(EXCHANGE_TYPE);
String type = fromValue(typeValue);
put(to, "csm.remote_workload_type", typeValue);
put(to, "csm.remote_workload_canonical_service", remote.get(EXCHANGE_CANONICAL_SERVICE));
if (TYPE_GKE.equals(type)) {
put(to, "csm.remote_workload_project_id", remote.get(EXCHANGE_PROJECT_ID));
put(to, "csm.remote_workload_location", remote.get(EXCHANGE_LOCATION));
put(to, "csm.remote_workload_cluster_name", remote.get(EXCHANGE_CLUSTER_NAME));
put(to, "csm.remote_workload_namespace_name", remote.get(EXCHANGE_NAMESPACE_NAME));
put(to, "csm.remote_workload_name", remote.get(EXCHANGE_WORKLOAD_NAME));
} else if (TYPE_GCE.equals(type)) {
put(to, "csm.remote_workload_project_id", remote.get(EXCHANGE_PROJECT_ID));
put(to, "csm.remote_workload_location", remote.get(EXCHANGE_LOCATION));
put(to, "csm.remote_workload_name", remote.get(EXCHANGE_WORKLOAD_NAME));
}
}
@Override
public boolean enablePluginForChannel(String target) {
URI uri;
try {
uri = new URI(target);
} catch (Exception ex) {
return false;
}
String authority = uri.getAuthority();
return "xds".equals(uri.getScheme())
&& (authority == null || "traffic-director-global.xds.googleapis.com".equals(authority));
}
@Override
public ClientCallPlugin newClientCallPlugin() {
return new ClientCallState();
}
public void configureServerBuilder(ServerBuilder> serverBuilder) {
serverBuilder.intercept(new ServerCallInterceptor());
}
@Override
public ServerStreamPlugin newServerStreamPlugin(Metadata inboundMetadata) {
return new ServerStreamState(inboundMetadata.get(RECV_KEY));
}
final class ClientCallState implements ClientCallPlugin {
private volatile Value serviceName;
private volatile Value serviceNamespace;
@Override
public ClientStreamPlugin newClientStreamPlugin() {
return new ClientStreamState();
}
@Override
public CallOptions filterCallOptions(CallOptions options) {
Consumer