Skip to content

Commit bc56c53

Browse files
fix(azure_functions): add await so async functions are instrumented without errors (#13280)
Add await for async Azure Functions to prevent errors on instrumentation. https://datadoghq.atlassian.net/browse/SVLS-6703 <img width="1075" alt="Screenshot 2025-04-24 at 8 58 00 PM" src="https://github.com/user-attachments/assets/9567fe6a-12ee-4b15-9509-78fe7b5bf3e5" /> ### Additional Notes: There is an opportunity for some refactoring in order to better reuse code. In the interest of getting this fix out as quickly as possible I plan on handling that refactor in a separate PR. ## 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)
1 parent 6c9d614 commit bc56c53

8 files changed

+201
-0
lines changed

ddtrace/contrib/internal/azure_functions/patch.py

+34
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import functools
2+
import inspect
23

34
import azure.functions as azure_functions
45
from wrapt import wrap_function_wrapper as _w
@@ -57,6 +58,25 @@ def _patched_route(wrapped, instance, args, kwargs):
5758
def _wrapper(func):
5859
function_name = get_function_name(pin, instance, func)
5960

61+
if inspect.iscoroutinefunction(func):
62+
63+
@functools.wraps(func)
64+
async def async_wrap_function(*args, **kwargs):
65+
req = kwargs.get(trigger_arg_name)
66+
with create_context("azure.functions.patched_route_request", pin) as ctx, ctx.span:
67+
ctx.set_item("req_span", ctx.span)
68+
core.dispatch("azure.functions.request_call_modifier", (ctx, config.azure_functions, req))
69+
res = None
70+
try:
71+
res = await func(*args, **kwargs)
72+
return res
73+
finally:
74+
core.dispatch(
75+
"azure.functions.start_response", (ctx, config.azure_functions, res, function_name, trigger)
76+
)
77+
78+
return wrapped(*args, **kwargs)(async_wrap_function)
79+
6080
@functools.wraps(func)
6181
def wrap_function(*args, **kwargs):
6282
req = kwargs.get(trigger_arg_name)
@@ -88,6 +108,20 @@ def _wrapper(func):
88108
function_name = get_function_name(pin, instance, func)
89109
resource_name = f"{trigger} {function_name}"
90110

111+
if inspect.iscoroutinefunction(func):
112+
113+
@functools.wraps(func)
114+
async def async_wrap_function(*args, **kwargs):
115+
with create_context("azure.functions.patched_timer", pin, resource_name) as ctx, ctx.span:
116+
ctx.set_item("trigger_span", ctx.span)
117+
core.dispatch(
118+
"azure.functions.trigger_call_modifier",
119+
(ctx, config.azure_functions, function_name, trigger),
120+
)
121+
await func(*args, **kwargs)
122+
123+
return wrapped(*args, **kwargs)(async_wrap_function)
124+
91125
@functools.wraps(func)
92126
def wrap_function(*args, **kwargs):
93127
with create_context("azure.functions.patched_timer", pin, resource_name) as ctx, ctx.span:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
fixes:
2+
- |
3+
azure_functions: This fix resolves an issue where async functions throw an error when instrumented.

tests/contrib/azure_functions/azure_function_app/function_app.py

+10
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ def http_get_ok(req: func.HttpRequest) -> func.HttpResponse:
1414
return func.HttpResponse("Hello Datadog!")
1515

1616

17+
@app.route(route="httpgetokasync", auth_level=func.AuthLevel.ANONYMOUS, methods=[func.HttpMethod.GET])
18+
async def http_get_ok_async(req: func.HttpRequest) -> func.HttpResponse:
19+
return func.HttpResponse("Hello Datadog!")
20+
21+
1722
@app.route(route="httpgeterror", auth_level=func.AuthLevel.ANONYMOUS, methods=[func.HttpMethod.GET])
1823
def http_get_error(req: func.HttpRequest) -> func.HttpResponse:
1924
raise Exception("Test Error")
@@ -48,3 +53,8 @@ def http_get_function_name_no_decorator(req: func.HttpRequest) -> func.HttpRespo
4853
@app.timer_trigger(schedule="0 0 0 1 1 *", arg_name="timer")
4954
def timer(timer: func.TimerRequest) -> None:
5055
pass
56+
57+
58+
@app.timer_trigger(schedule="0 0 0 1 1 *", arg_name="timer")
59+
async def timer_async(timer: func.TimerRequest) -> None:
60+
pass

tests/contrib/azure_functions/test_azure_functions_snapshot.py

+27
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,21 @@ def test_http_get_ok(azure_functions_client: Client) -> None:
5353
assert azure_functions_client.get("/api/httpgetok?key=val", headers=DEFAULT_HEADERS).status_code == 200
5454

5555

56+
@pytest.mark.snapshot
57+
def test_http_get_ok_async(azure_functions_client: Client) -> None:
58+
assert azure_functions_client.get("/api/httpgetokasync?key=val", headers=DEFAULT_HEADERS).status_code == 200
59+
60+
61+
@pytest.mark.snapshot
62+
def test_http_get_ok_obfuscated(azure_functions_client: Client) -> None:
63+
assert azure_functions_client.get("/api/httpgetok?secret=val", headers=DEFAULT_HEADERS).status_code == 200
64+
65+
66+
@pytest.mark.snapshot
67+
def test_http_get_ok_async_obfuscated(azure_functions_client: Client) -> None:
68+
assert azure_functions_client.get("/api/httpgetokasync?secret=val", headers=DEFAULT_HEADERS).status_code == 200
69+
70+
5671
@pytest.mark.snapshot(ignores=["meta.error.stack"])
5772
def test_http_get_error(azure_functions_client: Client) -> None:
5873
assert azure_functions_client.get("/api/httpgeterror", headers=DEFAULT_HEADERS).status_code == 500
@@ -90,3 +105,15 @@ def test_timer(azure_functions_client: Client) -> None:
90105
).status_code
91106
== 202
92107
)
108+
109+
110+
@pytest.mark.snapshot
111+
def test_timer_async(azure_functions_client: Client) -> None:
112+
assert (
113+
azure_functions_client.post(
114+
"/admin/functions/timer_async",
115+
headers={"User-Agent": "python-httpx/x.xx.x", "Content-Type": "application/json"},
116+
data=json.dumps({"input": None}),
117+
).status_code
118+
== 202
119+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
[[
2+
{
3+
"name": "azure.functions.invoke",
4+
"service": "test-func",
5+
"resource": "GET /api/httpgetokasync",
6+
"trace_id": 0,
7+
"span_id": 1,
8+
"parent_id": 0,
9+
"type": "serverless",
10+
"meta": {
11+
"_dd.p.dm": "-0",
12+
"_dd.p.tid": "680bbffe00000000",
13+
"aas.function.name": "http_get_ok_async",
14+
"aas.function.trigger": "Http",
15+
"component": "azure_functions",
16+
"http.method": "GET",
17+
"http.route": "/api/httpgetokasync",
18+
"http.status_code": "200",
19+
"http.url": "http://0.0.0.0:7071/api/httpgetokasync?key=val",
20+
"http.useragent": "python-httpx/x.xx.x",
21+
"language": "python",
22+
"runtime-id": "dc1bd0201db346a887fac16721f3b0e5",
23+
"span.kind": "server"
24+
},
25+
"metrics": {
26+
"_dd.top_level": 1,
27+
"_dd.tracer_kr": 1.0,
28+
"_sampling_priority_v1": 1,
29+
"process_id": 17518
30+
},
31+
"duration": 794292,
32+
"start": 1745600510630485377
33+
}]]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
[[
2+
{
3+
"name": "azure.functions.invoke",
4+
"service": "test-func",
5+
"resource": "GET /api/httpgetokasync",
6+
"trace_id": 0,
7+
"span_id": 1,
8+
"parent_id": 0,
9+
"type": "serverless",
10+
"meta": {
11+
"_dd.p.dm": "-0",
12+
"_dd.p.tid": "68135c0400000000",
13+
"aas.function.name": "http_get_ok_async",
14+
"aas.function.trigger": "Http",
15+
"component": "azure_functions",
16+
"http.method": "GET",
17+
"http.route": "/api/httpgetokasync",
18+
"http.status_code": "200",
19+
"http.url": "http://0.0.0.0:7071/api/httpgetokasync?<redacted>",
20+
"http.useragent": "python-httpx/x.xx.x",
21+
"language": "python",
22+
"runtime-id": "7edd9974ae834236be55679427d26dd1",
23+
"span.kind": "server"
24+
},
25+
"metrics": {
26+
"_dd.top_level": 1,
27+
"_dd.tracer_kr": 1.0,
28+
"_sampling_priority_v1": 1,
29+
"process_id": 6118
30+
},
31+
"duration": 744750,
32+
"start": 1746099204097478012
33+
}]]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
[[
2+
{
3+
"name": "azure.functions.invoke",
4+
"service": "test-func",
5+
"resource": "GET /api/httpgetok",
6+
"trace_id": 0,
7+
"span_id": 1,
8+
"parent_id": 0,
9+
"type": "serverless",
10+
"meta": {
11+
"_dd.p.dm": "-0",
12+
"_dd.p.tid": "68135be100000000",
13+
"aas.function.name": "http_get_ok",
14+
"aas.function.trigger": "Http",
15+
"component": "azure_functions",
16+
"http.method": "GET",
17+
"http.route": "/api/httpgetok",
18+
"http.status_code": "200",
19+
"http.url": "http://0.0.0.0:7071/api/httpgetok?<redacted>",
20+
"http.useragent": "python-httpx/x.xx.x",
21+
"language": "python",
22+
"runtime-id": "bebb19bc0290454d80b19e0ee5a4546e",
23+
"span.kind": "server"
24+
},
25+
"metrics": {
26+
"_dd.top_level": 1,
27+
"_dd.tracer_kr": 1.0,
28+
"_sampling_priority_v1": 1,
29+
"process_id": 3676
30+
},
31+
"duration": 902208,
32+
"start": 1746099169732256385
33+
}]]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
[[
2+
{
3+
"name": "azure.functions.invoke",
4+
"service": "test-func",
5+
"resource": "Timer timer_async",
6+
"trace_id": 0,
7+
"span_id": 1,
8+
"parent_id": 0,
9+
"type": "serverless",
10+
"meta": {
11+
"_dd.p.dm": "-0",
12+
"_dd.p.tid": "680bbf7f00000000",
13+
"aas.function.name": "timer_async",
14+
"aas.function.trigger": "Timer",
15+
"component": "azure_functions",
16+
"language": "python",
17+
"runtime-id": "60d7e0785b414408a3dafc9fb92d939e",
18+
"span.kind": "internal"
19+
},
20+
"metrics": {
21+
"_dd.top_level": 1,
22+
"_dd.tracer_kr": 1.0,
23+
"_sampling_priority_v1": 1,
24+
"process_id": 7776
25+
},
26+
"duration": 193000,
27+
"start": 1745600383620687347
28+
}]]

0 commit comments

Comments
 (0)