Skip to content

App Service Resource Detector #31390

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions sdk/monitor/azure-monitor-opentelemetry-exporter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -665,6 +684,7 @@ contact [[email protected]](mailto:[email protected]) with any additio
<!-- LINKS -->
[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/
Expand All @@ -691,6 +711,7 @@ contact [[email protected]](mailto:[email protected]) 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,4 +153,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
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,18 @@

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
opentelemetry_version = version("opentelemetry-sdk")


def _is_on_app_service():
return "WEBSITE_SITE_NAME" in environ
return _WEBSITE_SITE_NAME in environ


def _is_attach_enabled():
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def __init__(self, **kwargs: Any) -> None:

:keyword str api_version: The service API version used. Defaults to latest.
:keyword str connection_string: The connection string used for your Application Insights resource.
:keyword ManagedIdentityCredential/ClientSecretCredential credential: Token credential, such as ManagedIdentityCredential or ClientSecretCredential, used for Azure Active Directory (AAD) authentication. Defaults to None.
:keyword Union[ManagedIdentityCredential, ClientSecretCredential] credential: Token credential, such as ManagedIdentityCredential or ClientSecretCredential, used for Azure Active Directory (AAD) authentication. Defaults to None.
:keyword bool disable_offline_storage: Determines whether to disable storing failed telemetry records for retry. Defaults to `False`.
:keyword str storage_directory: Storage path in which to store retry files. Defaults to `<tempfile.gettempdir()>/opentelemetry-python-<your-instrumentation-key>`.
:rtype: None
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# 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 (
_AZURE_APP_SERVICE_STAMP_RESOURCE_ATTRIBUTE,
_CLOUD_RESOURCE_ID_RESOURCE_ATTRIBUTE,
_REGION_NAME,
_WEBSITE_HOME_STAMPNAME,
_WEBSITE_HOSTNAME,
_WEBSITE_INSTANCE_ID,
_WEBSITE_OWNER_NAME,
_WEBSITE_RESOURCE_GROUP,
_WEBSITE_SITE_NAME,
_WEBSITE_SLOT_NAME,
)


_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):
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 (key, env_var) in _APP_SERVICE_ATTRIBUTE_ENV_VARS.items():
value = environ.get(env_var)
if value:
attributes[key] = 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 "/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Web/sites/%s" % (
subscription_id,
website_resource_group,
website_site_name,
)
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
_REQ_RETRY_NAME,
_REQ_SUCCESS_NAME,
_REQ_THROTTLE_NAME,
_WEBSITE_HOME_STAMPNAME,
_WEBSITE_HOSTNAME,
_WEBSITE_SITE_NAME,
)
from azure.monitor.opentelemetry.exporter.statsbeat._state import (
_REQUESTS_MAP_LOCK,
Expand Down Expand Up @@ -142,17 +145,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]
Expand Down
3 changes: 3 additions & 0 deletions sdk/monitor/azure-monitor-opentelemetry-exporter/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]
}
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
import unittest
from unittest.mock import patch

from azure.monitor.opentelemetry.exporter.resource.app_service_resource_detector import (
AzureAppServiceResourceDetector,
)

TEST_WEBSITE_SITE_NAME = "TEST_WEBSITE_SITE_NAME"
TEST_REGION_NAME = "TEST_REGION_NAME"
TEST_WEBSITE_SLOT_NAME = "TEST_WEBSITE_SLOT_NAME"
TEST_WEBSITE_HOSTNAME = "TEST_WEBSITE_HOSTNAME"
TEST_WEBSITE_INSTANCE_ID = "TEST_WEBSITE_INSTANCE_ID"
TEST_WEBSITE_HOME_STAMPNAME = "TEST_WEBSITE_HOME_STAMPNAME"

TEST_WEBSITE_RESOURCE_GROUP = "TEST_WEBSITE_RESOURCE_GROUP"
TEST_WEBSITE_OWNER_NAME = "TEST_WEBSITE_OWNER_NAME"

class TestAzureAppServiceResourceDetector(unittest.TestCase):
@patch.dict("os.environ", {
"WEBSITE_SITE_NAME": TEST_WEBSITE_SITE_NAME,
"REGION_NAME": TEST_REGION_NAME,
"WEBSITE_SLOT_NAME": TEST_WEBSITE_SLOT_NAME,
"WEBSITE_HOSTNAME": TEST_WEBSITE_HOSTNAME,
"WEBSITE_INSTANCE_ID": TEST_WEBSITE_INSTANCE_ID,
"WEBSITE_HOME_STAMPNAME": TEST_WEBSITE_HOME_STAMPNAME,
"WEBSITE_RESOURCE_GROUP": TEST_WEBSITE_RESOURCE_GROUP,
"WEBSITE_OWNER_NAME": TEST_WEBSITE_OWNER_NAME,
}, clear=True)
def test_on_app_service(self):
resource = AzureAppServiceResourceDetector().detect()
attributes = resource.attributes
# self.assertEqual(attributes {
# "service.name": TEST_WEBSITE_SITE_NAME,
# "cloud.provider": ,
# "cloud.platform": ,
# "cloud.resource_id": ,
# "cloud.region": ,
# "deployment.environment": ,
# "deployment.environment": ,
# })
self.assertEqual(attributes["service.name"], TEST_WEBSITE_SITE_NAME)
self.assertEqual(attributes["cloud.provider"], "azure")
self.assertEqual(attributes["cloud.platform"], "azure_app_service")

