Closed sarnikowski closed 3 weeks ago
Yes, you should look into interceptors. For instance, this sample sends errors to Sentry. You can make an activity interceptor that logs and re-raises.
Also, feel free to join us on our forums or #python-sdk
on Slack with general questions.
For posterity, I ended up with the following solution:
I added a structlog interceptor, as suggested by @cretz:
from typing import Any, override
import structlog
from temporalio.worker import (
ActivityInboundInterceptor,
ExecuteActivityInput,
ExecuteWorkflowInput,
Interceptor,
WorkflowInboundInterceptor,
WorkflowInterceptorClassInput,
)
logger = structlog.get_logger()
class _StructlogActivityInboundInterceptor(ActivityInboundInterceptor):
@override
async def execute_activity(self, input: ExecuteActivityInput) -> Any:
try:
return await super().execute_activity(input)
except Exception as e:
logger.exception("uncaught exception in activity")
raise e
class _StructlogWorkflowInboundInterceptor(WorkflowInboundInterceptor):
@override
async def execute_workflow(self, input: ExecuteWorkflowInput) -> Any:
try:
return await super().execute_workflow(input)
except Exception as e:
logger.exception("uncaught exception in workflow")
raise e
class StructlogInterceptor(Interceptor):
"""Temporal Interceptor class which will log workflow & activity exceptions to stdout."""
@override
def intercept_activity(self, next: ActivityInboundInterceptor) -> ActivityInboundInterceptor:
return _StructlogActivityInboundInterceptor(super().intercept_activity(next))
@override
def workflow_interceptor_class(
self, input: WorkflowInterceptorClassInput
) -> type[WorkflowInboundInterceptor] | None:
return _StructlogWorkflowInboundInterceptor
However, this only ensured that uncaught exceptions would be written to stdout, using structlog. To suppress the exception being written to stderr, I ended up redirecting stderr to /dev/null
, which I don't think is an ideal solution. To limit the scope of the redirect, I made a context manager, that I am using on my threadpool executor. The manager looks like this:
class RedirectStdErrToDevNull:
"""A context manager that pipes stderr output to /dev/null.
Example:
>>> import sys
>>> with StdErrToDevNull():
>>> print("quack", file=sys.stderr)
>>> # No quacking will be written to stderr.
"""
def __init__(self) -> None:
self._devnull: TextIO | None = None
self._stderr_redirector: contextlib.redirect_stderr[TextIO] | None = None
def __enter__(self) -> "RedirectStdErrToDevNull":
self._devnull = open(os.devnull, "w")
self._stderr_redirector = contextlib.redirect_stderr(self._devnull)
self._stderr_redirector.__enter__()
return self
def __exit__(
self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None
) -> None:
if self._stderr_redirector is not None:
self._stderr_redirector.__exit__(exc_type, exc_val, exc_tb)
if self._devnull is not None:
self._devnull.close()
When one normally wants to override the writing of uncaught exceptions in python, you can use the
excepthook
of sys. Below is an example of how one can do this in temporal:However, if the workers are running using for example a threadpool executor, this does not work because the hook of
sys.excepthook
is not propagated to these threads (ref: https://github.com/python/cpython/pull/13515).I have an application where we do not want to print exceptions and their stacktraces to stderr, but rather log all uncaught exceptions. The problem is that if I want to do this for threads, the usual approach is to simply catch the exception and log the errors instead of raising an exception. However, if I want the temporal workflow to pick up the fact that an activity has failed, I believe I cannot do this ? So in essence I would like:
Is there a way to achieve this ?