Skip to content

Commit cc1513a

Browse files
andystaplesCopilot
andauthored
Add caller-provided logger configuration (#252)
* Add caller-provided loggers Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d3b014e3-808e-455f-b932-01d0a7aa5c93 * Document logger configuration patterns Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d3b014e3-808e-455f-b932-01d0a7aa5c93 * Clarify logger option conflicts Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d3b014e3-808e-455f-b932-01d0a7aa5c93 * Use host-managed Functions loggers Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d3b014e3-808e-455f-b932-01d0a7aa5c93 * Add Functions host logging E2E coverage Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ce0bfcf6-4e74-4a28-b802-45c98f3af2ef * Address logger review feedback Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d3b014e3-808e-455f-b932-01d0a7aa5c93 * Select matching logging warnings in tests Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d3b014e3-808e-455f-b932-01d0a7aa5c93 * Address logger review findings Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d3b014e3-808e-455f-b932-01d0a7aa5c93 --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d3b014e3-808e-455f-b932-01d0a7aa5c93 Copilot-Session: ce0bfcf6-4e74-4a28-b802-45c98f3af2ef
1 parent 6d99f9f commit cc1513a

19 files changed

Lines changed: 513 additions & 49 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,15 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
99

1010
ADDED
1111

12+
- Added a `logger` parameter to `TaskHubGrpcClient`,
13+
`AsyncTaskHubGrpcClient`, and `TaskHubGrpcWorker`. Applications can now
14+
provide fully configured loggers without the SDK modifying their handlers,
15+
formatter, level, filters, or propagation.
1216
- Added `ExceptionPropertiesProvider` and the
1317
`exception_properties_provider` worker option to attach portable custom
14-
properties to activity, entity, and orchestration failures. Failure
15-
properties and recursive inner failures are available through
16-
`FailureDetails`, task-failure exceptions, orchestration state, and history.
18+
properties to activity, entity, and orchestration failures. Failure properties
19+
and recursive inner failures are available through `FailureDetails`,
20+
task-failure exceptions, orchestration state, and history.
1721
- Added an optional timeout to filtered orchestration purges. Callers can now
1822
limit a purge operation's duration and inspect `PurgeInstancesResult.is_complete`
1923
for completed, partial, or backend-unknown completion status.
@@ -23,8 +27,15 @@ instances whose IDs begin with a specified prefix.
2327
`AsyncTaskHubGrpcClient` suspend and resume operations. The reason is now sent
2428
to the backend with the lifecycle request.
2529
- Added the optional `new_version` argument to
26-
`OrchestrationContext.continue_as_new()` so continued orchestrations can
27-
switch to a new version.
30+
`OrchestrationContext.continue_as_new()` so continued orchestrations can switch
31+
to a new version.
32+
33+
CHANGED
34+
35+
- Deprecated the `log_handler` and `log_formatter` parameters on
36+
`TaskHubGrpcClient`, `AsyncTaskHubGrpcClient`, and `TaskHubGrpcWorker`.
37+
Configure and pass a `logger` instead. These parameters will be removed in a
38+
future major release.
2839

2940
## v1.9.0
3041

‎azure-functions-durable/CHANGELOG.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ FIXED
2424
discarding their `reason` arguments. Reasons are now forwarded to the Durable
2525
Task backend.
2626

27+
CHANGED
28+
29+
- Durable Functions client and worker logs now use the Azure Functions
30+
host-managed `azure.durable_functions` logger hierarchy instead of private SDK
31+
handlers. Configure their level, destination, and telemetry routing through the
32+
standard Functions/Python logging configuration.
33+
2734
## v2.0.0b2
2835

2936
ADDED

‎azure-functions-durable/azure/durable_functions/client.py‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import asyncio
55
import atexit
66
import json
7+
import logging
78
import threading
89

910
from datetime import datetime, timedelta
@@ -35,6 +36,7 @@
3536

3637
_sync_client_cache: dict[str, "SyncDurableFunctionsClient"] = {}
3738
_sync_client_cache_lock = threading.Lock()
39+
_LOGGER = logging.getLogger(__name__)
3840

3941

4042
def _first_forwarded_value(value: str) -> str:
@@ -176,7 +178,8 @@ def __init__(self, client_as_string: str):
176178
interceptors=interceptors,
177179
channel_options=channel_options,
178180
data_converter=DEFAULT_FUNCTIONS_DATA_CONVERTER,
179-
emit_trace_spans=False)
181+
emit_trace_spans=False,
182+
logger=_LOGGER)
180183

181184
# The gRPC aio channel is bound to the event loop it is created on. A
182185
# ``durable_client_input`` decode runs on the worker's invocation loop,
@@ -661,7 +664,8 @@ def __init__(self, client_as_string: str):
661664
interceptors=interceptors,
662665
channel_options=channel_options,
663666
data_converter=DEFAULT_FUNCTIONS_DATA_CONVERTER,
664-
emit_trace_spans=False)
667+
emit_trace_spans=False,
668+
logger=_LOGGER)
665669

