diff --git a/e2e-test-server/Dockerfile b/e2e-test-server/Dockerfile index 6a22b2df..a7d69724 100644 --- a/e2e-test-server/Dockerfile +++ b/e2e-test-server/Dockerfile @@ -28,6 +28,7 @@ FROM python-base as build-base # copy local dependencies COPY opentelemetry-exporter-gcp-trace opentelemetry-exporter-gcp-trace COPY opentelemetry-propagator-gcp opentelemetry-propagator-gcp +COPY opentelemetry-resourcedetector-gcp opentelemetry-resourcedetector-gcp WORKDIR $SRC/e2e-test-server # copy requirements/constraints COPY e2e-test-server/requirements.txt e2e-test-server/constraints.txt ./ diff --git a/opentelemetry-exporter-gcp-monitoring/setup.cfg b/opentelemetry-exporter-gcp-monitoring/setup.cfg index 439c3b80..38c03266 100644 --- a/opentelemetry-exporter-gcp-monitoring/setup.cfg +++ b/opentelemetry-exporter-gcp-monitoring/setup.cfg @@ -28,6 +28,7 @@ install_requires = google-cloud-monitoring ~= 2.0 opentelemetry-api ~= 1.0 opentelemetry-sdk ~= 1.0 + opentelemetry-resourcedetector-gcp >= 1.5.0dev0, == 1.* [options.packages.find] where = src diff --git a/opentelemetry-exporter-gcp-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py b/opentelemetry-exporter-gcp-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py index b764b985..445d9ce5 100644 --- a/opentelemetry-exporter-gcp-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py +++ b/opentelemetry-exporter-gcp-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py @@ -24,6 +24,7 @@ from google.api.label_pb2 import LabelDescriptor from google.api.metric_pb2 import Metric as GMetric from google.api.metric_pb2 import MetricDescriptor +from google.api.monitored_resource_pb2 import MonitoredResource from google.cloud.monitoring_v3 import ( CreateMetricDescriptorRequest, CreateTimeSeriesRequest, @@ -39,10 +40,10 @@ # pylint: disable=no-name-in-module from google.protobuf.timestamp_pb2 import Timestamp -from opentelemetry.exporter.cloud_monitoring._resource import ( +from opentelemetry.exporter.cloud_monitoring.version import __version__ +from opentelemetry.resourcedetector.gcp_resource_detector._mapping import ( get_monitored_resource, ) -from opentelemetry.exporter.cloud_monitoring.version import __version__ from opentelemetry.sdk.metrics.export import ( Gauge, Histogram, @@ -296,9 +297,17 @@ def export( all_series = [] for resource_metric in metrics_data.resource_metrics: - monitored_resource = get_monitored_resource( + monitored_resource_data = get_monitored_resource( resource_metric.resource ) + # convert it to proto + monitored_resource = None + if monitored_resource_data: + monitored_resource = MonitoredResource( + type=monitored_resource_data.type, + labels=monitored_resource_data.labels, + ) + for scope_metric in resource_metric.scope_metrics: for metric in scope_metric.metrics: # Convert all data_points to Sequences, see diff --git a/opentelemetry-exporter-gcp-trace/setup.cfg b/opentelemetry-exporter-gcp-trace/setup.cfg index 9e40f6e7..1b07f799 100644 --- a/opentelemetry-exporter-gcp-trace/setup.cfg +++ b/opentelemetry-exporter-gcp-trace/setup.cfg @@ -28,6 +28,7 @@ install_requires = google-cloud-trace ~= 1.1 opentelemetry-api ~= 1.0 opentelemetry-sdk ~= 1.0 + opentelemetry-resourcedetector-gcp >= 1.5.0dev0, == 1.* [options.packages.find] where = src diff --git a/opentelemetry-exporter-gcp-trace/src/opentelemetry/exporter/cloud_trace/__init__.py b/opentelemetry-exporter-gcp-trace/src/opentelemetry/exporter/cloud_trace/__init__.py index df830a0b..dd2353cf 100644 --- a/opentelemetry-exporter-gcp-trace/src/opentelemetry/exporter/cloud_trace/__init__.py +++ b/opentelemetry-exporter-gcp-trace/src/opentelemetry/exporter/cloud_trace/__init__.py @@ -102,6 +102,9 @@ OTEL_EXPORTER_GCP_TRACE_RESOURCE_REGEX, ) from opentelemetry.exporter.cloud_trace.version import __version__ +from opentelemetry.resourcedetector.gcp_resource_detector._mapping import ( + get_monitored_resource, +) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import Event from opentelemetry.sdk.trace.export import ( @@ -420,24 +423,6 @@ def _strip_characters(ot_version): return "".join(filter(lambda x: x.isdigit() or x == ".", ot_version)) -OT_RESOURCE_ATTRIBUTE_TO_GCP = { - "gce_instance": { - "host.id": "instance_id", - "cloud.account.id": "project_id", - "cloud.zone": "zone", - }, - "gke_container": { - "k8s.cluster.name": "cluster_name", - "k8s.namespace.name": "namespace_id", - "k8s.pod.name": "pod_id", - "host.id": "instance_id", - "container.name": "container_name", - "cloud.account.id": "project_id", - "cloud.zone": "zone", - }, -} - - def _extract_resources( resource: Resource, resource_regex: Optional[Pattern] = None ) -> Dict[str, str]: @@ -451,24 +436,14 @@ def _extract_resources( if resource_regex.match(k) } ) - if resource_attributes.get("cloud.provider") != "gcp": - return extracted_attributes - resource_type = resource_attributes["gcp.resource_type"] - if ( - not isinstance(resource_type, str) - or resource_type not in OT_RESOURCE_ATTRIBUTE_TO_GCP - ): - return extracted_attributes - extracted_attributes.update( - { - "g.co/r/{}/{}".format(resource_type, gcp_resource_key): str( - resource_attributes[ot_resource_key] - ) - for ot_resource_key, gcp_resource_key in OT_RESOURCE_ATTRIBUTE_TO_GCP[ - resource_type - ].items() - } - ) + monitored_resource = get_monitored_resource(resource) + if monitored_resource: + extracted_attributes.update( + { + "g.co/r/{}/{}".format(monitored_resource.type, k): v + for k, v in monitored_resource.labels.items() + } + ) return extracted_attributes diff --git a/opentelemetry-exporter-gcp-trace/tests/test_cloud_trace_exporter.py b/opentelemetry-exporter-gcp-trace/tests/test_cloud_trace_exporter.py index 818129ee..52e6eb5c 100644 --- a/opentelemetry-exporter-gcp-trace/tests/test_cloud_trace_exporter.py +++ b/opentelemetry-exporter-gcp-trace/tests/test_cloud_trace_exporter.py @@ -124,11 +124,14 @@ def test_constructor_explicit(self): def test_export(self): resource_info = Resource( { - "cloud.account.id": 123, - "host.id": "host", - "cloud.zone": "US", + "cloud.account.id": "123", + "cloud.platform": "gcp_compute_engine", "cloud.provider": "gcp", - "gcp.resource_type": "gce_instance", + "cloud.region": "us-east4", + "cloud.availability_zone": "us-east4-b", + "host.id": "host", + "host.name": "fakeName", + "host.type": "fakeMachineType", } ) span_datas = [ @@ -157,13 +160,12 @@ def test_export(self): ), "attributes": ProtoSpan.Attributes( attribute_map={ - "g.co/r/gce_instance/zone": _format_attribute_value("US"), + "g.co/r/gce_instance/zone": _format_attribute_value( + "us-east4-b" + ), "g.co/r/gce_instance/instance_id": _format_attribute_value( "host" ), - "g.co/r/gce_instance/project_id": _format_attribute_value( - "123" - ), "g.co/agent": self.agent_code, "attr_key": _format_attribute_value("attr_value"), } @@ -652,27 +654,34 @@ def test_too_many_link_attributes(self): ) def test_extract_empty_resources(self): - self.assertEqual(_extract_resources(Resource.get_empty()), {}) + self.assertEqual( + _extract_resources(Resource.get_empty()), + { + "g.co/r/generic_node/location": "global", + "g.co/r/generic_node/namespace": "", + "g.co/r/generic_node/node_id": "", + }, + ) def test_extract_resource_attributes_with_regex(self): resource_regex = re.compile(r"service\..*") resource = Resource( attributes={ - "cloud.account.id": 123, - "host.id": "host", - "cloud.zone": "US", + "cloud.account.id": "123", + "cloud.availability_zone": "us-east4-b", + "cloud.platform": "gcp_compute_engine", "cloud.provider": "gcp", - "extra_info": "extra", - "gcp.resource_type": "gce_instance", - "not_gcp_resource": "value", + "cloud.region": "us-east4", + "host.id": "host", + "host.name": "fakeName", + "host.type": "fakeMachineType", "service.name": "my-app", "service.version": "1", } ) expected_extract = { - "g.co/r/gce_instance/project_id": "123", "g.co/r/gce_instance/instance_id": "host", - "g.co/r/gce_instance/zone": "US", + "g.co/r/gce_instance/zone": "us-east4-b", "service.name": "my-app", "service.version": "1", } @@ -684,19 +693,19 @@ def test_non_matching_regex(self): resource_regex = re.compile(r"this-regex-matches-nothing") resource = Resource( attributes={ - "cloud.account.id": 123, - "host.id": "host", - "cloud.zone": "US", + "cloud.account.id": "123", + "cloud.availability_zone": "us-east4-b", + "cloud.platform": "gcp_compute_engine", "cloud.provider": "gcp", - "extra_info": "extra", - "gcp.resource_type": "gce_instance", - "not_gcp_resource": "value", + "cloud.region": "us-east4", + "host.id": "host", + "host.name": "fakeName", + "host.type": "fakeMachineType", } ) expected_extract = { - "g.co/r/gce_instance/project_id": "123", "g.co/r/gce_instance/instance_id": "host", - "g.co/r/gce_instance/zone": "US", + "g.co/r/gce_instance/zone": "us-east4-b", } self.assertEqual( _extract_resources(resource, resource_regex), expected_extract @@ -705,34 +714,22 @@ def test_non_matching_regex(self): def test_extract_well_formed_resources(self): resource = Resource( attributes={ - "cloud.account.id": 123, - "host.id": "host", - "cloud.zone": "US", + "cloud.account.id": "123", + "cloud.availability_zone": "us-east4-b", + "cloud.platform": "gcp_compute_engine", "cloud.provider": "gcp", - "extra_info": "extra", - "gcp.resource_type": "gce_instance", - "not_gcp_resource": "value", + "cloud.region": "us-east4", + "host.id": "host", + "host.name": "fakeName", + "host.type": "fakeMachineType", } ) expected_extract = { - "g.co/r/gce_instance/project_id": "123", "g.co/r/gce_instance/instance_id": "host", - "g.co/r/gce_instance/zone": "US", + "g.co/r/gce_instance/zone": "us-east4-b", } self.assertEqual(_extract_resources(resource), expected_extract) - def test_extract_malformed_resources(self): - # This resource doesn't have all the fields required for a gce_instance - # Specifically its missing "host.id", "cloud.zone", "cloud.account.id" - resource = Resource( - attributes={ - "gcp.resource_type": "gce_instance", - "cloud.provider": "gcp", - } - ) - # Should throw when passed a malformed GCP resource dict - self.assertRaises(KeyError, _extract_resources, resource) - def test_extract_unsupported_gcp_resources(self): # Unsupported gcp resources will be ignored resource = Resource( @@ -741,24 +738,18 @@ def test_extract_unsupported_gcp_resources(self): "host.id": "host", "extra_info": "extra", "not_gcp_resource": "value", - "gcp.resource_type": "unsupported_gcp_resource", + "cloud.platform": "gcp_some_unsupported_thing", "cloud.provider": "gcp", } ) - self.assertEqual(_extract_resources(resource), {}) - - def test_extract_unsupported_provider_resources(self): - # Resources with currently unsupported providers will be ignored - resource = Resource( - attributes={ - "cloud.account.id": "123", - "host.id": "host", - "extra_info": "extra", - "not_gcp_resource": "value", - "cloud.provider": "aws", - } + self.assertEqual( + _extract_resources(resource), + { + "g.co/r/generic_node/location": "global", + "g.co/r/generic_node/namespace": "", + "g.co/r/generic_node/node_id": "host", + }, ) - self.assertEqual(_extract_resources(resource), {}) def test_truncate_string(self): """Cloud Trace API imposes limits on the length of many things, diff --git a/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_constants.py b/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_constants.py new file mode 100644 index 00000000..73ed4fd6 --- /dev/null +++ b/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_constants.py @@ -0,0 +1,61 @@ +# Copyright 2023 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. + +# TODO: use opentelemetry-semantic-conventions package for these constants once it has +# stabilized. Right now, pinning an unstable version would cause dependency conflicts for +# users so these are copied in. +class ResourceAttributes: + AWS_EC2 = "aws_ec2" + CLOUD_ACCOUNT_ID = "cloud.account.id" + CLOUD_AVAILABILITY_ZONE = "cloud.availability_zone" + CLOUD_PLATFORM_KEY = "cloud.platform" + CLOUD_PROVIDER = "cloud.provider" + CLOUD_REGION = "cloud.region" + GCP_COMPUTE_ENGINE = "gcp_compute_engine" + GCP_KUBERNETES_ENGINE = "gcp_kubernetes_engine" + HOST_ID = "host.id" + HOST_NAME = "host.name" + HOST_TYPE = "host.type" + K8S_CLUSTER_NAME = "k8s.cluster.name" + K8S_CONTAINER_NAME = "k8s.container.name" + K8S_NAMESPACE_NAME = "k8s.namespace.name" + K8S_NODE_NAME = "k8s.node.name" + K8S_POD_NAME = "k8s.pod.name" + SERVICE_INSTANCE_ID = "service.instance.id" + SERVICE_NAME = "service.name" + SERVICE_NAMESPACE = "service.namespace" + + +AWS_ACCOUNT = "aws_account" +AWS_EC2_INSTANCE = "aws_ec2_instance" +CLUSTER_NAME = "cluster_name" +CONTAINER_NAME = "container_name" +GCE_INSTANCE = "gce_instance" +GENERIC_NODE = "generic_node" +GENERIC_TASK = "generic_task" +INSTANCE_ID = "instance_id" +JOB = "job" +K8S_CLUSTER = "k8s_cluster" +K8S_CONTAINER = "k8s_container" +K8S_NODE = "k8s_node" +K8S_POD = "k8s_pod" +LOCATION = "location" +NAMESPACE = "namespace" +NAMESPACE_NAME = "namespace_name" +NODE_ID = "node_id" +NODE_NAME = "node_name" +POD_NAME = "pod_name" +REGION = "region" +TASK_ID = "task_id" +ZONE = "zone" diff --git a/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_detector.py b/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_detector.py index 6bfba291..94256bbd 100644 --- a/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_detector.py +++ b/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_detector.py @@ -19,35 +19,13 @@ _gke, _metadata, ) +from opentelemetry.resourcedetector.gcp_resource_detector._constants import ( + ResourceAttributes, +) from opentelemetry.sdk.resources import Resource, ResourceDetector from opentelemetry.util.types import AttributeValue -# TODO: use opentelemetry-semantic-conventions package for these constants once it has -# stabilized. Right now, pinning an unstable version would cause dependency conflicts for -# users so these are copied in. -class ResourceAttributes: - AWS_EC2 = "aws_ec2" - CLOUD_ACCOUNT_ID = "cloud.account.id" - CLOUD_AVAILABILITY_ZONE = "cloud.availability_zone" - CLOUD_PLATFORM_KEY = "cloud.platform" - CLOUD_PROVIDER = "cloud.provider" - CLOUD_REGION = "cloud.region" - GCP_COMPUTE_ENGINE = "gcp_compute_engine" - GCP_KUBERNETES_ENGINE = "gcp_kubernetes_engine" - HOST_ID = "host.id" - HOST_NAME = "host.name" - HOST_TYPE = "host.type" - K8S_CLUSTER_NAME = "k8s.cluster.name" - K8S_CONTAINER_NAME = "k8s.container.name" - K8S_NAMESPACE_NAME = "k8s.namespace.name" - K8S_NODE_NAME = "k8s.node.name" - K8S_POD_NAME = "k8s.pod.name" - SERVICE_INSTANCE_ID = "service.instance.id" - SERVICE_NAME = "service.name" - SERVICE_NAMESPACE = "service.namespace" - - class GoogleCloudResourceDetector(ResourceDetector): def detect(self) -> Resource: if not _metadata.is_available(): diff --git a/opentelemetry-exporter-gcp-monitoring/src/opentelemetry/exporter/cloud_monitoring/_resource.py b/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_mapping.py similarity index 52% rename from opentelemetry-exporter-gcp-monitoring/src/opentelemetry/exporter/cloud_monitoring/_resource.py rename to opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_mapping.py index 35acf8a6..1c22e66c 100644 --- a/opentelemetry-exporter-gcp-monitoring/src/opentelemetry/exporter/cloud_monitoring/_resource.py +++ b/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_mapping.py @@ -1,4 +1,4 @@ -# Copyright 2022 Google LLC +# Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,60 +13,16 @@ # limitations under the License. import json -from typing import Dict, Optional, Tuple +from dataclasses import dataclass +from typing import Dict, Mapping, Optional, Tuple -from google.api.monitored_resource_pb2 import MonitoredResource +from opentelemetry.resourcedetector.gcp_resource_detector import _constants +from opentelemetry.resourcedetector.gcp_resource_detector._constants import ( + ResourceAttributes, +) from opentelemetry.sdk.resources import Attributes, Resource -# TODO: use opentelemetry-semantic-conventions package for these constants once it has -# stabilized. Right now, pinning an unstable version would cause dependency conflicts for -# users so these are copied in. -class ResourceAttributes: - AWS_EC2 = "aws_ec2" - CLOUD_ACCOUNT_ID = "cloud.account.id" - CLOUD_AVAILABILITY_ZONE = "cloud.availability_zone" - CLOUD_PLATFORM_KEY = "cloud.platform" - CLOUD_PROVIDER = "cloud.provider" - CLOUD_REGION = "cloud.region" - GCP_COMPUTE_ENGINE = "gcp_compute_engine" - GCP_KUBERNETES_ENGINE = "gcp_kubernetes_engine" - HOST_ID = "host.id" - HOST_NAME = "host.name" - K8S_CLUSTER_NAME = "k8s.cluster.name" - K8S_CONTAINER_NAME = "k8s.container.name" - K8S_NAMESPACE_NAME = "k8s.namespace.name" - K8S_NODE_NAME = "k8s.node.name" - K8S_POD_NAME = "k8s.pod.name" - SERVICE_INSTANCE_ID = "service.instance.id" - SERVICE_NAME = "service.name" - SERVICE_NAMESPACE = "service.namespace" - - -AWS_ACCOUNT = "aws_account" -AWS_EC2_INSTANCE = "aws_ec2_instance" -CLUSTER_NAME = "cluster_name" -CONTAINER_NAME = "container_name" -GCE_INSTANCE = "gce_instance" -GENERIC_NODE = "generic_node" -GENERIC_TASK = "generic_task" -INSTANCE_ID = "instance_id" -JOB = "job" -K8S_CLUSTER = "k8s_cluster" -K8S_CONTAINER = "k8s_container" -K8S_NODE = "k8s_node" -K8S_POD = "k8s_pod" -LOCATION = "location" -NAMESPACE = "namespace" -NAMESPACE_NAME = "namespace_name" -NODE_ID = "node_id" -NODE_NAME = "node_name" -POD_NAME = "pod_name" -REGION = "region" -TASK_ID = "task_id" -ZONE = "zone" - - class MapConfig: otel_keys: Tuple[str, ...] """ @@ -87,79 +43,102 @@ def __init__(self, *otel_keys: str, fallback: str = ""): # monitored resource type. Copied from Go impl: # https://github.com/GoogleCloudPlatform/opentelemetry-operations-go/blob/v1.8.0/internal/resourcemapping/resourcemapping.go#L51 MAPPINGS = { - GCE_INSTANCE: { - ZONE: MapConfig(ResourceAttributes.CLOUD_AVAILABILITY_ZONE), - INSTANCE_ID: MapConfig(ResourceAttributes.HOST_ID), + _constants.GCE_INSTANCE: { + _constants.ZONE: MapConfig(ResourceAttributes.CLOUD_AVAILABILITY_ZONE), + _constants.INSTANCE_ID: MapConfig(ResourceAttributes.HOST_ID), }, - K8S_CONTAINER: { - LOCATION: MapConfig( + _constants.K8S_CONTAINER: { + _constants.LOCATION: MapConfig( ResourceAttributes.CLOUD_AVAILABILITY_ZONE, ResourceAttributes.CLOUD_REGION, ), - CLUSTER_NAME: MapConfig(ResourceAttributes.K8S_CLUSTER_NAME), - NAMESPACE_NAME: MapConfig(ResourceAttributes.K8S_NAMESPACE_NAME), - POD_NAME: MapConfig(ResourceAttributes.K8S_POD_NAME), - CONTAINER_NAME: MapConfig(ResourceAttributes.K8S_CONTAINER_NAME), + _constants.CLUSTER_NAME: MapConfig( + ResourceAttributes.K8S_CLUSTER_NAME + ), + _constants.NAMESPACE_NAME: MapConfig( + ResourceAttributes.K8S_NAMESPACE_NAME + ), + _constants.POD_NAME: MapConfig(ResourceAttributes.K8S_POD_NAME), + _constants.CONTAINER_NAME: MapConfig( + ResourceAttributes.K8S_CONTAINER_NAME + ), }, - K8S_POD: { - LOCATION: MapConfig( + _constants.K8S_POD: { + _constants.LOCATION: MapConfig( ResourceAttributes.CLOUD_AVAILABILITY_ZONE, ResourceAttributes.CLOUD_REGION, ), - CLUSTER_NAME: MapConfig(ResourceAttributes.K8S_CLUSTER_NAME), - NAMESPACE_NAME: MapConfig(ResourceAttributes.K8S_NAMESPACE_NAME), - POD_NAME: MapConfig(ResourceAttributes.K8S_POD_NAME), + _constants.CLUSTER_NAME: MapConfig( + ResourceAttributes.K8S_CLUSTER_NAME + ), + _constants.NAMESPACE_NAME: MapConfig( + ResourceAttributes.K8S_NAMESPACE_NAME + ), + _constants.POD_NAME: MapConfig(ResourceAttributes.K8S_POD_NAME), }, - K8S_NODE: { - LOCATION: MapConfig( + _constants.K8S_NODE: { + _constants.LOCATION: MapConfig( ResourceAttributes.CLOUD_AVAILABILITY_ZONE, ResourceAttributes.CLOUD_REGION, ), - CLUSTER_NAME: MapConfig(ResourceAttributes.K8S_CLUSTER_NAME), - NODE_NAME: MapConfig(ResourceAttributes.K8S_NODE_NAME), + _constants.CLUSTER_NAME: MapConfig( + ResourceAttributes.K8S_CLUSTER_NAME + ), + _constants.NODE_NAME: MapConfig(ResourceAttributes.K8S_NODE_NAME), }, - K8S_CLUSTER: { - LOCATION: MapConfig( + _constants.K8S_CLUSTER: { + _constants.LOCATION: MapConfig( ResourceAttributes.CLOUD_AVAILABILITY_ZONE, ResourceAttributes.CLOUD_REGION, ), - CLUSTER_NAME: MapConfig(ResourceAttributes.K8S_CLUSTER_NAME), + _constants.CLUSTER_NAME: MapConfig( + ResourceAttributes.K8S_CLUSTER_NAME + ), }, - AWS_EC2_INSTANCE: { - INSTANCE_ID: MapConfig(ResourceAttributes.HOST_ID), - REGION: MapConfig( + _constants.AWS_EC2_INSTANCE: { + _constants.INSTANCE_ID: MapConfig(ResourceAttributes.HOST_ID), + _constants.REGION: MapConfig( ResourceAttributes.CLOUD_AVAILABILITY_ZONE, ResourceAttributes.CLOUD_REGION, ), - AWS_ACCOUNT: MapConfig(ResourceAttributes.CLOUD_ACCOUNT_ID), + _constants.AWS_ACCOUNT: MapConfig(ResourceAttributes.CLOUD_ACCOUNT_ID), }, - GENERIC_TASK: { - LOCATION: MapConfig( + _constants.GENERIC_TASK: { + _constants.LOCATION: MapConfig( ResourceAttributes.CLOUD_AVAILABILITY_ZONE, ResourceAttributes.CLOUD_REGION, fallback="global", ), - NAMESPACE: MapConfig(ResourceAttributes.SERVICE_NAMESPACE), - JOB: MapConfig(ResourceAttributes.SERVICE_NAME), - TASK_ID: MapConfig(ResourceAttributes.SERVICE_INSTANCE_ID), + _constants.NAMESPACE: MapConfig(ResourceAttributes.SERVICE_NAMESPACE), + _constants.JOB: MapConfig(ResourceAttributes.SERVICE_NAME), + _constants.TASK_ID: MapConfig(ResourceAttributes.SERVICE_INSTANCE_ID), }, - GENERIC_NODE: { - LOCATION: MapConfig( + _constants.GENERIC_NODE: { + _constants.LOCATION: MapConfig( ResourceAttributes.CLOUD_AVAILABILITY_ZONE, ResourceAttributes.CLOUD_REGION, fallback="global", ), - NAMESPACE: MapConfig(ResourceAttributes.SERVICE_NAMESPACE), - NODE_ID: MapConfig( + _constants.NAMESPACE: MapConfig(ResourceAttributes.SERVICE_NAMESPACE), + _constants.NODE_ID: MapConfig( ResourceAttributes.HOST_ID, ResourceAttributes.HOST_NAME ), }, } +@dataclass +class MonitoredResourceData: + """Dataclass representing a protobuf monitored resource. Make sure to convert to a protobuf + if needed.""" + + type: str + labels: Mapping[str, str] + + def get_monitored_resource( resource: Resource, -) -> Optional[MonitoredResource]: +) -> Optional[MonitoredResourceData]: """Add Google resource specific information (e.g. instance id, region). See @@ -173,34 +152,34 @@ def get_monitored_resource( platform = attrs.get(ResourceAttributes.CLOUD_PLATFORM_KEY) if platform == ResourceAttributes.GCP_COMPUTE_ENGINE: - mr = _create_monitored_resource(GCE_INSTANCE, attrs) + mr = _create_monitored_resource(_constants.GCE_INSTANCE, attrs) elif platform == ResourceAttributes.GCP_KUBERNETES_ENGINE: if ResourceAttributes.K8S_CONTAINER_NAME in attrs: - mr = _create_monitored_resource(K8S_CONTAINER, attrs) + mr = _create_monitored_resource(_constants.K8S_CONTAINER, attrs) elif ResourceAttributes.K8S_POD_NAME in attrs: - mr = _create_monitored_resource(K8S_POD, attrs) + mr = _create_monitored_resource(_constants.K8S_POD, attrs) elif ResourceAttributes.K8S_NODE_NAME in attrs: - mr = _create_monitored_resource(K8S_NODE, attrs) + mr = _create_monitored_resource(_constants.K8S_NODE, attrs) else: - mr = _create_monitored_resource(K8S_CLUSTER, attrs) + mr = _create_monitored_resource(_constants.K8S_CLUSTER, attrs) elif platform == ResourceAttributes.AWS_EC2: - mr = _create_monitored_resource(AWS_EC2_INSTANCE, attrs) + mr = _create_monitored_resource(_constants.AWS_EC2_INSTANCE, attrs) else: # fallback to generic_task if ( ResourceAttributes.SERVICE_NAME in attrs and ResourceAttributes.SERVICE_INSTANCE_ID in attrs ): - mr = _create_monitored_resource(GENERIC_TASK, attrs) + mr = _create_monitored_resource(_constants.GENERIC_TASK, attrs) else: - mr = _create_monitored_resource(GENERIC_NODE, attrs) + mr = _create_monitored_resource(_constants.GENERIC_NODE, attrs) return mr def _create_monitored_resource( monitored_resource_type: str, resource_attrs: Attributes -) -> MonitoredResource: +) -> MonitoredResourceData: mapping = MAPPINGS[monitored_resource_type] labels: Dict[str, str] = {} @@ -222,4 +201,4 @@ def _create_monitored_resource( ) labels[mr_key] = mr_value - return MonitoredResource(type=monitored_resource_type, labels=labels) + return MonitoredResourceData(type=monitored_resource_type, labels=labels) diff --git a/opentelemetry-resourcedetector-gcp/tests/__snapshots__/test_detector.ambr b/opentelemetry-resourcedetector-gcp/tests/__snapshots__/test_detector.ambr index 10fa584f..c3cc24f7 100644 --- a/opentelemetry-resourcedetector-gcp/tests/__snapshots__/test_detector.ambr +++ b/opentelemetry-resourcedetector-gcp/tests/__snapshots__/test_detector.ambr @@ -19,7 +19,7 @@ 'service.name': 'unknown_service', 'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', - 'telemetry.sdk.version': '1.15.0', + 'telemetry.sdk.version': '1.16.0', }) # --- # name: test_detects_gke[regional] @@ -33,7 +33,7 @@ 'service.name': 'unknown_service', 'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', - 'telemetry.sdk.version': '1.15.0', + 'telemetry.sdk.version': '1.16.0', }) # --- # name: test_detects_gke[zonal] @@ -47,6 +47,6 @@ 'service.name': 'unknown_service', 'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', - 'telemetry.sdk.version': '1.15.0', + 'telemetry.sdk.version': '1.16.0', }) # --- diff --git a/opentelemetry-exporter-gcp-monitoring/tests/__snapshots__/test_resource.ambr b/opentelemetry-resourcedetector-gcp/tests/__snapshots__/test_mapping.ambr similarity index 100% rename from opentelemetry-exporter-gcp-monitoring/tests/__snapshots__/test_resource.ambr rename to opentelemetry-resourcedetector-gcp/tests/__snapshots__/test_mapping.ambr diff --git a/opentelemetry-exporter-gcp-monitoring/tests/test_resource.py b/opentelemetry-resourcedetector-gcp/tests/test_mapping.py similarity index 94% rename from opentelemetry-exporter-gcp-monitoring/tests/test_resource.py rename to opentelemetry-resourcedetector-gcp/tests/test_mapping.py index 0a00e31f..f8ed7131 100644 --- a/opentelemetry-exporter-gcp-monitoring/tests/test_resource.py +++ b/opentelemetry-resourcedetector-gcp/tests/test_mapping.py @@ -12,9 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. +import dataclasses + import pytest -from google.protobuf import json_format -from opentelemetry.exporter.cloud_monitoring._resource import ( +from opentelemetry.resourcedetector.gcp_resource_detector._mapping import ( get_monitored_resource, ) from opentelemetry.sdk.resources import Attributes, LabelValue, Resource @@ -213,13 +214,8 @@ def test_get_monitored_resource( otel_attributes: Attributes, snapshot: SnapshotAssertion ) -> None: resource = Resource.create(otel_attributes) - monitored_resource = get_monitored_resource(resource) - - as_dict = ( - json_format.MessageToDict(monitored_resource) - if monitored_resource - else None - ) + monitored_resource_data = get_monitored_resource(resource) + as_dict = dataclasses.asdict(monitored_resource_data) assert as_dict == snapshot @@ -236,8 +232,10 @@ def test_get_monitored_resource( ) def test_non_string_values(value: LabelValue, expect: str): # host.id will end up in generic_node's node_id label - monitored_resource = get_monitored_resource(Resource({"host.id": value})) - assert monitored_resource is not None + monitored_resource_data = get_monitored_resource( + Resource({"host.id": value}) + ) + assert monitored_resource_data is not None - value_as_gcm_label = monitored_resource.labels["node_id"] + value_as_gcm_label = monitored_resource_data.labels["node_id"] assert value_as_gcm_label == expect diff --git a/tox.ini b/tox.ini index f73d4505..6e3a3fa7 100644 --- a/tox.ini +++ b/tox.ini @@ -21,6 +21,11 @@ base_deps = -c {toxinidir}/dev-constraints.txt -e {toxinidir}/test-common +; the inter-monorepo dependencies +monorepo_deps = + cloudmonitoring: -e {toxinidir}/opentelemetry-resourcedetector-gcp/ + cloudtrace: -e {toxinidir}/opentelemetry-resourcedetector-gcp/ + dev_basepython = python3.10 dev_deps = {[constants]base_deps} @@ -53,6 +58,7 @@ setenv = [testenv:py3{7,8,9,10}-ci-test-{cloudtrace,cloudmonitoring,propagator,resourcedetector}] deps = test: {[constants]base_deps} + test: {[constants]monorepo_deps} test: pytest test: syrupy passenv = SKIP_GET_MOCK_SERVER @@ -70,6 +76,7 @@ whitelist_externals = bash basepython = {[constants]dev_basepython} deps = {[constants]dev_deps} + {[constants]monorepo_deps} changedir = {env:PACKAGE_NAME} commands_pre = @@ -104,6 +111,7 @@ envdir = resourcedetector: opentelemetry-resourcedetector-gcp/venv deps = {[constants]dev_deps} + {[constants]monorepo_deps} -e {env:PACKAGE_NAME} changedir = {env:PACKAGE_NAME}