From 5369023ebf6a8803cdc89d23cee4f2cd4439cf01 Mon Sep 17 00:00:00 2001 From: Tim Pansino Date: Fri, 19 Dec 2025 16:15:45 -0500 Subject: [PATCH 1/3] Patch issues in azure functions utilization --- newrelic/common/utilization.py | 38 +++++++++++++++++----- newrelic/hooks/framework_azurefunctions.py | 9 +++-- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/newrelic/common/utilization.py b/newrelic/common/utilization.py index 22b158e3ec..de3ea7ee67 100644 --- a/newrelic/common/utilization.py +++ b/newrelic/common/utilization.py @@ -27,8 +27,6 @@ _logger = logging.getLogger(__name__) VALID_CHARS_RE = re.compile(r"[0-9a-zA-Z_ ./-]") -AZURE_RESOURCE_GROUP_NAME_RE = re.compile(r"\+([a-zA-Z0-9\-]+)-[a-zA-Z0-9]+(?:-Linux)") -AZURE_RESOURCE_GROUP_NAME_PARTIAL_RE = re.compile(r"\+([a-zA-Z0-9\-]+)(?:-Linux)?-[a-zA-Z0-9]+") class UtilizationHttpClient(InsecureHttpClient): @@ -233,18 +231,42 @@ class AzureFunctionUtilization(CommonUtilization): HEADERS = {"Metadata": "true"} # noqa: RUF012 VENDOR_NAME = "azurefunction" - @staticmethod - def fetch(): + RESOURCE_GROUP_NAME_RE = re.compile(r"\+([a-zA-Z0-9\-]+)-[a-zA-Z0-9]+(?:-Linux)") + RESOURCE_GROUP_NAME_PARTIAL_RE = re.compile(r"\+([a-zA-Z0-9\-]+)(?:-Linux)?-[a-zA-Z0-9]+") + SUBSCRIPTION_ID_RE = re.compile(r"(?:(?!\+).)*") + + @classmethod + def fetch(cls): cloud_region = os.environ.get("REGION_NAME") website_owner_name = os.environ.get("WEBSITE_OWNER_NAME") azure_function_app_name = os.environ.get("WEBSITE_SITE_NAME") if all((cloud_region, website_owner_name, azure_function_app_name)): - if website_owner_name.endswith("-Linux"): - resource_group_name = AZURE_RESOURCE_GROUP_NAME_RE.search(website_owner_name).group(1) + resource_group_name_re = ( + cls.RESOURCE_GROUP_NAME_RE + if website_owner_name.endswith("-Linux") + else cls.RESOURCE_GROUP_NAME_PARTIAL_RE + ) + match = resource_group_name_re.search(website_owner_name) + if match: + resource_group_name = match.group(1) else: - resource_group_name = AZURE_RESOURCE_GROUP_NAME_PARTIAL_RE.search(website_owner_name).group(1) - subscription_id = re.search(r"(?:(?!\+).)*", website_owner_name).group(0) + _logger.debug( + "Unable to determine Azure Functions resource group name from WEBSITE_OWNER_NAME. %r", + website_owner_name, + ) + return None + + match = cls.SUBSCRIPTION_ID_RE.search(website_owner_name) + if match: + subscription_id = match.group(0) + else: + _logger.debug( + "Unable to determine Azure Functions subscription id from WEBSITE_OWNER_NAME. %r", + website_owner_name, + ) + return None + faas_app_name = f"/subscriptions/{subscription_id}/resourceGroups/{resource_group_name}/providers/Microsoft.Web/sites/{azure_function_app_name}" # Only send if all values are present return (faas_app_name, cloud_region) diff --git a/newrelic/hooks/framework_azurefunctions.py b/newrelic/hooks/framework_azurefunctions.py index 968e235e50..830f91be90 100644 --- a/newrelic/hooks/framework_azurefunctions.py +++ b/newrelic/hooks/framework_azurefunctions.py @@ -21,7 +21,10 @@ from newrelic.api.web_transaction import WebTransaction from newrelic.common.object_wrapper import wrap_function_wrapper from newrelic.common.signature import bind_args -from newrelic.common.utilization import AZURE_RESOURCE_GROUP_NAME_PARTIAL_RE, AZURE_RESOURCE_GROUP_NAME_RE +from newrelic.common.utilization import AzureFunctionUtilization + +RESOURCE_GROUP_NAME_PARTIAL_RE = AzureFunctionUtilization.RESOURCE_GROUP_NAME_PARTIAL_RE +RESOURCE_GROUP_NAME_RE = AzureFunctionUtilization.RESOURCE_GROUP_NAME_RE def original_agent_instance(): @@ -42,9 +45,9 @@ def intrinsics_populator(application, context): r"(?:(?!\+).)*", website_owner_name ).group(0) if website_owner_name and website_owner_name.endswith("-Linux"): - resource_group_name = AZURE_RESOURCE_GROUP_NAME_RE.search(website_owner_name).group(1) + resource_group_name = RESOURCE_GROUP_NAME_RE.search(website_owner_name).group(1) elif website_owner_name: - resource_group_name = AZURE_RESOURCE_GROUP_NAME_PARTIAL_RE.search(website_owner_name).group(1) + resource_group_name = RESOURCE_GROUP_NAME_PARTIAL_RE.search(website_owner_name).group(1) else: resource_group_name = os.environ.get("WEBSITE_RESOURCE_GROUP", "Unknown") azure_function_app_name = os.environ.get("WEBSITE_SITE_NAME", getattr(application, "name", "Azure Function App")) From e1cbb1227368d364df8274f6155cf5323106ccd7 Mon Sep 17 00:00:00 2001 From: Tim Pansino Date: Fri, 19 Dec 2025 16:15:52 -0500 Subject: [PATCH 2/3] Add regression test --- .../test_utilization.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 tests/framework_azurefunctions/test_utilization.py diff --git a/tests/framework_azurefunctions/test_utilization.py b/tests/framework_azurefunctions/test_utilization.py new file mode 100644 index 0000000000..6ef0a753c9 --- /dev/null +++ b/tests/framework_azurefunctions/test_utilization.py @@ -0,0 +1,38 @@ +# Copyright 2010 New Relic, Inc. +# +# 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 +# +# http://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. + +from newrelic.common.utilization import AzureFunctionUtilization + + +def test_utilization(monkeypatch): + monkeypatch.setenv("REGION_NAME", "EastUS2") + monkeypatch.setenv("WEBSITE_OWNER_NAME", "b999997b-cb91-49e0-b922-c9188372bdba+testing-rg-EastUS2webspace-Linux") + monkeypatch.setenv("WEBSITE_SITE_NAME", "test-func-linux") + + result = AzureFunctionUtilization.fetch() + assert result, "Failed to parse utilization for Azure Functions." + + faas_app_name, cloud_region = result + expected_faas_app_name = "/subscriptions/b999997b-cb91-49e0-b922-c9188372bdba/resourceGroups/testing-rg/providers/Microsoft.Web/sites/test-func-linux" + assert faas_app_name == expected_faas_app_name + assert cloud_region == "EastUS2" + + +def test_utilization_bad_website_owner_name(monkeypatch): + monkeypatch.setenv("REGION_NAME", "EastUS2") + monkeypatch.setenv("WEBSITE_OWNER_NAME", "ERROR") + monkeypatch.setenv("WEBSITE_SITE_NAME", "test-func-linux") + + result = AzureFunctionUtilization.fetch() + assert result is None, f"Expected failure but got result instead. {result}" From e843f4893e71075aee3a1641138e7c32f2e4ca34 Mon Sep 17 00:00:00 2001 From: Tim Pansino Date: Mon, 29 Dec 2025 10:54:32 -0500 Subject: [PATCH 3/3] Use WEBSITE_RESOURCE_GROUP as primary info source --- newrelic/common/utilization.py | 33 +++++-------------- newrelic/hooks/framework_azurefunctions.py | 23 ++++--------- .../test_utilization.py | 1 + 3 files changed, 15 insertions(+), 42 deletions(-) diff --git a/newrelic/common/utilization.py b/newrelic/common/utilization.py index de3ea7ee67..d96d9d9aa8 100644 --- a/newrelic/common/utilization.py +++ b/newrelic/common/utilization.py @@ -231,36 +231,19 @@ class AzureFunctionUtilization(CommonUtilization): HEADERS = {"Metadata": "true"} # noqa: RUF012 VENDOR_NAME = "azurefunction" - RESOURCE_GROUP_NAME_RE = re.compile(r"\+([a-zA-Z0-9\-]+)-[a-zA-Z0-9]+(?:-Linux)") - RESOURCE_GROUP_NAME_PARTIAL_RE = re.compile(r"\+([a-zA-Z0-9\-]+)(?:-Linux)?-[a-zA-Z0-9]+") - SUBSCRIPTION_ID_RE = re.compile(r"(?:(?!\+).)*") - @classmethod def fetch(cls): cloud_region = os.environ.get("REGION_NAME") website_owner_name = os.environ.get("WEBSITE_OWNER_NAME") azure_function_app_name = os.environ.get("WEBSITE_SITE_NAME") - - if all((cloud_region, website_owner_name, azure_function_app_name)): - resource_group_name_re = ( - cls.RESOURCE_GROUP_NAME_RE - if website_owner_name.endswith("-Linux") - else cls.RESOURCE_GROUP_NAME_PARTIAL_RE - ) - match = resource_group_name_re.search(website_owner_name) - if match: - resource_group_name = match.group(1) - else: - _logger.debug( - "Unable to determine Azure Functions resource group name from WEBSITE_OWNER_NAME. %r", - website_owner_name, - ) - return None - - match = cls.SUBSCRIPTION_ID_RE.search(website_owner_name) - if match: - subscription_id = match.group(0) - else: + resource_group_name = os.environ.get("WEBSITE_RESOURCE_GROUP") + + if all((cloud_region, website_owner_name, azure_function_app_name, resource_group_name)): + subscription_id = "" + if "+" in website_owner_name: + subscription_id = website_owner_name.split("+")[0] + # Catch missing subscription_id or missing + + if not subscription_id: _logger.debug( "Unable to determine Azure Functions subscription id from WEBSITE_OWNER_NAME. %r", website_owner_name, diff --git a/newrelic/hooks/framework_azurefunctions.py b/newrelic/hooks/framework_azurefunctions.py index 830f91be90..d76725afca 100644 --- a/newrelic/hooks/framework_azurefunctions.py +++ b/newrelic/hooks/framework_azurefunctions.py @@ -13,7 +13,6 @@ # limitations under the License. import os -import re import urllib.parse as urlparse from newrelic.api.application import application_instance @@ -21,10 +20,6 @@ from newrelic.api.web_transaction import WebTransaction from newrelic.common.object_wrapper import wrap_function_wrapper from newrelic.common.signature import bind_args -from newrelic.common.utilization import AzureFunctionUtilization - -RESOURCE_GROUP_NAME_PARTIAL_RE = AzureFunctionUtilization.RESOURCE_GROUP_NAME_PARTIAL_RE -RESOURCE_GROUP_NAME_RE = AzureFunctionUtilization.RESOURCE_GROUP_NAME_RE def original_agent_instance(): @@ -38,18 +33,12 @@ def intrinsics_populator(application, context): trigger_type = "http" website_owner_name = os.environ.get("WEBSITE_OWNER_NAME", None) - if not website_owner_name: - subscription_id = "Unknown" - else: - subscription_id = re.search(r"(?:(?!\+).)*", website_owner_name) and re.search( - r"(?:(?!\+).)*", website_owner_name - ).group(0) - if website_owner_name and website_owner_name.endswith("-Linux"): - resource_group_name = RESOURCE_GROUP_NAME_RE.search(website_owner_name).group(1) - elif website_owner_name: - resource_group_name = RESOURCE_GROUP_NAME_PARTIAL_RE.search(website_owner_name).group(1) - else: - resource_group_name = os.environ.get("WEBSITE_RESOURCE_GROUP", "Unknown") + + subscription_id = "Unknown" + if website_owner_name and "+" in website_owner_name: + subscription_id = website_owner_name.split("+")[0] or "Unknown" + + resource_group_name = os.environ.get("WEBSITE_RESOURCE_GROUP", "Unknown") azure_function_app_name = os.environ.get("WEBSITE_SITE_NAME", getattr(application, "name", "Azure Function App")) cloud_resource_id = f"/subscriptions/{subscription_id}/resourceGroups/{resource_group_name}/providers/Microsoft.Web/sites/{azure_function_app_name}/functions/{getattr(context, 'function_name', 'Unknown')}" diff --git a/tests/framework_azurefunctions/test_utilization.py b/tests/framework_azurefunctions/test_utilization.py index 6ef0a753c9..3bcc5f9f63 100644 --- a/tests/framework_azurefunctions/test_utilization.py +++ b/tests/framework_azurefunctions/test_utilization.py @@ -17,6 +17,7 @@ def test_utilization(monkeypatch): monkeypatch.setenv("REGION_NAME", "EastUS2") + monkeypatch.setenv("WEBSITE_RESOURCE_GROUP", "testing-rg") monkeypatch.setenv("WEBSITE_OWNER_NAME", "b999997b-cb91-49e0-b922-c9188372bdba+testing-rg-EastUS2webspace-Linux") monkeypatch.setenv("WEBSITE_SITE_NAME", "test-func-linux")