666670
@classmethod
667671
def get_cached(cls, client_as_string: str) -> "SyncDurableFunctionsClient":

‎azure-functions-durable/azure/durable_functions/worker.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# Licensed under the MIT License.
33

44
import base64
5+
import logging
56
from threading import Lock
67
from typing import Any, Optional
78

@@ -19,6 +20,8 @@
1920
from .internal.compat.orchestration_context import wrap_orchestrator
2021
from .internal.serialization import DEFAULT_FUNCTIONS_DATA_CONVERTER
2122

23+
_LOGGER = logging.getLogger(__name__)
24+
2225

2326
# Host-driven Azure Functions execution engine for orchestrator and entity
2427
# functions. Despite subclassing ``TaskHubGrpcWorker``, it never opens a gRPC
@@ -44,6 +47,7 @@ def __init__(self) -> None:
4447
super().__init__(
4548
data_converter=DEFAULT_FUNCTIONS_DATA_CONVERTER,
4649
emit_trace_spans=False,
50+
logger=_LOGGER,
4751
)
4852
self._registration_lock = Lock()
4953
self._registered_orchestrator_functions: dict[

‎docs/features.md‎

Lines changed: 71 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,77 @@ message formatting.
268268
> Create the replay-safe logger once at the start of your orchestrator
269269
> and reuse it throughout the function.
270270

271+
### SDK logging
272+
273+
`TaskHubGrpcClient`, `AsyncTaskHubGrpcClient`, and `TaskHubGrpcWorker`
274+
accept a configured `logging.Logger` through their `logger` parameter. The
275+
SDK uses a supplied logger unchanged: it does not add handlers or modify its
276+
formatter, level, filters, or propagation setting. The Durable Task Scheduler
277+
client and worker accept the same parameter.
278+
279+
Use one shared logger when client and worker records should use the same
280+
destination, format, and correlation filters:
281+
282+
```python
283+
import logging
284+
285+
from durabletask.client import TaskHubGrpcClient
286+
from durabletask.worker import TaskHubGrpcWorker
287+
288+
sdk_logger = logging.getLogger("myapp.durabletask")
289+
sdk_logger.setLevel(logging.INFO)
290+
291+
handler = logging.StreamHandler()
292+
handler.setFormatter(logging.Formatter(
293+
"%(asctime)s %(name)s %(levelname)s %(message)s"))
294+
sdk_logger.addHandler(handler)
295+
sdk_logger.propagate = False
296+
297+
worker = TaskHubGrpcWorker(logger=sdk_logger)
298+
client = TaskHubGrpcClient(logger=sdk_logger)
299+
```
300+
301+
Because `logging.getLogger()` returns the same logger for a given name,
302+
configure a shared logger once during application startup. Do not add a new
303+
handler every time a client or worker is constructed, or every record will be
304+
emitted once by each handler.
305+
306+
Alternatively, configure handlers on the root logger and allow propagation:
307+
308+
```python
309+
import logging
310+
311+
logging.basicConfig(level=logging.INFO)
312+
sdk_logger = logging.getLogger("myapp.durabletask")
313+
314+
worker = TaskHubGrpcWorker(logger=sdk_logger)
315+
client = TaskHubGrpcClient(logger=sdk_logger)
316+
```
317+
318+
> [!WARNING]
319+
> Use either handlers attached directly to `sdk_logger` with
320+
> `sdk_logger.propagate = False`, or root handlers with propagation enabled.
321+
> Combining both emits each Durable Task record twice.
322+
323+
Pass separate named loggers when clients and workers need independently
324+
configured levels, destinations, or filters:
325+
326+
```python
327+
worker = TaskHubGrpcWorker(
328+
logger=logging.getLogger("myapp.durabletask.worker"))
329+
client = TaskHubGrpcClient(
330+
logger=logging.getLogger("myapp.durabletask.client"))
331+
```
332+
333+
The older `log_handler` and `log_formatter` parameters are deprecated. They
334+
will be removed in a future major release. Passing either one together with
335+
`logger` raises `ValueError`; none of the supplied logging options are ignored.
336+
337+
> [!NOTE]
338+
> SDK clients and workers emit detailed `DEBUG` logs that can help diagnose
339+
> orchestration behavior. Enable that level on the logger you pass to the SDK
340+
> when reproducing an issue.
341+
271342
### Large payload externalization
272343

273344
Orchestration inputs, outputs, and event data are transmitted through gRPC messages. When these
@@ -571,34 +642,6 @@ export_client = ExportHistoryClient(
571642
> first-party destinations (S3, GCS, etc.) will be packaged as
572643
> additional optional extras using the same pattern.
573644

574-
### Logging configuration
575-
576-
Both the TaskHubGrpcWorker and TaskHubGrpcClient (as well as DurableTaskSchedulerWorker and
577-
DurableTaskSchedulerClient for durabletask-azuremanaged) accept a log_handler and log_formatter
578-
object from `logging`. These can be used to customize verbosity, output location, and format of logs
579-
emitted by these sources.
580-
581-
For example, to output logs to a file called `worker.log` at level `DEBUG`, the following syntax
582-
might apply:
583-
584-
```python
585-
log_handler = logging.FileHandler('durable.log', encoding='utf-8')
586-
log_handler.setLevel(logging.DEBUG)
587-
588-
with DurableTaskSchedulerWorker(
589-
host_address=endpoint,
590-
secure_channel=secure_channel,
591-
taskhub=taskhub_name,
592-
token_credential=credential,
593-
log_handler=log_handler,
594-
) as w:
595-
```
596-
597-
> [!NOTE]
598-
> The worker and client output many logs at the `DEBUG` level that will be useful when understanding
599-
orchestration flow and diagnosing issues with Durable applications. Before submitting issues, please
600-
attempt a repro of the issue with debug logging enabled.
601-
602645
### Work item filtering
603646

604647
By default a worker receives **all** work items from the backend, regardless of which

‎durabletask-azuremanaged/CHANGELOG.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
99

1010
ADDED
1111

12+
- Added a `logger` parameter to `DurableTaskSchedulerClient`,
13+
`AsyncDurableTaskSchedulerClient`, and `DurableTaskSchedulerWorker`.
14+
Applications can now provide fully configured loggers without the SDK modifying
15+
them.
1216
- Added `exception_properties_provider` to `DurableTaskSchedulerWorker` for
1317
attaching portable custom properties to activity, entity, and orchestration
1418
failures.
@@ -19,6 +23,13 @@ orchestration instances whose IDs begin with a specified prefix.
1923
- Added optional `reason` parameters to suspend and resume operations inherited
2024
from `TaskHubGrpcClient` and `AsyncTaskHubGrpcClient`.
2125

26+
CHANGED
27+
28+
- Deprecated the `log_handler` and `log_formatter` parameters on
29+
`DurableTaskSchedulerClient`, `AsyncDurableTaskSchedulerClient`, and
30+
`DurableTaskSchedulerWorker`. Configure and pass a `logger` instead. These
31+
parameters will be removed in a future major release.
32+
2233
## v1.9.0
2334

2435
CHANGED

‎durabletask-azuremanaged/durabletask/azuremanaged/client.py‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ def __init__(self, *,
3838
payload_store: PayloadStore | None = None,
3939
data_converter: DataConverter | None = None,
4040
log_handler: logging.Handler | None = None,
41-
log_formatter: logging.Formatter | None = None):
41+
log_formatter: logging.Formatter | None = None,
42+
logger: logging.Logger | None = None):
4243

4344
if not taskhub:
4445
raise ValueError("Taskhub value cannot be empty. Please provide a value for your taskhub")
@@ -57,6 +58,7 @@ def __init__(self, *,
5758
metadata=None,
5859
log_handler=log_handler,
5960
log_formatter=log_formatter,
61+
logger=logger,
6062
interceptors=resolved_interceptors,
6163
channel_options=channel_options,
6264
resiliency_options=resiliency_options,
@@ -86,8 +88,13 @@ class AsyncDurableTaskSchedulerClient(AsyncTaskHubGrpcClient):
8688
default_version (str | None, optional): Default version string for orchestrations.
8789
payload_store (PayloadStore | None, optional): A payload store for
8890
externalizing large payloads. If None, payloads are sent inline.
89-
log_handler (logging.Handler | None, optional): Custom logging handler for client logs.
90-
log_formatter (logging.Formatter | None, optional): Custom log formatter for client logs.
91+
log_handler (logging.Handler | None, optional): Deprecated custom logging
92+
handler for client logs. Use ``logger`` instead.
93+
log_formatter (logging.Formatter | None, optional): Deprecated custom log
94+
formatter. Use ``logger`` instead.
95+
logger (logging.Logger | None, optional): Caller-configured logger for
96+
client logs. It cannot be combined with ``log_handler`` or
97+
``log_formatter``; doing so raises ``ValueError``.
9198
9299
Raises:
93100
ValueError: If taskhub is empty or None.
@@ -118,7 +125,8 @@ def __init__(self, *,
118125
payload_store: PayloadStore | None = None,
119126
data_converter: DataConverter | None = None,
120127
log_handler: logging.Handler | None = None,
121-
log_formatter: logging.Formatter | None = None):
128+
log_formatter: logging.Formatter | None = None,
129+
logger: logging.Logger | None = None):
122130

123131
if not taskhub:
124132
raise ValueError("Taskhub value cannot be empty. Please provide a value for your taskhub")
@@ -137,6 +145,7 @@ def __init__(self, *,
137145
metadata=None,
138146
log_handler=log_handler,
139147
log_formatter=log_formatter,
148+
logger=logger,
140149
interceptors=resolved_interceptors,
141150
channel_options=channel_options,
142151
resiliency_options=resiliency_options,

‎durabletask-azuremanaged/durabletask/azuremanaged/worker.py‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,13 @@ class DurableTaskSchedulerWorker(TaskHubGrpcWorker):
4848
settings will be used.
4949
payload_store (PayloadStore | None, optional): A payload store for
5050
externalizing large payloads. If None, payloads are sent inline.
51-
log_handler (logging.Handler | None, optional): Custom logging handler for worker logs.
52-
log_formatter (logging.Formatter | None, optional): Custom log formatter for worker logs.
51+
log_handler (logging.Handler | None, optional): Deprecated custom logging
52+
handler for worker logs. Use ``logger`` instead.
53+
log_formatter (logging.Formatter | None, optional): Deprecated custom log
54+
formatter. Use ``logger`` instead.
55+
logger (logging.Logger | None, optional): Caller-configured logger for
56+
worker logs. It cannot be combined with ``log_handler`` or
57+
``log_formatter``; doing so raises ``ValueError``.
5358
exception_properties_provider (ExceptionPropertiesProvider | None, optional):
5459
Extracts portable custom properties from exceptions reported by this worker.
5560
@@ -91,6 +96,7 @@ def __init__(self, *,
9196
data_converter: DataConverter | None = None,
9297
log_handler: logging.Handler | None = None,
9398
log_formatter: logging.Formatter | None = None,
99+
logger: logging.Logger | None = None,
94100
exception_properties_provider: ExceptionPropertiesProvider | None = None):
95101

96102
if not taskhub:
@@ -113,6 +119,7 @@ def __init__(self, *,
113119
metadata=None,
114120
log_handler=log_handler,
115121
log_formatter=log_formatter,
122+
logger=logger,
116123
interceptors=resolved_interceptors,
117124
channel_options=channel_options,
118125
resiliency_options=resiliency_options,

‎durabletask/client.py‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,7 @@ def __init__(self, *,
423423
metadata: list[tuple[str, str]] | None = None,
424424
log_handler: logging.Handler | None = None,
425425
log_formatter: logging.Formatter | None = None,
426+
logger: logging.Logger | None = None,
426427
channel: grpc.Channel | None = None,
427428
secure_channel: bool = False,
428429
interceptors: Sequence[shared.ClientInterceptor] | None = None,
@@ -433,6 +434,7 @@ def __init__(self, *,
433434
data_converter: DataConverter | None = None,
434435
emit_trace_spans: bool = True):
435436

437+
self._logger = shared.get_logger("client", log_handler, log_formatter, logger)
436438
self._owns_channel = channel is None
437439
self._data_converter = data_converter if data_converter is not None else JsonDataConverter()
438440
self._host_address = (
@@ -493,7 +495,6 @@ def __init__(self, *,
493495
# can prepend the interceptor themselves via grpc.intercept_channel.
494496
self._channel = channel
495497
self._stub = cast(_SyncTaskHubSidecarServiceStub, stubs.TaskHubSidecarServiceStub(channel))
496-
self._logger = shared.get_logger("client", log_handler, log_formatter)
497498
self.default_version = default_version
498499
self._payload_store = payload_store
499500
self._emit_trace_spans = emit_trace_spans
@@ -962,6 +963,7 @@ def __init__(self, *,
962963
metadata: list[tuple[str, str]] | None = None,
963964
log_handler: logging.Handler | None = None,
964965
log_formatter: logging.Formatter | None = None,
966+
logger: logging.Logger | None = None,
965967
channel: grpc.aio.Channel | None = None,
966968
secure_channel: bool = False,
967969
interceptors: Sequence[shared.AsyncClientInterceptor] | None = None,
@@ -972,6 +974,7 @@ def __init__(self, *,
972974
data_converter: DataConverter | None = None,
973975
emit_trace_spans: bool = True):
974976

977+
self._logger = shared.get_logger("async_client", log_handler, log_formatter, logger)
975978
self._owns_channel = channel is None
976979
self._data_converter = data_converter if data_converter is not None else JsonDataConverter()
977980
self._host_address = (
@@ -1029,7 +1032,6 @@ def __init__(self, *,
10291032
if channel is not None
10301033
else None
10311034
)
1032-
self._logger = shared.get_logger("async_client", log_handler, log_formatter)
10331035
self.default_version = default_version
10341036
self._payload_store = payload_store
10351037
self._emit_trace_spans = emit_trace_spans

0 commit comments

Comments
 (0)