self.assertEqual(attributes["cloud.resource_id"], \
f"/subscriptions/{TEST_WEBSITE_OWNER_NAME}/resourceGroups/{TEST_WEBSITE_RESOURCE_GROUP}/providers/Microsoft.Web/sites/{TEST_WEBSITE_SITE_NAME}")

self.assertEqual(attributes["cloud.region"], TEST_REGION_NAME)
self.assertEqual(attributes["deployment.environment"], TEST_WEBSITE_SLOT_NAME)
self.assertEqual(attributes["host.id"], TEST_WEBSITE_HOSTNAME)
self.assertEqual(attributes["service.instance.id"], TEST_WEBSITE_INSTANCE_ID)
self.assertEqual(attributes["azure.app.service.stamp"], TEST_WEBSITE_HOME_STAMPNAME)

@patch.dict("os.environ", {
"WEBSITE_SITE_NAME": TEST_WEBSITE_SITE_NAME,
"REGION_NAME": TEST_REGION_NAME,
"WEBSITE_SLOT_NAME": TEST_WEBSITE_SLOT_NAME,
"WEBSITE_HOSTNAME": TEST_WEBSITE_HOSTNAME,
"WEBSITE_INSTANCE_ID": TEST_WEBSITE_INSTANCE_ID,
"WEBSITE_HOME_STAMPNAME": TEST_WEBSITE_HOME_STAMPNAME,
"WEBSITE_OWNER_NAME": TEST_WEBSITE_OWNER_NAME,
}, clear=True)
def test_on_app_service_no_resource_group(self):
resource = AzureAppServiceResourceDetector().detect()
attributes = resource.attributes
self.assertEqual(attributes["service.name"], TEST_WEBSITE_SITE_NAME)
self.assertEqual(attributes["cloud.provider"], "azure")
self.assertEqual(attributes["cloud.platform"], "azure_app_service")

self.assertTrue("cloud.resource_id" not in attributes)

self.assertEqual(attributes["cloud.region"], TEST_REGION_NAME)
self.assertEqual(attributes["deployment.environment"], TEST_WEBSITE_SLOT_NAME)
self.assertEqual(attributes["host.id"], TEST_WEBSITE_HOSTNAME)
self.assertEqual(attributes["service.instance.id"], TEST_WEBSITE_INSTANCE_ID)
self.assertEqual(attributes["azure.app.service.stamp"], TEST_WEBSITE_HOME_STAMPNAME)

@patch.dict("os.environ", {
"WEBSITE_SITE_NAME": TEST_WEBSITE_SITE_NAME,
"REGION_NAME": TEST_REGION_NAME,
"WEBSITE_SLOT_NAME": TEST_WEBSITE_SLOT_NAME,
"WEBSITE_HOSTNAME": TEST_WEBSITE_HOSTNAME,
"WEBSITE_INSTANCE_ID": TEST_WEBSITE_INSTANCE_ID,
"WEBSITE_HOME_STAMPNAME": TEST_WEBSITE_HOME_STAMPNAME,
"WEBSITE_OWNER_NAME": TEST_WEBSITE_OWNER_NAME,
}, clear=True)
def test_on_app_service_no_owner(self):
resource = AzureAppServiceResourceDetector().detect()
attributes = resource.attributes
self.assertEqual(attributes["service.name"], TEST_WEBSITE_SITE_NAME)
self.assertEqual(attributes["cloud.provider"], "azure")
self.assertEqual(attributes["cloud.platform"], "azure_app_service")

self.assertTrue("cloud.resource_id" not in attributes)

self.assertEqual(attributes["cloud.region"], TEST_REGION_NAME)
self.assertEqual(attributes["deployment.environment"], TEST_WEBSITE_SLOT_NAME)
self.assertEqual(attributes["host.id"], TEST_WEBSITE_HOSTNAME)
self.assertEqual(attributes["service.instance.id"], TEST_WEBSITE_INSTANCE_ID)
self.assertEqual(attributes["azure.app.service.stamp"], TEST_WEBSITE_HOME_STAMPNAME)

@patch.dict("os.environ", {
"REGION_NAME": TEST_REGION_NAME,
"WEBSITE_SLOT_NAME": TEST_WEBSITE_SLOT_NAME,
"WEBSITE_HOSTNAME": TEST_WEBSITE_HOSTNAME,
"WEBSITE_INSTANCE_ID": TEST_WEBSITE_INSTANCE_ID,
"WEBSITE_HOME_STAMPNAME": TEST_WEBSITE_HOME_STAMPNAME,
"WEBSITE_OWNER_NAME": TEST_WEBSITE_OWNER_NAME,
}, clear=True)
def test_off_app_service(self):
resource = AzureAppServiceResourceDetector().detect()
self.assertEqual(resource.attributes, {})