Skip to content

Commit 72a161c

Browse files
andystaplesCopilot
andcommitted
Add Functions host logging E2E coverage
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ce0bfcf6-4e74-4a28-b802-45c98f3af2ef
1 parent c191f6c commit 72a161c

5 files changed

Lines changed: 105 additions & 1 deletion

File tree

tests/azure-functions-durable/e2e/_harness.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,23 @@ def _read_log(self) -> str:
411411
except OSError:
412412
return "(no host log captured)"
413413

414+
def read_host_log(self) -> str:
415+
"""Return the Functions host output captured for this app."""
416+
return self._read_log()
417+
418+
def wait_for_host_log(self, text: str, timeout: float = 30) -> str:
419+
"""Wait until captured Functions host output contains ``text``."""
420+
deadline = time.time() + timeout
421+
log = self._read_log()
422+
while text not in log and time.time() < deadline:
423+
time.sleep(0.1)
424+
log = self._read_log()
425+
if text not in log:
426+
raise TimeoutError(
427+
f"Functions host log did not contain {text!r} within {timeout}s.\n"
428+
f"{log}")
429+
return next(line for line in log.splitlines() if text in line)
430+
414431
def stop(self) -> None:
415432
proc = self._process
416433
if proc is not None:

tests/azure-functions-durable/e2e/apps/dtask_style/client_routes.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
import asyncio
1717
import json
18+
import logging
1819
from datetime import datetime, timedelta, timezone
1920
from typing import Any
2021

@@ -32,6 +33,7 @@
3233
)
3334

3435
bp = df.Blueprint()
36+
_LOGGER = logging.getLogger(__name__)
3537

3638

3739
def _sync_client(client: df.DurableFunctionsClient) -> TaskHubGrpcClient:
@@ -91,6 +93,19 @@ async def start_orchestration(
9193
json.dumps({"id": instance_id}), status_code=202, mimetype="application/json")
9294

9395

96+
@bp.route(route="start-logging-filtered/{name}", methods=["POST"])
97+
@bp.durable_client_input(client_name="client")
98+
async def start_logging_filtered(
99+
req: func.HttpRequest, client: df.DurableFunctionsClient) -> func.HttpResponse:
100+
name = req.route_params["name"]
101+
body = req.get_json()
102+
instance_id = await client.schedule_new_orchestration(name, input=body.get("input"))
103+
_LOGGER.info("client-info-filter-anchor %s", instance_id)
104+
_LOGGER.warning("client-filter-anchor %s", instance_id)
105+
return func.HttpResponse(
106+
json.dumps({"id": instance_id}), status_code=202, mimetype="application/json")
107+
108+
94109
@bp.route(route="status/{id}", methods=["GET"])
95110
@bp.durable_client_input(client_name="client")
96111
async def get_orchestration_status(

tests/azure-functions-durable/e2e/apps/dtask_style/host.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
"version": "2.0",
33
"logging": {
44
"logLevel": {
5-
"default": "Information"
5+
"default": "Information",
6+
"Function.start_logging_filtered.User": "Warning",
7+
"Function.logging_filtered.User": "Warning"
68
}
79
},
810
"extensionBundle": {

tests/azure-functions-durable/e2e/apps/dtask_style/orchestrators.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@
1313
"""
1414

1515
from datetime import timedelta
16+
import logging
1617
from typing import Any
1718

1819
import azure.durable_functions as df
1920
from durabletask import entities, task
2021

2122
bp = df.Blueprint()
23+
_LOGGER = logging.getLogger(__name__)
2224

2325

2426
# ---------------------------------------------------------------------------
@@ -33,6 +35,13 @@ def activity_chain(ctx: task.OrchestrationContext, _: Any):
3335
return [first, second, third]
3436

3537

38+
@bp.orchestration_trigger(context_name="context")
39+
def logging_filtered(ctx: task.OrchestrationContext, _: Any):
40+
_LOGGER.info("worker-info-filter-anchor %s", ctx.instance_id)
41+
_LOGGER.warning("worker-filter-anchor %s", ctx.instance_id)
42+
return "logged"
43+
44+
3645
@bp.orchestration_trigger(context_name="context")
3746
def fan_out_fan_in(ctx: task.OrchestrationContext, count: Any):
3847
count = count or 5
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
"""E2E validation for Durable Functions SDK logging."""
5+
6+
import re
7+
8+
import pytest
9+
10+
from ._harness import http_request
11+
12+
pytestmark = pytest.mark.functions_e2e
13+
14+
15+
def _assert_host_routed(line: str, message: str) -> None:
16+
assert re.fullmatch(rf"\[[^\]]+Z\] {re.escape(message)}", line)
17+
18+
19+
def test_client_and_worker_logs_flow_through_host(dtask_app):
20+
instance_id = dtask_app.start_orchestration("activity_chain")
21+
status = dtask_app.wait_for_completion(instance_id)
22+
assert status["runtimeStatus"] == "COMPLETED"
23+
24+
client_message = (
25+
f"Starting new 'activity_chain' instance with ID = '{instance_id}'.")
26+
worker_message = (
27+
f"{instance_id}: Orchestration activity_chain completed with status: COMPLETED")
28+
client_line = dtask_app.wait_for_host_log(client_message)
29+
worker_line = dtask_app.wait_for_host_log(worker_message)
30+
31+
_assert_host_routed(client_line, client_message)
32+
_assert_host_routed(worker_line, worker_message)
33+
34+
35+
def test_host_category_filter_applies_to_client_and_worker_logs(dtask_app):
36+
result = http_request(
37+
"POST",
38+
f"{dtask_app.base_url}/api/start-logging-filtered/logging_filtered",
39+
data={"input": None},
40+
)
41+
assert result.status == 202
42+
instance_id = result.json()["id"]
43+
44+
status = dtask_app.wait_for_completion(instance_id)
45+
assert status["runtimeStatus"] == "COMPLETED"
46+
assert status["output"] == "logged"
47+
48+
dtask_app.wait_for_host_log(f"client-filter-anchor {instance_id}")
49+
dtask_app.wait_for_host_log(f"worker-filter-anchor {instance_id}")
50+
log = dtask_app.read_host_log()
51+
52+
assert f"client-info-filter-anchor {instance_id}" not in log
53+
assert f"worker-info-filter-anchor {instance_id}" not in log
54+
assert (
55+
f"Starting new 'logging_filtered' instance with ID = '{instance_id}'."
56+
not in log
57+
)
58+
assert (
59+
f"{instance_id}: Orchestration logging_filtered completed with status: COMPLETED"
60+
not in log
61+
)

0 commit comments

Comments
 (0)