Skip to content
Open
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
25 changes: 15 additions & 10 deletions newrelic/common/utilization.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -233,18 +231,25 @@ class AzureFunctionUtilization(CommonUtilization):
HEADERS = {"Metadata": "true"} # noqa: RUF012
VENDOR_NAME = "azurefunction"

@staticmethod
def fetch():
@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")
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,
)
return None

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)
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)
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)
Expand Down
20 changes: 6 additions & 14 deletions newrelic/hooks/framework_azurefunctions.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,13 @@
# limitations under the License.

import os
import re
import urllib.parse as urlparse

from newrelic.api.application import application_instance
from newrelic.api.transaction import current_transaction
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


def original_agent_instance():
Expand All @@ -35,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 = AZURE_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)
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")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The biggest issue here is that the WEBSITE_RESOURCE_GROUP environment variable is not guaranteed to be present.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More specifically, it is not present in the Consumer plan and it is not always present in the Flex Consumer plan at the time we would need the value of this.

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')}"
Expand Down
39 changes: 39 additions & 0 deletions tests/framework_azurefunctions/test_utilization.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# 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")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turns out this is case sensitive when being passed into the application: it should be all lowercase

Suggested change
monkeypatch.setenv("REGION_NAME", "EastUS2")
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")

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}"
Loading