From 27f5b9d237c08ef57792ae80e14ad886294056c1 Mon Sep 17 00:00:00 2001 From: jerevoss Date: Tue, 1 Aug 2023 12:34:56 -0700 Subject: [PATCH] App Service Resource Detector --- .../README.md | 21 +++++++ .../opentelemetry/exporter/_constants.py | 17 +++++ .../monitor/opentelemetry/exporter/_utils.py | 7 ++- .../exporter/resource/__init__.py | 0 .../resource/app_service_resource_detector.py | 63 +++++++++++++++++++ .../exporter/statsbeat/_statsbeat_metrics.py | 18 ++++-- .../setup.py | 3 + 7 files changed, 123 insertions(+), 6 deletions(-) create mode 100644 sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/resource/__init__.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/resource/app_service_resource_detector.py diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/README.md b/sdk/monitor/azure-monitor-opentelemetry-exporter/README.md index 612598e5ae4f6..6f77cc48c2f91 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/README.md +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/README.md @@ -74,6 +74,25 @@ from azure.monitor.opentelemetry.exporter import AzureMonitorTraceExporter exporter = AzureMonitorTraceExporter() ``` +#### Resource Detectors + +OpenTelemetry Python has an experimental feature whereby [Resource Detectors][resource_detector_docs] can be injected to Resource Attributes. This package includes a resource detector for Azure App Service. This detector fills out the following Resource Attributes: + * `service.name` + * `cloud.provider` + * `cloud.platform` + * `cloud.resource_id` + * `cloud.region` + * `deployment.environment` + * `host.id` + * `service.instance.id` + * `azure.app.service.stamp` + + For more information, see the [Semantic Conventions for Cloud Resource Attributes][cloud_sem_conv] + + To enable the App Service Resource Detector, add `azure_monitor_opentelemetry_app_service_resource_detector` to the `OTEL_EXPERIMENTAL_RESOURCE_DETECTORS` environment variable: + + `export OTEL_EXPERIMENTAL_RESOURCE_DETECTORS=azure_monitor_opentelemetry_app_service_resource_detector` + ## Key concepts Some of the key concepts for the Azure monitor exporter include: @@ -665,6 +684,7 @@ contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additio [aad_for_ai_docs]: https://learn.microsoft.com/azure/azure-monitor/app/azure-ad-authentication?tabs=python [api_docs]: https://azuresdkdocs.blob.core.windows.net/$web/python/azure-opentelemetry-exporter-azuremonitor/1.0.0b2/index.html +[cloud_sem_conv]: https://opentelemetry.io/docs/specs/otel/resource/semantic_conventions/cloud/ [exporter_samples]: https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/monitor/azure-monitor-opentelemetry-exporter/samples [product_docs]: https://docs.microsoft.com/azure/azure-monitor/overview [azure_sub]: https://azure.microsoft.com/free/ @@ -691,6 +711,7 @@ contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additio [metric_reader]:https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#metricreader [metric_reference]: https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/metrics/_exporter.py [ot_metrics_sdk]: https://opentelemetry-python.readthedocs.io/en/stable/sdk/metrics.html +[resource_detector_docs]: https://opentelemetry.io/docs/specs/otel/resource/sdk/#detecting-resource-information-from-the-environment [trace_concept]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/overview.md#tracing-signal [span]: https://opentelemetry-python.readthedocs.io/en/stable/api/trace.html?highlight=TracerProvider#opentelemetry.trace.Span [tracer]: https://opentelemetry-python.readthedocs.io/en/stable/api/trace.html?highlight=TracerProvider#opentelemetry.trace.Tracer diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/_constants.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/_constants.py index 3afdf5ab16d0c..fa603c2f529b1 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/_constants.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/_constants.py @@ -139,4 +139,21 @@ _SAMPLE_RATE_KEY = "_MS.sampleRate" +# Resource detectors + +# TODO: Remove once this resource attribute is no longer missing from SDK +_CLOUD_RESOURCE_ID_RESOURCE_ATTRIBUTE = "cloud.resource_id" +_AZURE_APP_SERVICE_STAMP_RESOURCE_ATTRIBUTE = "azure.app.service.stamp" + +# App Service environment variables + +_WEBSITE_SITE_NAME = "WEBSITE_SITE_NAME" +_REGION_NAME = "REGION_NAME" +_WEBSITE_SLOT_NAME = "WEBSITE_SLOT_NAME" +_WEBSITE_HOSTNAME = "WEBSITE_HOSTNAME" +_WEBSITE_INSTANCE_ID = "WEBSITE_INSTANCE_ID" +_WEBSITE_HOME_STAMPNAME = "WEBSITE_HOME_STAMPNAME" +_WEBSITE_RESOURCE_GROUP = "WEBSITE_RESOURCE_GROUP" +_WEBSITE_OWNER_NAME = "WEBSITE_OWNER_NAME" + # cSpell:disable diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/_utils.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/_utils.py index 94102e554e166..d0f5eb63e8895 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/_utils.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/_utils.py @@ -19,7 +19,10 @@ from azure.monitor.opentelemetry.exporter._generated.models import TelemetryItem from azure.monitor.opentelemetry.exporter._version import VERSION as ext_version -from azure.monitor.opentelemetry.exporter._constants import _INSTRUMENTATIONS_BIT_MAP +from azure.monitor.opentelemetry.exporter._constants import ( + _INSTRUMENTATIONS_BIT_MAP, + _WEBSITE_SITE_NAME, +) # Workaround for missing version file @@ -27,7 +30,7 @@ def _get_sdk_version_prefix(): - is_on_app_service = "WEBSITE_SITE_NAME" in environ + is_on_app_service = _WEBSITE_SITE_NAME in environ is_attach_enabled = isdir("/agents/python/") sdk_version_prefix = '' if is_on_app_service and is_attach_enabled: diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/resource/__init__.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/resource/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/resource/app_service_resource_detector.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/resource/app_service_resource_detector.py new file mode 100644 index 0000000000000..38ab16252df09 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/resource/app_service_resource_detector.py @@ -0,0 +1,63 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +from os import environ + +from opentelemetry.sdk.resources import ResourceDetector, Resource +from opentelemetry.semconv.resource import ResourceAttributes, CloudPlatformValues, CloudProviderValues + +from azure.monitor.opentelemetry.exporter._constants import ( + _CLOUD_RESOURCE_ID_RESOURCE_ATTRIBUTE, + _REGION_NAME, + _WEBSITE_SITE_NAME, + _WEBSITE_HOME_STAMPNAME, + _WEBSITE_HOSTNAME, + _WEBSITE_INSTANCE_ID, + _WEBSITE_OWNER_NAME, + _WEBSITE_RESOURCE_GROUP, + _WEBSITE_SLOT_NAME, + _AZURE_APP_SERVICE_STAMP_RESOURCE_ATTRIBUTE, +) + + +_APP_SERVICE_ATTRIBUTE_ENV_VARS = { + ResourceAttributes.CLOUD_REGION: _REGION_NAME, + ResourceAttributes.DEPLOYMENT_ENVIRONMENT: _WEBSITE_SLOT_NAME, + ResourceAttributes.HOST_ID: _WEBSITE_HOSTNAME, + ResourceAttributes.SERVICE_INSTANCE_ID: _WEBSITE_INSTANCE_ID, + _AZURE_APP_SERVICE_STAMP_RESOURCE_ATTRIBUTE: _WEBSITE_HOME_STAMPNAME, +} + +class AzureAppServiceResourceDetector(ResourceDetector): + # pylint: disable=no-self-use + def detect(self) -> "Resource": + attributes = {} + website_site_name = environ.get(_WEBSITE_SITE_NAME) + if website_site_name: + print(_WEBSITE_SITE_NAME) + attributes[ResourceAttributes.SERVICE_NAME] = website_site_name + attributes[ResourceAttributes.CLOUD_PROVIDER] = CloudProviderValues.AZURE.value + attributes[ResourceAttributes.CLOUD_PLATFORM] = CloudPlatformValues.AZURE_APP_SERVICE.value + + azure_resource_uri = _get_azure_resource_uri(website_site_name) + if azure_resource_uri: + attributes[_CLOUD_RESOURCE_ID_RESOURCE_ATTRIBUTE] = azure_resource_uri + for attribute_key in _APP_SERVICE_ATTRIBUTE_ENV_VARS: + attribute_value = environ.get(_APP_SERVICE_ATTRIBUTE_ENV_VARS[attribute_key]) + if attribute_value: + attributes[attribute_key] = attribute_value + + print(attributes) + return Resource(attributes) + +def _get_azure_resource_uri(website_site_name): + website_resource_group = environ.get(_WEBSITE_RESOURCE_GROUP) + website_owner_name = environ.get(_WEBSITE_OWNER_NAME) + + subscription_id = website_owner_name + if website_owner_name and '+' in website_owner_name: + subscription_id = website_owner_name[0:website_owner_name.index('+')] + + if not (website_resource_group and subscription_id): + return None + + return f"/subscriptions/{subscription_id}/resourceGroups/{website_resource_group}/providers/Microsoft.Web/sites/{website_site_name}" \ No newline at end of file diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/statsbeat/_statsbeat_metrics.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/statsbeat/_statsbeat_metrics.py index 945bc96b11e42..bde0c8f484f5f 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/statsbeat/_statsbeat_metrics.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/statsbeat/_statsbeat_metrics.py @@ -23,6 +23,16 @@ _REQ_RETRY_NAME, _REQ_SUCCESS_NAME, _REQ_THROTTLE_NAME, + _CLOUD_RESOURCE_ID_RESOURCE_ATTRIBUTE, + _WEBSITE_SITE_NAME, + _REGION_NAME, + _WEBSITE_SITE_NAME, + _WEBSITE_HOME_STAMPNAME, + _WEBSITE_HOSTNAME, + _WEBSITE_INSTANCE_ID, + _WEBSITE_OWNER_NAME, + _WEBSITE_RESOURCE_GROUP, + _WEBSITE_SLOT_NAME, ) from azure.monitor.opentelemetry.exporter.statsbeat._state import ( _REQUESTS_MAP_LOCK, @@ -142,17 +152,17 @@ def _get_attach_metric(self, options: CallbackOptions) -> Iterable[Observation]: rpId = '' os_type = platform.system() # rp, rpId - if os.environ.get("WEBSITE_SITE_NAME") is not None: + if os.environ.get(_WEBSITE_SITE_NAME) is not None: # Web apps rp = _RP_NAMES[0] rpId = '{}/{}'.format( - os.environ.get("WEBSITE_SITE_NAME"), - os.environ.get("WEBSITE_HOME_STAMPNAME", '') + os.environ.get(_WEBSITE_SITE_NAME), + os.environ.get(_WEBSITE_HOME_STAMPNAME, '') ) elif os.environ.get("FUNCTIONS_WORKER_RUNTIME") is not None: # Function apps rp = _RP_NAMES[1] - rpId = os.environ.get("WEBSITE_HOSTNAME") + rpId = os.environ.get(_WEBSITE_HOSTNAME) elif self._vm_retry and self._get_azure_compute_metadata(): # VM rp = _RP_NAMES[2] diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/setup.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/setup.py index c5664935cce2f..88456efd67b3a 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/setup.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/setup.py @@ -100,6 +100,9 @@ ], "opentelemetry_traces_sampler": [ "azure_monitor_opentelemetry_sampler = azure.monitor.opentelemetry.exporter.export.trace._sampling:azure_monitor_opentelemetry_sampler_factory" + ], + "opentelemetry_resource_detector": [ + "azure_monitor_opentelemetry_app_service_resource_detector = azure.monitor.opentelemetry.exporter.resource.app_service_resource_detector:AzureAppServiceResourceDetector", ] } )