Skip to content

Commit b02ad62

Browse files
Cleanup the resource detector code a bit (#389)
* Add space to logging exporter README. Make minor update to releasing.md. * Update code examples for how to use the GoogleCloudResourceDetector. Delete the old deprecated resource detector in __init__.py, and replace it with the new resource detector from _detector.py. Add an entry point to setup.cfg so that the GCP resource detector works with auto instrumentation. * Add telemetry endpoint to trace examples. * Remove obsolete comment * Fix e2e test and more samples to use GoogleCloudResourceDetector * More sample cleanup * Fix samples again * respond to comments * fix rst file * fix rst --------- Co-authored-by: Aaron Abbott <[email protected]>
1 parent 5fffd7f commit b02ad62

File tree

11 files changed

+304
-1032
lines changed

11 files changed

+304
-1032
lines changed

e2e-test-server/e2e_test_server/scenarios.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,7 @@
2222
from opentelemetry.propagators.cloud_trace_propagator import (
2323
CloudTraceFormatPropagator,
2424
)
25-
from opentelemetry.resourcedetector.gcp_resource_detector._detector import (
26-
GoogleCloudResourceDetector,
27-
)
25+
from opentelemetry.resourcedetector.gcp_resource_detector import GoogleCloudResourceDetector
2826
from opentelemetry.sdk.trace import TracerProvider
2927
from opentelemetry.sdk.trace.export import BatchSpanProcessor
3028
from opentelemetry.sdk.trace.sampling import ALWAYS_ON

opentelemetry-resourcedetector-gcp/CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22

33
## Unreleased
44

5-
Update opentelemetry-api/sdk dependencies to 1.3.
5+
- Update opentelemetry-api/sdk dependencies to 1.3.
6+
7+
- This is a breaking change which moves our official recource detector from `opentelemetry.resourcedetector.gcp_resource_detector._detector`
8+
into `opentelemetry.resourcedetector.gcp_resource_detector` and deletes the outdated duplicate resource detector
9+
which used to be there. Use `from opentelemetry.resourcedetector.gcp_resource_detector import GoogleCloudResourceDetector`
10+
to import it. See ([#389](https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/pull/389)) for details.
611

712
## Version 1.9.0a0
813

opentelemetry-resourcedetector-gcp/README.rst

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,29 @@ Installation
2424
2525
pip install opentelemetry-resourcedetector-gcp
2626
27-
..
28-
TODO: Add usage info here
29-
27+
Usage
28+
-----
29+
30+
.. code:: python
31+
32+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
33+
from opentelemetry.sdk.trace import TracerProvider
34+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
35+
from opentelemetry import trace
36+
from opentelemetry.sdk.resources import SERVICE_INSTANCE_ID, Resource
37+
38+
# This will use the GooglecloudResourceDetector under the covers.
39+
resource = Resource.create(
40+
attributes={
41+
# Use the PID as the service.instance.id to avoid duplicate timeseries
42+
# from different Gunicorn worker processes.
43+
SERVICE_INSTANCE_ID: f"worker-{os.getpid()}",
44+
}
45+
)
46+
traceProvider = TracerProvider(resource=resource)
47+
processor = BatchSpanProcessor(OTLPSpanExporter())
48+
traceProvider.add_span_processor(processor)
49+
trace.set_tracer_provider(traceProvider)
3050
3151
References
3252
----------

opentelemetry-resourcedetector-gcp/setup.cfg

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,7 @@ where = src
3737

3838
[options.extras_require]
3939
test =
40+
41+
[options.entry_points]
42+
opentelemetry_resource_detector =
43+
gcp_resource_detector = opentelemetry.resourcedetector.gcp_resource_detector:GoogleCloudResourceDetector
Lines changed: 101 additions & 164 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2021 Google LLC
1+
# Copyright 2025 Google LLC
22
#
33
# Licensed under the Apache License, Version 2.0 (the "License");
44
# you may not use this file except in compliance with the License.
@@ -12,195 +12,132 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
import logging
16-
import os
15+
from typing import Mapping
1716

18-
import requests
19-
from opentelemetry.context import attach, detach, set_value
17+
from opentelemetry.resourcedetector.gcp_resource_detector import (
18+
_faas,
19+
_gae,
20+
_gce,
21+
_gke,
22+
_metadata,
23+
)
24+
from opentelemetry.resourcedetector.gcp_resource_detector._constants import (
25+
ResourceAttributes,
26+
)
2027
from opentelemetry.sdk.resources import Resource, ResourceDetector
28+
from opentelemetry.util.types import AttributeValue
2129

22-
_GCP_METADATA_URL = (
23-
"http://metadata.google.internal/computeMetadata/v1/?recursive=true"
24-
)
25-
_GCP_METADATA_URL_HEADER = {"Metadata-Flavor": "Google"}
26-
_TIMEOUT_SEC = 5
27-
28-
logger = logging.getLogger(__name__)
29-
30-
31-
def _get_google_metadata_and_common_attributes():
32-
token = attach(set_value("suppress_instrumentation", True))
33-
all_metadata = requests.get(
34-
_GCP_METADATA_URL,
35-
headers=_GCP_METADATA_URL_HEADER,
36-
timeout=_TIMEOUT_SEC,
37-
).json()
38-
detach(token)
39-
common_attributes = {
40-
"cloud.account.id": all_metadata["project"]["projectId"],
41-
"cloud.provider": "gcp",
42-
"cloud.zone": all_metadata["instance"]["zone"].split("/")[-1],
43-
}
44-
return common_attributes, all_metadata
45-
46-
47-
def get_gce_resources():
48-
"""Resource finder for common GCE attributes
49-
50-
See: https://cloud.google.com/compute/docs/storing-retrieving-metadata
51-
"""
52-
(
53-
common_attributes,
54-
all_metadata,
55-
) = _get_google_metadata_and_common_attributes()
56-
common_attributes.update(
30+
31+
class GoogleCloudResourceDetector(ResourceDetector):
32+
def detect(self) -> Resource:
33+
# pylint: disable=too-many-return-statements
34+
if not _metadata.is_available():
35+
return Resource.get_empty()
36+
37+
if _gke.on_gke():
38+
return _gke_resource()
39+
if _faas.on_cloud_functions():
40+
return _cloud_functions_resource()
41+
if _faas.on_cloud_run():
42+
return _cloud_run_resource()
43+
if _gae.on_app_engine():
44+
return _gae_resource()
45+
if _gce.on_gce():
46+
return _gce_resource()
47+
48+
return Resource.get_empty()
49+
50+
51+
def _gke_resource() -> Resource:
52+
zone_or_region = _gke.availability_zone_or_region()
53+
zone_or_region_key = (
54+
ResourceAttributes.CLOUD_AVAILABILITY_ZONE
55+
if zone_or_region.type == "zone"
56+
else ResourceAttributes.CLOUD_REGION
57+
)
58+
return _make_resource(
5759
{
58-
"host.id": all_metadata["instance"]["id"],
59-
"gcp.resource_type": "gce_instance",
60+
ResourceAttributes.CLOUD_PLATFORM_KEY: ResourceAttributes.GCP_KUBERNETES_ENGINE,
61+
zone_or_region_key: zone_or_region.value,
62+
ResourceAttributes.K8S_CLUSTER_NAME: _gke.cluster_name(),
63+
ResourceAttributes.HOST_ID: _gke.host_id(),
6064
}
6165
)
62-
return common_attributes
63-
6466

65-
def get_gke_resources():
66-
"""Resource finder for GKE attributes"""
6767

68-
if os.getenv("KUBERNETES_SERVICE_HOST") is None:
69-
return {}
70-
71-
(
72-
common_attributes,
73-
all_metadata,
74-
) = _get_google_metadata_and_common_attributes()
75-
76-
container_name = os.getenv("CONTAINER_NAME")
77-
if container_name is not None:
78-
common_attributes["container.name"] = container_name
79-
80-
# Fallback to reading namespace from a file is the env var is not set
81-
pod_namespace = os.getenv("NAMESPACE")
82-
if pod_namespace is None:
83-
try:
84-
with open(
85-
"/var/run/secrets/kubernetes.io/serviceaccount/namespace", "r"
86-
) as namespace_file:
87-
pod_namespace = namespace_file.read().strip()
88-
except FileNotFoundError:
89-
pod_namespace = ""
90-
91-
common_attributes.update(
68+
def _gce_resource() -> Resource:
69+
zone_and_region = _gce.availability_zone_and_region()
70+
return _make_resource(
9271
{
93-
"k8s.cluster.name": all_metadata["instance"]["attributes"][
94-
"cluster-name"
95-
],
96-
"k8s.namespace.name": pod_namespace,
97-
"k8s.pod.name": os.getenv("POD_NAME", os.getenv("HOSTNAME", "")),
98-
"host.id": all_metadata["instance"]["id"],
99-
"gcp.resource_type": "gke_container",
72+
ResourceAttributes.CLOUD_PLATFORM_KEY: ResourceAttributes.GCP_COMPUTE_ENGINE,
73+
ResourceAttributes.CLOUD_AVAILABILITY_ZONE: zone_and_region.zone,
74+
ResourceAttributes.CLOUD_REGION: zone_and_region.region,
75+
ResourceAttributes.HOST_TYPE: _gce.host_type(),
76+
ResourceAttributes.HOST_ID: _gce.host_id(),
77+
ResourceAttributes.HOST_NAME: _gce.host_name(),
10078
}
10179
)
102-
return common_attributes
103-
104-
105-
def get_cloudrun_resources():
106-
"""Resource finder for Cloud Run attributes"""
10780

108-
if os.getenv("K_CONFIGURATION") is None:
109-
return {}
11081

111-
(
112-
common_attributes,
113-
all_metadata,
114-
) = _get_google_metadata_and_common_attributes()
115-
116-
faas_name = os.getenv("K_SERVICE")
117-
if faas_name is not None:
118-
common_attributes["faas.name"] = str(faas_name)
119-
120-
faas_version = os.getenv("K_REVISION")
121-
if faas_version is not None:
122-
common_attributes["faas.version"] = str(faas_version)
123-
124-
common_attributes.update(
82+
def _cloud_run_resource() -> Resource:
83+
return _make_resource(
12584
{
126-
"cloud.platform": "gcp_cloud_run",
127-
"cloud.region": all_metadata["instance"]["region"].split("/")[-1],
128-
"faas.instance": all_metadata["instance"]["id"],
129-
"gcp.resource_type": "cloud_run",
85+
ResourceAttributes.CLOUD_PLATFORM_KEY: ResourceAttributes.GCP_CLOUD_RUN,
86+
ResourceAttributes.FAAS_NAME: _faas.faas_name(),
87+
ResourceAttributes.FAAS_VERSION: _faas.faas_version(),
88+
ResourceAttributes.FAAS_INSTANCE: _faas.faas_instance(),
89+
ResourceAttributes.CLOUD_REGION: _faas.faas_cloud_region(),
13090
}
13191
)
132-
return common_attributes
133-
13492

135-
def get_cloudfunctions_resources():
136-
"""Resource finder for Cloud Functions attributes"""
13793

138-
if os.getenv("FUNCTION_TARGET") is None:
139-
return {}
94+
def _cloud_functions_resource() -> Resource:
95+
return _make_resource(
96+
{
97+
ResourceAttributes.CLOUD_PLATFORM_KEY: ResourceAttributes.GCP_CLOUD_FUNCTIONS,
98+
ResourceAttributes.FAAS_NAME: _faas.faas_name(),
99+
ResourceAttributes.FAAS_VERSION: _faas.faas_version(),
100+
ResourceAttributes.FAAS_INSTANCE: _faas.faas_instance(),
101+
ResourceAttributes.CLOUD_REGION: _faas.faas_cloud_region(),
102+
}
103+
)
140104

141-
(
142-
common_attributes,
143-
all_metadata,
144-
) = _get_google_metadata_and_common_attributes()
145105

146-
faas_name = os.getenv("K_SERVICE")
147-
if faas_name is not None:
148-
common_attributes["faas.name"] = str(faas_name)
106+
def _gae_resource() -> Resource:
107+
if _gae.on_app_engine_standard():
108+
zone = _gae.standard_availability_zone()
109+
region = _gae.standard_cloud_region()
110+
else:
111+
zone_and_region = _gae.flex_availability_zone_and_region()
112+
zone = zone_and_region.zone
113+
region = zone_and_region.region
149114

150-
faas_version = os.getenv("K_REVISION")
151-
if faas_version is not None:
152-
common_attributes["faas.version"] = str(faas_version)
115+
faas_name = _gae.service_name()
116+
faas_version = _gae.service_version()
117+
faas_instance = _gae.service_instance()
153118

154-
common_attributes.update(
119+
return _make_resource(
155120
{
156-
"cloud.platform": "gcp_cloud_functions",
157-
"cloud.region": all_metadata["instance"]["region"].split("/")[-1],
158-
"faas.instance": all_metadata["instance"]["id"],
159-
"gcp.resource_type": "cloud_functions",
121+
ResourceAttributes.CLOUD_PLATFORM_KEY: ResourceAttributes.GCP_APP_ENGINE,
122+
ResourceAttributes.FAAS_NAME: faas_name,
123+
ResourceAttributes.FAAS_VERSION: faas_version,
124+
ResourceAttributes.FAAS_INSTANCE: faas_instance,
125+
ResourceAttributes.CLOUD_AVAILABILITY_ZONE: zone,
126+
ResourceAttributes.CLOUD_REGION: region,
160127
}
161128
)
162-
return common_attributes
163129

164130

165-
# Order here matters. Since a GKE_CONTAINER is a specialized type of GCE_INSTANCE
166-
# We need to first check if it matches the criteria for being a GKE_CONTAINER
167-
# before falling back and checking if its a GCE_INSTANCE.
168-
# This list should be sorted from most specialized to least specialized.
169-
_RESOURCE_FINDERS = [
170-
("gke_container", get_gke_resources),
171-
("cloud_run", get_cloudrun_resources),
172-
("cloud_functions", get_cloudfunctions_resources),
173-
("gce_instance", get_gce_resources),
174-
]
175-
176-
177-
class NoGoogleResourcesFound(Exception):
178-
pass
131+
def _make_resource(attrs: Mapping[str, AttributeValue]) -> Resource:
132+
return Resource(
133+
{
134+
ResourceAttributes.CLOUD_PROVIDER: "gcp",
135+
ResourceAttributes.CLOUD_ACCOUNT_ID: _metadata.get_metadata()[
136+
"project"
137+
]["projectId"],
138+
**attrs,
139+
}
140+
)
179141

180142

181-
class GoogleCloudResourceDetector(ResourceDetector):
182-
def __init__(self, raise_on_error=False):
183-
super().__init__(raise_on_error)
184-
self.cached = False
185-
self.gcp_resources = {}
186-
187-
def detect(self) -> "Resource":
188-
if not self.cached:
189-
self.cached = True
190-
for resource_type, resource_finder in _RESOURCE_FINDERS:
191-
try:
192-
found_resources = resource_finder()
193-
# pylint: disable=broad-except
194-
except Exception as ex:
195-
logger.warning(
196-
"Exception %s occured attempting %s resource detection",
197-
ex,
198-
resource_type,
199-
)
200-
found_resources = None
201-
if found_resources:
202-
self.gcp_resources = found_resources
203-
break
204-
if self.raise_on_error and not self.gcp_resources:
205-
raise NoGoogleResourcesFound()
206-
return Resource(self.gcp_resources)
143+
__all__ = ["GoogleCloudResourceDetector"]

0 commit comments

Comments
 (0)