|
| 1 | +# Copyright 2022 Google LLC |
| 2 | +# |
| 3 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +# you may not use this file except in compliance with the License. |
| 5 | +# You may obtain a copy of the License at |
| 6 | +# |
| 7 | +# https://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +# |
| 9 | +# Unless required by applicable law or agreed to in writing, software |
| 10 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +# See the License for the specific language governing permissions and |
| 13 | +# limitations under the License. |
| 14 | + |
| 15 | +import json |
| 16 | +from typing import Dict, Optional, Tuple |
| 17 | + |
| 18 | +from google.api.monitored_resource_pb2 import MonitoredResource |
| 19 | +from opentelemetry.sdk.resources import Attributes, Resource |
| 20 | + |
| 21 | + |
| 22 | +# TODO: use opentelemetry-semantic-conventions package for these constants once it has |
| 23 | +# stabilized. Right now, pinning an unstable version would cause dependency conflicts for |
| 24 | +# users so these are copied in. |
| 25 | +class ResourceAttributes: |
| 26 | + AWS_EC2 = "aws_ec2" |
| 27 | + CLOUD_ACCOUNT_ID = "cloud.account.id" |
| 28 | + CLOUD_AVAILABILITY_ZONE = "cloud.availability_zone" |
| 29 | + CLOUD_PLATFORM_KEY = "cloud.platform" |
| 30 | + CLOUD_PROVIDER = "cloud.provider" |
| 31 | + CLOUD_REGION = "cloud.region" |
| 32 | + GCP_COMPUTE_ENGINE = "gcp_compute_engine" |
| 33 | + GCP_KUBERNETES_ENGINE = "gcp_kubernetes_engine" |
| 34 | + HOST_ID = "host.id" |
| 35 | + HOST_NAME = "host.name" |
| 36 | + K8S_CLUSTER_NAME = "k8s.cluster.name" |
| 37 | + K8S_CONTAINER_NAME = "k8s.container.name" |
| 38 | + K8S_NAMESPACE_NAME = "k8s.namespace.name" |
| 39 | + K8S_NODE_NAME = "k8s.node.name" |
| 40 | + K8S_POD_NAME = "k8s.pod.name" |
| 41 | + SERVICE_INSTANCE_ID = "service.instance.id" |
| 42 | + SERVICE_NAME = "service.name" |
| 43 | + SERVICE_NAMESPACE = "service.namespace" |
| 44 | + |
| 45 | + |
| 46 | +AWS_ACCOUNT = "aws_account" |
| 47 | +AWS_EC2_INSTANCE = "aws_ec2_instance" |
| 48 | +CLUSTER_NAME = "cluster_name" |
| 49 | +CONTAINER_NAME = "container_name" |
| 50 | +GCE_INSTANCE = "gce_instance" |
| 51 | +GENERIC_NODE = "generic_node" |
| 52 | +GENERIC_TASK = "generic_task" |
| 53 | +INSTANCE_ID = "instance_id" |
| 54 | +JOB = "job" |
| 55 | +K8S_CLUSTER = "k8s_cluster" |
| 56 | +K8S_CONTAINER = "k8s_container" |
| 57 | +K8S_NODE = "k8s_node" |
| 58 | +K8S_POD = "k8s_pod" |
| 59 | +LOCATION = "location" |
| 60 | +NAMESPACE = "namespace" |
| 61 | +NAMESPACE_NAME = "namespace_name" |
| 62 | +NODE_ID = "node_id" |
| 63 | +NODE_NAME = "node_name" |
| 64 | +POD_NAME = "pod_name" |
| 65 | +REGION = "region" |
| 66 | +TASK_ID = "task_id" |
| 67 | +ZONE = "zone" |
| 68 | + |
| 69 | + |
| 70 | +class MapConfig: |
| 71 | + otel_keys: Tuple[str, ...] |
| 72 | + """ |
| 73 | + OTel resource keys to try and populate the resource label from. For entries with multiple |
| 74 | + OTel resource keys, the keys' values will be coalesced in order until there is a non-empty |
| 75 | + value. |
| 76 | + """ |
| 77 | + |
| 78 | + fallback: str |
| 79 | + """If none of the otelKeys are present in the Resource, fallback to this literal value""" |
| 80 | + |
| 81 | + def __init__(self, *otel_keys: str, fallback: str = ""): |
| 82 | + self.otel_keys = otel_keys |
| 83 | + self.fallback = fallback |
| 84 | + |
| 85 | + |
| 86 | +# Mappings of GCM resource label keys onto mapping config from OTel resource for a given |
| 87 | +# monitored resource type. Copied from Go impl: |
| 88 | +# https://github.com/GoogleCloudPlatform/opentelemetry-operations-go/blob/v1.8.0/internal/resourcemapping/resourcemapping.go#L51 |
| 89 | +MAPPINGS = { |
| 90 | + GCE_INSTANCE: { |
| 91 | + ZONE: MapConfig(ResourceAttributes.CLOUD_AVAILABILITY_ZONE), |
| 92 | + INSTANCE_ID: MapConfig(ResourceAttributes.HOST_ID), |
| 93 | + }, |
| 94 | + K8S_CONTAINER: { |
| 95 | + LOCATION: MapConfig( |
| 96 | + ResourceAttributes.CLOUD_AVAILABILITY_ZONE, |
| 97 | + ResourceAttributes.CLOUD_REGION, |
| 98 | + ), |
| 99 | + CLUSTER_NAME: MapConfig(ResourceAttributes.K8S_CLUSTER_NAME), |
| 100 | + NAMESPACE_NAME: MapConfig(ResourceAttributes.K8S_NAMESPACE_NAME), |
| 101 | + POD_NAME: MapConfig(ResourceAttributes.K8S_POD_NAME), |
| 102 | + CONTAINER_NAME: MapConfig(ResourceAttributes.K8S_CONTAINER_NAME), |
| 103 | + }, |
| 104 | + K8S_POD: { |
| 105 | + LOCATION: MapConfig( |
| 106 | + ResourceAttributes.CLOUD_AVAILABILITY_ZONE, |
| 107 | + ResourceAttributes.CLOUD_REGION, |
| 108 | + ), |
| 109 | + CLUSTER_NAME: MapConfig(ResourceAttributes.K8S_CLUSTER_NAME), |
| 110 | + NAMESPACE_NAME: MapConfig(ResourceAttributes.K8S_NAMESPACE_NAME), |
| 111 | + POD_NAME: MapConfig(ResourceAttributes.K8S_POD_NAME), |
| 112 | + }, |
| 113 | + K8S_NODE: { |
| 114 | + LOCATION: MapConfig( |
| 115 | + ResourceAttributes.CLOUD_AVAILABILITY_ZONE, |
| 116 | + ResourceAttributes.CLOUD_REGION, |
| 117 | + ), |
| 118 | + CLUSTER_NAME: MapConfig(ResourceAttributes.K8S_CLUSTER_NAME), |
| 119 | + NODE_NAME: MapConfig(ResourceAttributes.K8S_NODE_NAME), |
| 120 | + }, |
| 121 | + K8S_CLUSTER: { |
| 122 | + LOCATION: MapConfig( |
| 123 | + ResourceAttributes.CLOUD_AVAILABILITY_ZONE, |
| 124 | + ResourceAttributes.CLOUD_REGION, |
| 125 | + ), |
| 126 | + CLUSTER_NAME: MapConfig(ResourceAttributes.K8S_CLUSTER_NAME), |
| 127 | + }, |
| 128 | + AWS_EC2_INSTANCE: { |
| 129 | + INSTANCE_ID: MapConfig(ResourceAttributes.HOST_ID), |
| 130 | + REGION: MapConfig( |
| 131 | + ResourceAttributes.CLOUD_AVAILABILITY_ZONE, |
| 132 | + ResourceAttributes.CLOUD_REGION, |
| 133 | + ), |
| 134 | + AWS_ACCOUNT: MapConfig(ResourceAttributes.CLOUD_ACCOUNT_ID), |
| 135 | + }, |
| 136 | + GENERIC_TASK: { |
| 137 | + LOCATION: MapConfig( |
| 138 | + ResourceAttributes.CLOUD_AVAILABILITY_ZONE, |
| 139 | + ResourceAttributes.CLOUD_REGION, |
| 140 | + fallback="global", |
| 141 | + ), |
| 142 | + NAMESPACE: MapConfig(ResourceAttributes.SERVICE_NAMESPACE), |
| 143 | + JOB: MapConfig(ResourceAttributes.SERVICE_NAME), |
| 144 | + TASK_ID: MapConfig(ResourceAttributes.SERVICE_INSTANCE_ID), |
| 145 | + }, |
| 146 | + GENERIC_NODE: { |
| 147 | + LOCATION: MapConfig( |
| 148 | + ResourceAttributes.CLOUD_AVAILABILITY_ZONE, |
| 149 | + ResourceAttributes.CLOUD_REGION, |
| 150 | + fallback="global", |
| 151 | + ), |
| 152 | + NAMESPACE: MapConfig(ResourceAttributes.SERVICE_NAMESPACE), |
| 153 | + NODE_ID: MapConfig( |
| 154 | + ResourceAttributes.HOST_ID, ResourceAttributes.HOST_NAME |
| 155 | + ), |
| 156 | + }, |
| 157 | +} |
| 158 | + |
| 159 | + |
| 160 | +def get_monitored_resource( |
| 161 | + resource: Resource, |
| 162 | +) -> Optional[MonitoredResource]: |
| 163 | + """Add Google resource specific information (e.g. instance id, region). |
| 164 | +
|
| 165 | + See |
| 166 | + https://cloud.google.com/monitoring/custom-metrics/creating-metrics#custom-metric-resources |
| 167 | + for supported types |
| 168 | + Args: |
| 169 | + resource: OTel resource |
| 170 | + """ |
| 171 | + |
| 172 | + attrs = resource.attributes |
| 173 | + |
| 174 | + platform = attrs.get(ResourceAttributes.CLOUD_PLATFORM_KEY) |
| 175 | + if platform == ResourceAttributes.GCP_COMPUTE_ENGINE: |
| 176 | + mr = _create_monitored_resource(GCE_INSTANCE, attrs) |
| 177 | + elif platform == ResourceAttributes.GCP_KUBERNETES_ENGINE: |
| 178 | + if ResourceAttributes.K8S_CONTAINER_NAME in attrs: |
| 179 | + mr = _create_monitored_resource(K8S_CONTAINER, attrs) |
| 180 | + elif ResourceAttributes.K8S_POD_NAME in attrs: |
| 181 | + mr = _create_monitored_resource(K8S_POD, attrs) |
| 182 | + elif ResourceAttributes.K8S_NODE_NAME in attrs: |
| 183 | + mr = _create_monitored_resource(K8S_NODE, attrs) |
| 184 | + else: |
| 185 | + mr = _create_monitored_resource(K8S_CLUSTER, attrs) |
| 186 | + elif platform == ResourceAttributes.AWS_EC2: |
| 187 | + mr = _create_monitored_resource(AWS_EC2_INSTANCE, attrs) |
| 188 | + else: |
| 189 | + # fallback to generic_task |
| 190 | + if ( |
| 191 | + ResourceAttributes.SERVICE_NAME in attrs |
| 192 | + and ResourceAttributes.SERVICE_INSTANCE_ID in attrs |
| 193 | + ): |
| 194 | + mr = _create_monitored_resource(GENERIC_TASK, attrs) |
| 195 | + else: |
| 196 | + mr = _create_monitored_resource(GENERIC_NODE, attrs) |
| 197 | + |
| 198 | + return mr |
| 199 | + |
| 200 | + |
| 201 | +def _create_monitored_resource( |
| 202 | + monitored_resource_type: str, resource_attrs: Attributes |
| 203 | +) -> MonitoredResource: |
| 204 | + mapping = MAPPINGS[monitored_resource_type] |
| 205 | + labels: Dict[str, str] = {} |
| 206 | + |
| 207 | + for mr_key, map_config in mapping.items(): |
| 208 | + mr_value = None |
| 209 | + for otel_key in map_config.otel_keys: |
| 210 | + if otel_key in resource_attrs: |
| 211 | + mr_value = resource_attrs[otel_key] |
| 212 | + break |
| 213 | + |
| 214 | + if mr_value is None: |
| 215 | + mr_value = map_config.fallback |
| 216 | + |
| 217 | + # OTel attribute values can be any of str, bool, int, float, or Sequence of any of |
| 218 | + # them. Encode any non-strings as json string |
| 219 | + if not isinstance(mr_value, str): |
| 220 | + mr_value = json.dumps( |
| 221 | + mr_value, sort_keys=True, indent=None, separators=(",", ":") |
| 222 | + ) |
| 223 | + labels[mr_key] = mr_value |
| 224 | + |
| 225 | + return MonitoredResource(type=monitored_resource_type, labels=labels) |
0 commit comments