Skip to content

Commit e914c4e

Browse files
lrafeeiTimPansinomergify[bot]
authored
Azure function instrumentation (#1395)
* Azure Function App instrumentation * Validate new agent attributes * [MegaLinter] Apply linters fixes * Trigger Tests * Remove actions permissions * Update setup.py Co-authored-by: Timothy Pansino <[email protected]> * Pin tracerite for sanic tests * Add attribute check logic * [MegaLinter] Apply linters fixes * Trigger tests --------- Co-authored-by: Timothy Pansino <[email protected]> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
1 parent 94f6628 commit e914c4e

File tree

20 files changed

+973
-45
lines changed

20 files changed

+973
-45
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#!/bin/bash
2+
# Copyright 2010 New Relic, Inc.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
set -euo pipefail
17+
18+
# Create build dir
19+
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
20+
BUILD_DIR="${TOX_ENV_DIR:-${SCRIPT_DIR}}/build/azure-functions-worker"
21+
rm -rf ${BUILD_DIR}
22+
mkdir -p ${BUILD_DIR}
23+
24+
# Clone repository
25+
git clone https://github.com/Azure/azure-functions-python-worker.git ${BUILD_DIR}
26+
27+
# Setup virtual environment and install dependencies
28+
python -m venv "${BUILD_DIR}/.venv"
29+
PYTHON="${BUILD_DIR}/.venv/bin/python"
30+
PIP="${BUILD_DIR}/.venv/bin/pip"
31+
PIPCOMPILE="${BUILD_DIR}/.venv/bin/pip-compile"
32+
INVOKE="${BUILD_DIR}/.venv/bin/invoke"
33+
${PIP} install pip-tools build invoke
34+
35+
# Install proto build dependencies
36+
$(cd ${BUILD_DIR} && ${PIPCOMPILE} >${BUILD_DIR}/requirements.txt)
37+
${PIP} install -r ${BUILD_DIR}/requirements.txt
38+
39+
# Build proto files into pb2 files
40+
cd ${BUILD_DIR}/tests && ${INVOKE} -c test_setup build-protos
41+
42+
# Build and install the package into the original environment (not the build venv)
43+
pip install ${BUILD_DIR}
44+
45+
# Clean up and return to the original directory
46+
rm -rf ${BUILD_DIR}

.github/workflows/tests.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ on:
2424
schedule:
2525
- cron: "0 15 * * *"
2626

27+
permissions:
28+
contents: read
29+
2730
concurrency:
2831
group: ${{ github.ref || github.run_id }}-${{ github.workflow }}
2932
cancel-in-progress: true

newrelic/common/utilization.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626

2727
_logger = logging.getLogger(__name__)
2828
VALID_CHARS_RE = re.compile(r"[0-9a-zA-Z_ ./-]")
29+
AZURE_RESOURCE_GROUP_NAME_RE = re.compile(r"\+([a-zA-Z0-9\-]+)-[a-zA-Z0-9]+(?:-Linux)")
30+
AZURE_RESOURCE_GROUP_NAME_PARTIAL_RE = re.compile(r"\+([a-zA-Z0-9\-]+)(?:-Linux)?-[a-zA-Z0-9]+")
2931

3032

3133
class UtilizationHttpClient(InsecureHttpClient):
@@ -207,6 +209,43 @@ class AzureUtilization(CommonUtilization):
207209
VENDOR_NAME = "azure"
208210

209211

212+
class AzureFunctionUtilization(CommonUtilization):
213+
METADATA_HOST = "169.254.169.254"
214+
METADATA_PATH = "/metadata/instance/compute"
215+
METADATA_QUERY = {"api-version": "2017-03-01"}
216+
EXPECTED_KEYS = ("faas.app_name", "cloud.region")
217+
HEADERS = {"Metadata": "true"}
218+
VENDOR_NAME = "azurefunction"
219+
220+
@staticmethod
221+
def fetch():
222+
cloud_region = os.environ.get("REGION_NAME")
223+
website_owner_name = os.environ.get("WEBSITE_OWNER_NAME")
224+
azure_function_app_name = os.environ.get("WEBSITE_SITE_NAME")
225+
226+
if all((cloud_region, website_owner_name, azure_function_app_name)):
227+
if website_owner_name.endswith("-Linux"):
228+
resource_group_name = AZURE_RESOURCE_GROUP_NAME_RE.search(website_owner_name).group(1)
229+
else:
230+
resource_group_name = AZURE_RESOURCE_GROUP_NAME_PARTIAL_RE.search(website_owner_name).group(1)
231+
subscription_id = re.search(r"(?:(?!\+).)*", website_owner_name).group(0)
232+
faas_app_name = f"/subscriptions/{subscription_id}/resourceGroups/{resource_group_name}/providers/Microsoft.Web/sites/{azure_function_app_name}"
233+
# Only send if all values are present
234+
return (faas_app_name, cloud_region)
235+
236+
@classmethod
237+
def get_values(cls, response):
238+
if response is None or len(response) != 2:
239+
return
240+
241+
values = {}
242+
for k, v in zip(cls.EXPECTED_KEYS, response):
243+
if hasattr(v, "decode"):
244+
v = v.decode("utf-8")
245+
values[k] = v
246+
return values
247+
248+
210249
class GCPUtilization(CommonUtilization):
211250
EXPECTED_KEYS = ("id", "machineType", "name", "zone")
212251
HEADERS = {"Metadata-Flavor": "Google"}

newrelic/config.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,7 @@ def _process_configuration(section):
452452
_process_setting(section, "process_host.display_name", "get", None)
453453
_process_setting(section, "utilization.detect_aws", "getboolean", None)
454454
_process_setting(section, "utilization.detect_azure", "getboolean", None)
455+
_process_setting(section, "utilization.detect_azurefunction", "getboolean", None)
455456
_process_setting(section, "utilization.detect_docker", "getboolean", None)
456457
_process_setting(section, "utilization.detect_kubernetes", "getboolean", None)
457458
_process_setting(section, "utilization.detect_gcp", "getboolean", None)
@@ -4091,6 +4092,14 @@ def _process_module_builtin_defaults():
40914092
)
40924093
_process_module_definition("tornado.routing", "newrelic.hooks.framework_tornado", "instrument_tornado_routing")
40934094
_process_module_definition("tornado.web", "newrelic.hooks.framework_tornado", "instrument_tornado_web")
4095+
_process_module_definition(
4096+
"azure.functions._http", "newrelic.hooks.framework_azurefunctions", "instrument_azure_function__http"
4097+
)
4098+
_process_module_definition(
4099+
"azure_functions_worker.dispatcher",
4100+
"newrelic.hooks.framework_azurefunctions",
4101+
"instrument_azure_functions_worker_dispatcher",
4102+
)
40944103

