Skip to content

Commit

Permalink
feat(azure_functions): add azure functions integration (#11474)
Browse files Browse the repository at this point in the history
This PR adds an integration for tracing the
[azure-functions](https://pypi.org/project/azure-functions/) package.

### Additional Notes:
- This change only supports the [v2 programming
model](https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference-python?tabs=get-started%2Casgi%2Capplication-level&pivots=python-mode-decorators).
If there are enough requests for the v1 programming model we can add
tracing in a future PR
- This change only supports tracing [Http
triggers](https://github.com/Azure/azure-functions-python-library/blob/dd4fac4db0ff4ca3cd01d314a0ddf280aa59813e/azure/functions/decorators/function_app.py#L462).
Tracing for other triggers will be added in future PRs
- Azure Functions package currently supports Python versions `3.7` to
`3.11` (no `3.12` support at the moment)
- Builds off the integration work started by @gord02 in
#9726
- Dockerfile changes to testrunner made in
#11617 and
#11609
  * `mariadb` install was broken in the testrunner image
*
[azure-functions-core-tools](https://github.com/Azure/azure-functions-core-tools)
package must be installed on the test runner for tests to work
- Version pinned to
[4.0.6280](https://github.com/Azure/azure-functions-core-tools/releases/tag/4.0.6280)
due to some issues with the most recent versions
    - Package only supported on `linux/amd64` architecture

## Checklist
- [x] PR author has checked that all the criteria below are met
- The PR description includes an overview of the change
- The PR description articulates the motivation for the change
- The change includes tests OR the PR description describes a testing
strategy
- The PR description notes risks associated with the change, if any
- Newly-added code is easy to change
- The change follows the [library release note
guidelines](https://ddtrace.readthedocs.io/en/stable/releasenotes.html)
- The change includes or references documentation updates if necessary
- Backport labels are set (if
[applicable](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting))

## Reviewer Checklist
- [x] Reviewer has checked that all the criteria below are met 
- Title is accurate
- All changes are related to the pull request's stated goal
- Avoids breaking
[API](https://ddtrace.readthedocs.io/en/stable/versioning.html#interfaces)
changes
- Testing strategy adequately addresses listed risks
- Newly-added code is easy to change
- Release note makes sense to a user of the library
- If necessary, author has acknowledged and discussed the performance
implications of this PR as reported in the benchmarks PR comment
- Backport labels are set in a manner that is consistent with the
[release branch maintenance
policy](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting)
  • Loading branch information
duncanpharvey authored Dec 18, 2024
1 parent de9bc48 commit beb87f6
Show file tree
Hide file tree
Showing 23 changed files with 589 additions and 0 deletions.
26 changes: 26 additions & 0 deletions .riot/requirements/1337ee3.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#
# This file is autogenerated by pip-compile with Python 3.8
# by the following command:
#
# pip-compile --allow-unsafe --no-annotate .riot/requirements/1337ee3.in
#
attrs==24.2.0
azure-functions==1.21.3
certifi==2024.8.30
charset-normalizer==3.4.0
coverage[toml]==7.6.1
exceptiongroup==1.2.2
hypothesis==6.45.0
idna==3.10
iniconfig==2.0.0
mock==5.1.0
opentracing==2.4.0
packaging==24.2
pluggy==1.5.0
pytest==8.3.3
pytest-cov==5.0.0
pytest-mock==3.14.0
requests==2.32.3
sortedcontainers==2.4.0
tomli==2.1.0
urllib3==2.2.3
24 changes: 24 additions & 0 deletions .riot/requirements/14b54db.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#
# This file is autogenerated by pip-compile with Python 3.11
# by the following command:
#
# pip-compile --allow-unsafe --no-annotate .riot/requirements/14b54db.in
#
attrs==24.2.0
azure-functions==1.21.3
certifi==2024.8.30
charset-normalizer==3.4.0
coverage[toml]==7.6.8
hypothesis==6.45.0
idna==3.10
iniconfig==2.0.0
mock==5.1.0
opentracing==2.4.0
packaging==24.2
pluggy==1.5.0
pytest==8.3.3
pytest-cov==6.0.0
pytest-mock==3.14.0
requests==2.32.3
sortedcontainers==2.4.0
urllib3==2.2.3
26 changes: 26 additions & 0 deletions .riot/requirements/1e62aea.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#
# This file is autogenerated by pip-compile with Python 3.10
# by the following command:
#
# pip-compile --allow-unsafe --no-annotate .riot/requirements/1e62aea.in
#
attrs==24.2.0
azure-functions==1.21.3
certifi==2024.8.30
charset-normalizer==3.4.0
coverage[toml]==7.6.8
exceptiongroup==1.2.2
hypothesis==6.45.0
idna==3.10
iniconfig==2.0.0
mock==5.1.0
opentracing==2.4.0
packaging==24.2
pluggy==1.5.0
pytest==8.3.3
pytest-cov==6.0.0
pytest-mock==3.14.0
requests==2.32.3
sortedcontainers==2.4.0
tomli==2.1.0
urllib3==2.2.3
29 changes: 29 additions & 0 deletions .riot/requirements/73109d5.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#
# This file is autogenerated by pip-compile with Python 3.7
# by the following command:
#
# pip-compile --allow-unsafe --config=pyproject.toml --no-annotate --resolver=backtracking .riot/requirements/73109d5.in
#
attrs==24.2.0
azure-functions==1.21.3
certifi==2024.8.30
charset-normalizer==3.4.0
coverage[toml]==7.2.7
exceptiongroup==1.2.2
hypothesis==6.45.0
idna==3.10
importlib-metadata==6.7.0
iniconfig==2.0.0
mock==5.1.0
opentracing==2.4.0
packaging==24.0
pluggy==1.2.0
pytest==7.4.4
pytest-cov==4.1.0
pytest-mock==3.11.1
requests==2.31.0
sortedcontainers==2.4.0
tomli==2.0.1
typing-extensions==4.7.1
urllib3==2.0.7
zipp==3.15.0
26 changes: 26 additions & 0 deletions .riot/requirements/c2420c2.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#
# This file is autogenerated by pip-compile with Python 3.9
# by the following command:
#
# pip-compile --allow-unsafe --no-annotate .riot/requirements/c2420c2.in
#
attrs==24.2.0
azure-functions==1.21.3
certifi==2024.8.30
charset-normalizer==3.4.0
coverage[toml]==7.6.8
exceptiongroup==1.2.2
hypothesis==6.45.0
idna==3.10
iniconfig==2.0.0
mock==5.1.0
opentracing==2.4.0
packaging==24.2
pluggy==1.5.0
pytest==8.3.3
pytest-cov==6.0.0
pytest-mock==3.14.0
requests==2.32.3
sortedcontainers==2.4.0
tomli==2.1.0
urllib3==2.2.3
2 changes: 2 additions & 0 deletions ddtrace/_monkey.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
"yaaredis": True,
"asyncpg": True,
"aws_lambda": True, # patch only in AWS Lambda environments
"azure_functions": True,
"tornado": False,
"openai": True,
"langchain": True,
Expand Down Expand Up @@ -143,6 +144,7 @@
"futures": ("concurrent.futures.thread",),
"vertica": ("vertica_python",),
"aws_lambda": ("datadog_lambda",),
"azure_functions": ("azure.functions",),
"httplib": ("http.client",),
"kafka": ("confluent_kafka",),
"google_generativeai": ("google.generativeai",),
Expand Down
38 changes: 38 additions & 0 deletions ddtrace/_trace/trace_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from ddtrace.ext import http
from ddtrace.internal import core
from ddtrace.internal.compat import maybe_stringify
from ddtrace.internal.compat import parse
from ddtrace.internal.constants import COMPONENT
from ddtrace.internal.constants import FLASK_ENDPOINT
from ddtrace.internal.constants import FLASK_URL_RULE
Expand Down Expand Up @@ -675,6 +676,40 @@ def _set_span_pointer(span: "Span", span_pointer_description: _SpanPointerDescri
)


def _set_azure_function_tags(span, azure_functions_config, function_name, trigger):
span.set_tag_str(COMPONENT, azure_functions_config.integration_name)
span.set_tag_str(SPAN_KIND, SpanKind.SERVER)
span.set_tag_str("aas.function.name", function_name) # codespell:ignore
span.set_tag_str("aas.function.trigger", trigger) # codespell:ignore


def _on_azure_functions_request_span_modifier(ctx, azure_functions_config, req):
span = ctx.get_item("req_span")
parsed_url = parse.urlparse(req.url)
path = parsed_url.path
span.resource = f"{req.method} {path}"
trace_utils.set_http_meta(
span,
azure_functions_config,
method=req.method,
url=req.url,
request_headers=req.headers,
request_body=req.get_body(),
route=path,
)


def _on_azure_functions_start_response(ctx, azure_functions_config, res, function_name, trigger):
span = ctx.get_item("req_span")
_set_azure_function_tags(span, azure_functions_config, function_name, trigger)
trace_utils.set_http_meta(
span,
azure_functions_config,
status_code=res.status_code if res else None,
response_headers=res.headers if res else None,
)


def listen():
core.on("wsgi.request.prepare", _on_request_prepare)
core.on("wsgi.request.prepared", _on_request_prepared)
Expand Down Expand Up @@ -723,6 +758,8 @@ def listen():
core.on("botocore.kinesis.GetRecords.post", _on_botocore_kinesis_getrecords_post)
core.on("redis.async_command.post", _on_redis_command_post)
core.on("redis.command.post", _on_redis_command_post)
core.on("azure.functions.request_call_modifier", _on_azure_functions_request_span_modifier)
core.on("azure.functions.start_response", _on_azure_functions_start_response)

core.on("test_visibility.enable", _on_test_visibility_enable)
core.on("test_visibility.disable", _on_test_visibility_disable)
Expand Down Expand Up @@ -754,6 +791,7 @@ def listen():
"rq.worker.perform_job",
"rq.job.perform",
"rq.job.fetch_many",
"azure.functions.patched_route_request",
):
core.on(f"context.started.start_span.{context_name}", _start_span)

Expand Down
46 changes: 46 additions & 0 deletions ddtrace/contrib/azure_functions/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""
The azure_functions integration traces all http requests to your Azure Function app.
Enabling
~~~~~~~~
Use :func:`patch()<ddtrace.patch>` to manually enable the integration::
from ddtrace import patch
patch(azure_functions=True)
Global Configuration
~~~~~~~~~~~~~~~~~~~~
.. py:data:: ddtrace.config.azure_functions["service"]
The service name reported by default for azure_functions instances.
This option can also be set with the ``DD_SERVICE`` environment
variable.
Default: ``"azure_functions"``
"""

from ddtrace.internal.utils.importlib import require_modules


required_modules = ["azure.functions"]

with require_modules(required_modules) as missing_modules:
if not missing_modules:
# Required to allow users to import from `ddtrace.contrib.azure_functions.patch` directly
import warnings as _w

with _w.catch_warnings():
_w.simplefilter("ignore", DeprecationWarning)
from . import patch as _ # noqa: F401, I001

# Expose public methods
from ddtrace.contrib.internal.azure_functions.patch import get_version
from ddtrace.contrib.internal.azure_functions.patch import patch
from ddtrace.contrib.internal.azure_functions.patch import unpatch

__all__ = ["patch", "unpatch", "get_version"]
14 changes: 14 additions & 0 deletions ddtrace/contrib/azure_functions/patch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from ddtrace.contrib.internal.azure_functions.patch import * # noqa: F403
from ddtrace.internal.utils.deprecations import DDTraceDeprecationWarning
from ddtrace.vendor.debtcollector import deprecate


def __getattr__(name):
deprecate(
("%s.%s is deprecated" % (__name__, name)),
category=DDTraceDeprecationWarning,
)

if name in globals():
return globals()[name]
raise AttributeError("%s has no attribute %s", __name__, name)
85 changes: 85 additions & 0 deletions ddtrace/contrib/internal/azure_functions/patch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import azure.functions as azure_functions
from wrapt import wrap_function_wrapper as _w

from ddtrace import config
from ddtrace.contrib.trace_utils import int_service
from ddtrace.contrib.trace_utils import unwrap as _u
from ddtrace.ext import SpanTypes
from ddtrace.internal import core
from ddtrace.internal.schema import schematize_cloud_faas_operation
from ddtrace.internal.schema import schematize_service_name
from ddtrace.pin import Pin


config._add(
"azure_functions",
{
"_default_service": schematize_service_name("azure_functions"),
},
)


def get_version():
# type: () -> str
return getattr(azure_functions, "__version__", "")


def patch():
"""
Patch `azure.functions` module for tracing
"""
# Check to see if we have patched azure.functions yet or not
if getattr(azure_functions, "_datadog_patch", False):
return
azure_functions._datadog_patch = True

Pin().onto(azure_functions.FunctionApp)
_w("azure.functions", "FunctionApp.route", _patched_route)


def _patched_route(wrapped, instance, args, kwargs):
trigger = "Http"

pin = Pin.get_from(instance)
if not pin or not pin.enabled():
return wrapped(*args, **kwargs)

def _wrapper(func):
function_name = func.__name__

def wrap_function(req: azure_functions.HttpRequest, context: azure_functions.Context):
operation_name = schematize_cloud_faas_operation(
"azure.functions.invoke", cloud_provider="azure", cloud_service="functions"
)
with core.context_with_data(
"azure.functions.patched_route_request",
span_name=operation_name,
pin=pin,
service=int_service(pin, config.azure_functions),
span_type=SpanTypes.SERVERLESS,
) as ctx, ctx.span:
ctx.set_item("req_span", ctx.span)
core.dispatch("azure.functions.request_call_modifier", (ctx, config.azure_functions, req))
res = None
try:
res = func(req)
return res
finally:
core.dispatch(
"azure.functions.start_response", (ctx, config.azure_functions, res, function_name, trigger)
)

# Needed to correctly display function name when running 'func start' locally
wrap_function.__name__ = function_name

return wrapped(*args, **kwargs)(wrap_function)

return _wrapper


def unpatch():
if not getattr(azure_functions, "_datadog_patch", False):
return
azure_functions._datadog_patch = False

_u(azure_functions.FunctionApp, "route")
1 change: 1 addition & 0 deletions ddtrace/ext/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class SpanTypes(object):
HTTP = "http"
MONGODB = "mongodb"
REDIS = "redis"
SERVERLESS = "serverless"
SQL = "sql"
TEMPLATE = "template"
TEST = "test"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
features:
- |
azure_functions: This introduces support for Azure Functions.
9 changes: 9 additions & 0 deletions riotfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -2837,6 +2837,15 @@ def select_pys(min_version: str = MIN_PYTHON_VERSION, max_version: str = MAX_PYT
"envier": "==0.5.2",
},
),
Venv(
name="azure_functions",
command="pytest {cmdargs} tests/contrib/azure_functions",
pys=select_pys(min_version="3.7", max_version="3.11"),
pkgs={
"azure.functions": latest,
"requests": latest,
},
),
Venv(
name="sourcecode",
command="pytest {cmdargs} tests/sourcecode",
Expand Down
Empty file.
Loading

0 comments on commit beb87f6

Please sign in to comment.