internal/resourcemapping/resourcemapping.go (231 lines of code) (raw):

// Copyright 2022 Google LLC // // 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 // // https://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 resourcemapping import ( "strings" semconv "go.opentelemetry.io/otel/semconv/v1.24.0" monitoredrespb "google.golang.org/genproto/googleapis/api/monitoredres" ) const ( ProjectIDAttributeKey = "gcp.project.id" awsAccount = "aws_account" awsEc2Instance = "aws_ec2_instance" clusterName = "cluster_name" containerName = "container_name" gceInstance = "gce_instance" genericNode = "generic_node" genericTask = "generic_task" instanceID = "instance_id" job = "job" k8sCluster = "k8s_cluster" k8sContainer = "k8s_container" k8sNode = "k8s_node" k8sPod = "k8s_pod" location = "location" namespace = "namespace" namespaceName = "namespace_name" nodeID = "node_id" nodeName = "node_name" podName = "pod_name" region = "region" taskID = "task_id" zone = "zone" gaeInstance = "gae_instance" gaeApp = "gae_app" gaeModuleID = "module_id" gaeVersionID = "version_id" cloudRunRevision = "cloud_run_revision" cloudFunction = "cloud_function" cloudFunctionName = "function_name" serviceName = "service_name" configurationName = "configuration_name" revisionName = "revision_name" bmsInstance = "baremetalsolution.googleapis.com/Instance" unknownServicePrefix = "unknown_service" ) var ( // monitoredResourceMappings contains mappings of GCM resource label keys onto mapping config from OTel // resource for a given monitored resource type. monitoredResourceMappings = map[string]map[string]struct { // If none of the otelKeys are present in the Resource, fallback to this literal value fallbackLiteral string // OTel resource keys to try and populate the resource label from. For entries with // multiple OTel resource keys, the keys' values will be coalesced in order until there // is a non-empty value. otelKeys []string }{ gceInstance: { zone: {otelKeys: []string{string(semconv.CloudAvailabilityZoneKey)}}, instanceID: {otelKeys: []string{string(semconv.HostIDKey)}}, }, k8sContainer: { location: {otelKeys: []string{ string(semconv.CloudAvailabilityZoneKey), string(semconv.CloudRegionKey), }}, clusterName: {otelKeys: []string{string(semconv.K8SClusterNameKey)}}, namespaceName: {otelKeys: []string{string(semconv.K8SNamespaceNameKey)}}, podName: {otelKeys: []string{string(semconv.K8SPodNameKey)}}, containerName: {otelKeys: []string{string(semconv.K8SContainerNameKey)}}, }, k8sPod: { location: {otelKeys: []string{ string(semconv.CloudAvailabilityZoneKey), string(semconv.CloudRegionKey), }}, clusterName: {otelKeys: []string{string(semconv.K8SClusterNameKey)}}, namespaceName: {otelKeys: []string{string(semconv.K8SNamespaceNameKey)}}, podName: {otelKeys: []string{string(semconv.K8SPodNameKey)}}, }, k8sNode: { location: {otelKeys: []string{ string(semconv.CloudAvailabilityZoneKey), string(semconv.CloudRegionKey), }}, clusterName: {otelKeys: []string{string(semconv.K8SClusterNameKey)}}, nodeName: {otelKeys: []string{string(semconv.K8SNodeNameKey)}}, }, k8sCluster: { location: {otelKeys: []string{ string(semconv.CloudAvailabilityZoneKey), string(semconv.CloudRegionKey), }}, clusterName: {otelKeys: []string{string(semconv.K8SClusterNameKey)}}, }, gaeInstance: { location: {otelKeys: []string{ string(semconv.CloudAvailabilityZoneKey), string(semconv.CloudRegionKey), }}, gaeModuleID: {otelKeys: []string{string(semconv.FaaSNameKey)}}, gaeVersionID: {otelKeys: []string{string(semconv.FaaSVersionKey)}}, instanceID: {otelKeys: []string{string(semconv.FaaSInstanceKey)}}, }, gaeApp: { location: {otelKeys: []string{ string(semconv.CloudAvailabilityZoneKey), string(semconv.CloudRegionKey), }}, gaeModuleID: {otelKeys: []string{string(semconv.FaaSNameKey)}}, gaeVersionID: {otelKeys: []string{string(semconv.FaaSVersionKey)}}, }, awsEc2Instance: { instanceID: {otelKeys: []string{string(semconv.HostIDKey)}}, region: { otelKeys: []string{ string(semconv.CloudAvailabilityZoneKey), string(semconv.CloudRegionKey), }, }, awsAccount: {otelKeys: []string{string(semconv.CloudAccountIDKey)}}, }, bmsInstance: { location: {otelKeys: []string{string(semconv.CloudRegionKey)}}, instanceID: {otelKeys: []string{string(semconv.HostIDKey)}}, }, genericTask: { location: { otelKeys: []string{ string(semconv.CloudAvailabilityZoneKey), string(semconv.CloudRegionKey), }, fallbackLiteral: "global", }, namespace: {otelKeys: []string{string(semconv.ServiceNamespaceKey)}}, job: {otelKeys: []string{string(semconv.ServiceNameKey), string(semconv.FaaSNameKey)}}, taskID: {otelKeys: []string{string(semconv.ServiceInstanceIDKey), string(semconv.FaaSInstanceKey)}}, }, genericNode: { location: { otelKeys: []string{ string(semconv.CloudAvailabilityZoneKey), string(semconv.CloudRegionKey), }, fallbackLiteral: "global", }, namespace: {otelKeys: []string{string(semconv.ServiceNamespaceKey)}}, nodeID: {otelKeys: []string{string(semconv.HostIDKey), string(semconv.HostNameKey)}}, }, } ) // ReadOnlyAttributes is an interface to abstract between pulling attributes from PData library or OTEL SDK. type ReadOnlyAttributes interface { GetString(string) (string, bool) } // ResourceAttributesToLoggingMonitoredResource converts from a set of OTEL resource attributes into a // GCP monitored resource type and label set for Cloud Logging. // E.g. // This may output `gce_instance` type with appropriate labels. func ResourceAttributesToLoggingMonitoredResource(attrs ReadOnlyAttributes) *monitoredrespb.MonitoredResource { cloudPlatform, _ := attrs.GetString(string(semconv.CloudPlatformKey)) switch cloudPlatform { case semconv.CloudPlatformGCPAppEngine.Value.AsString(): return createMonitoredResource(gaeApp, attrs) default: return commonResourceAttributesToMonitoredResource(cloudPlatform, attrs) } } // ResourceAttributesToMonitoringMonitoredResource converts from a set of OTEL resource attributes into a // GCP monitored resource type and label set for Cloud Monitoring // E.g. // This may output `gce_instance` type with appropriate labels. func ResourceAttributesToMonitoringMonitoredResource(attrs ReadOnlyAttributes) *monitoredrespb.MonitoredResource { cloudPlatform, _ := attrs.GetString(string(semconv.CloudPlatformKey)) switch cloudPlatform { case semconv.CloudPlatformGCPAppEngine.Value.AsString(): return createMonitoredResource(gaeInstance, attrs) default: return commonResourceAttributesToMonitoredResource(cloudPlatform, attrs) } } func commonResourceAttributesToMonitoredResource(cloudPlatform string, attrs ReadOnlyAttributes) *monitoredrespb.MonitoredResource { switch cloudPlatform { case semconv.CloudPlatformGCPComputeEngine.Value.AsString(): return createMonitoredResource(gceInstance, attrs) case semconv.CloudPlatformAWSEC2.Value.AsString(): return createMonitoredResource(awsEc2Instance, attrs) // TODO(alex-basinov): replace this string literal with semconv.CloudPlatformGCPBareMetalSolution // once https://github.com/open-telemetry/semantic-conventions/pull/64 makes its way // into the semconv module. case "gcp_bare_metal_solution": return createMonitoredResource(bmsInstance, attrs) default: // if k8s.cluster.name is set, pattern match for various k8s resources. // this will also match non-cloud k8s platforms like minikube. if _, ok := attrs.GetString(string(semconv.K8SClusterNameKey)); ok { // Try for most to least specific k8s_container, k8s_pod, etc if _, ok := attrs.GetString(string(semconv.K8SContainerNameKey)); ok { return createMonitoredResource(k8sContainer, attrs) } else if _, ok := attrs.GetString(string(semconv.K8SPodNameKey)); ok { return createMonitoredResource(k8sPod, attrs) } else if _, ok := attrs.GetString(string(semconv.K8SNodeNameKey)); ok { return createMonitoredResource(k8sNode, attrs) } return createMonitoredResource(k8sCluster, attrs) } // Fallback to generic_task _, hasServiceName := attrs.GetString(string(semconv.ServiceNameKey)) _, hasFaaSName := attrs.GetString(string(semconv.FaaSNameKey)) _, hasServiceInstanceID := attrs.GetString(string(semconv.ServiceInstanceIDKey)) _, hasFaaSInstance := attrs.GetString(string(semconv.FaaSInstanceKey)) if (hasServiceName && hasServiceInstanceID) || (hasFaaSInstance && hasFaaSName) { return createMonitoredResource(genericTask, attrs) } // Everything else fallback to generic_node return createMonitoredResource(genericNode, attrs) } } func createMonitoredResource( monitoredResourceType string, resourceAttrs ReadOnlyAttributes, ) *monitoredrespb.MonitoredResource { mappings := monitoredResourceMappings[monitoredResourceType] mrLabels := make(map[string]string, len(mappings)) for mrKey, mappingConfig := range mappings { mrValue := "" ok := false // Coalesce the possible keys in order for _, otelKey := range mappingConfig.otelKeys { mrValue, ok = resourceAttrs.GetString(otelKey) if mrValue != "" && !strings.HasPrefix(mrValue, unknownServicePrefix) { break } } if mrValue == "" && contains(mappingConfig.otelKeys, string(semconv.ServiceNameKey)) { // the service name started with unknown_service, and was ignored above mrValue, ok = resourceAttrs.GetString(string(semconv.ServiceNameKey)) } if !ok || mrValue == "" { mrValue = mappingConfig.fallbackLiteral } mrLabels[mrKey] = sanitizeUTF8(mrValue) } return &monitoredrespb.MonitoredResource{ Type: monitoredResourceType, Labels: mrLabels, } } func contains(list []string, element string) bool { for _, item := range list { if item == element { return true } } return false } func sanitizeUTF8(s string) string { return strings.ToValidUTF8(s, "�") }