40954104

40964105
def _process_module_entry_points():

newrelic/core/agent_protocol.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from newrelic.common.encoding_utils import json_decode, json_encode, serverless_payload_encode
2222
from newrelic.common.utilization import (
2323
AWSUtilization,
24+
AzureFunctionUtilization,
2425
AzureUtilization,
2526
DockerUtilization,
2627
ECSUtilization,
@@ -302,7 +303,7 @@ def _connect_payload(app_name, linked_applications, environment, settings):
302303

303304
utilization_settings = {}
304305
# metadata_version corresponds to the utilization spec being used.
305-
utilization_settings["metadata_version"] = 5
306+
utilization_settings["metadata_version"] = 6
306307
utilization_settings["logical_processors"] = system_info.logical_processor_count()
307308
utilization_settings["total_ram_mib"] = system_info.total_physical_memory()
308309
utilization_settings["hostname"] = hostname
@@ -342,6 +343,8 @@ def _connect_payload(app_name, linked_applications, environment, settings):
342343
vendors.append(GCPUtilization)
343344
if settings["utilization.detect_azure"]:
344345
vendors.append(AzureUtilization)
346+
if settings["utilization.detect_azurefunction"]:
347+
vendors.append(AzureFunctionUtilization)
345348

346349
for vendor in vendors:
347350
metadata = vendor.detect()

newrelic/core/application.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -597,6 +597,11 @@ def connect_to_data_collector(self, activate_agent):
597597
if self._agent_control.health_check_enabled:
598598
internal_metric("Supportability/AgentControl/Health/enabled", 1)
599599

600+
# Azure Function mode metric
601+
# Note: This environment variable will be set by the Azure Functions runtime
602+
if os.environ.get("FUNCTIONS_WORKER_RUNTIME", None):
603+
internal_metric("Supportability/Python/AzureFunctionMode/enabled", 1)
604+
600605
self._stats_engine.merge_custom_metrics(internal_metrics.metrics())
601606

602607
# Update the active session in this object. This will the

newrelic/core/attribute.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@
6464
"error.expected",
6565
"error.group.name",
6666
"error.message",
67+
"faas.name",
68+
"faas.trigger",
69+
"faas.invocation_id",
70+
"faas.coldStart",
6771
"graphql.field.name",
6872
"graphql.field.parentType",
6973
"graphql.field.path",

newrelic/core/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -959,6 +959,7 @@ def default_otlp_host(host):
959959

960960
_settings.utilization.detect_aws = True
961961
_settings.utilization.detect_azure = True
962+
_settings.utilization.detect_azurefunction = True
962963
_settings.utilization.detect_docker = True
963964
_settings.utilization.detect_kubernetes = True
964965
_settings.utilization.detect_gcp = True

0 commit comments

Comments
 (0)