Skip to content
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

feat(azure_functions): add azure functions integration #11474

Merged
merged 20 commits into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
856e062
add azure_functions integration
duncanpharvey Nov 14, 2024
cdecdaa
add unit tests for azure_functions integration
duncanpharvey Nov 25, 2024
547dc32
add release note for azure functions integration
duncanpharvey Nov 26, 2024
cc5b4a7
fix failing mariadb install in testrunner
duncanpharvey Nov 26, 2024
4ae77b7
update testrunner image
duncanpharvey Nov 27, 2024
d6a3ae4
Merge branch 'main' into duncan-harvey/azure-functions-integration
duncanpharvey Nov 27, 2024
ac03dfd
configure suitespec for azure_functions integration
duncanpharvey Nov 27, 2024
886bff1
refactor azure_functions integration to not reference contrib directly
duncanpharvey Dec 4, 2024
016964d
add more tests for azure_functions integration
duncanpharvey Dec 4, 2024
cf45184
ignore error stack for azure_functions tests
duncanpharvey Dec 4, 2024
9ca0401
Merge branch 'main' into duncan-harvey/azure-functions-integration
duncanpharvey Dec 5, 2024
c612746
remove circleci test config and fix gitlab test config
duncanpharvey Dec 5, 2024
99e1ef8
remove dockerfile change
duncanpharvey Dec 5, 2024
6e7563c
configure client max tries and delay for azure functions integration …
duncanpharvey Dec 5, 2024
7f70d02
Revert "configure client max tries and delay for azure functions inte…
duncanpharvey Dec 5, 2024
b42b0bd
set stdout and stderr to DEVNULL for azure functions integration tests
duncanpharvey Dec 9, 2024
04c98f4
Merge branch 'main' into duncan-harvey/azure-functions-integration
duncanpharvey Dec 9, 2024
ea946e0
add delay for connecting to azure functions test client
duncanpharvey Dec 13, 2024
5c1bb4e
simulate azure functions environment so default service is set automa…
duncanpharvey Dec 13, 2024
bf3baa9
Merge branch 'main' into duncan-harvey/azure-functions-integration
duncanpharvey Dec 18, 2024
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
9 changes: 8 additions & 1 deletion .circleci/config.templ.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ default_resource_class: &default_resource_class medium
ubuntu_base_image: &ubuntu_base_img ubuntu-2004:2023.04.2
cimg_base_image: &cimg_base_image cimg/base:2022.08
python310_image: &python310_image cimg/python:3.10.12
ddtrace_dev_image: &ddtrace_dev_image ghcr.io/datadog/dd-trace-py/testrunner@sha256:8ca43d46ff34e078bd7bc0662e74e6be38547a98140a5cd4203805f6b214b583
ddtrace_dev_image: &ddtrace_dev_image ghcr.io/datadog/dd-trace-py/testrunner@sha256:1a76ac6a5f398d9fcca77468aed92b5f12e875e31370c628f7ceacd4246727c6
redis_image: &redis_image redis:4.0-alpine@sha256:3e99741f293147ff406657dda7644c2b88564b80a498cd00da8f905743449c9f
memcached_image: &memcached_image memcached:1.5-alpine@sha256:48cb7207e3d34871893fa1628f3a4984375153e9942facf82e25935b0a633c8a
cassandra_image: &cassandra_image cassandra:3.11.7@sha256:495e5752526f7e75d3ad85b6a6bbf3b79714321b17a44255a216c341e3baae11
Expand Down Expand Up @@ -434,6 +434,13 @@ jobs:
pattern: 'aws_lambda'
snapshot: true

azure_functions:
duncanpharvey marked this conversation as resolved.
Show resolved Hide resolved
<<: *machine_executor
steps:
- run_test:
pattern: 'azure_functions'
snapshot: true

datastreams:
<<: *contrib_job_small
steps:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/requirements-locks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
validate:
name: Check requirements lockfiles
runs-on: ubuntu-latest
container: ghcr.io/datadog/dd-trace-py/testrunner@sha256:8ca43d46ff34e078bd7bc0662e74e6be38547a98140a5cd4203805f6b214b583
container: ghcr.io/datadog/dd-trace-py/testrunner@sha256:1a76ac6a5f398d9fcca77468aed92b5f12e875e31370c628f7ceacd4246727c6
steps:
- uses: actions/checkout@v4
with:
Expand Down
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 @@ -93,6 +93,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 @@ -141,6 +142,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)
wantsui marked this conversation as resolved.
Show resolved Hide resolved
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):
duncanpharvey marked this conversation as resolved.
Show resolved Hide resolved
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),
wconti27 marked this conversation as resolved.
Show resolved Hide resolved
duncanpharvey marked this conversation as resolved.
Show resolved Hide resolved
span_type=SpanTypes.SERVERLESS,
) as ctx, ctx.span:
ctx.set_item("req_span", ctx.span)
duncanpharvey marked this conversation as resolved.
Show resolved Hide resolved
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")
Loading
Loading