Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit 1b09b08

Browse files
Allow use of both @trace and @tag_args stacked on the same function (#13453)
```py @trace @tag_args async def get_oldest_event_ids_with_depth_in_room(...) ... ``` Before this PR, you would see a warning in the logs and the span was not exported: ``` 2022-08-03 19:11:59,383 - synapse.logging.opentracing - 835 - ERROR - GET-0 - @trace may not have wrapped EventFederationWorkerStore.get_oldest_event_ids_with_depth_in_room correctly! The function is not async but returned a coroutine. ```
1 parent 1595052 commit 1b09b08

File tree

3 files changed

+186
-56
lines changed

3 files changed

+186
-56
lines changed

changelog.d/13453.misc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Allow use of both `@trace` and `@tag_args` stacked on the same function (tracing).

synapse/logging/opentracing.py

Lines changed: 102 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ def set_fates(clotho, lachesis, atropos, father="Zues", mother="Themis"):
173173
Any,
174174
Callable,
175175
Collection,
176+
ContextManager,
176177
Dict,
177178
Generator,
178179
Iterable,
@@ -823,75 +824,117 @@ def extract_text_map(carrier: Dict[str, str]) -> Optional["opentracing.SpanConte
823824
# Tracing decorators
824825

825826

826-
def trace_with_opname(opname: str) -> Callable[[Callable[P, R]], Callable[P, R]]:
827+
def _custom_sync_async_decorator(
828+
func: Callable[P, R],
829+
wrapping_logic: Callable[[Callable[P, R], Any, Any], ContextManager[None]],
830+
) -> Callable[P, R]:
827831
"""
828-
Decorator to trace a function with a custom opname.
829-
830-
See the module's doc string for usage examples.
832+
Decorates a function that is sync or async (coroutines), or that returns a Twisted
833+
`Deferred`. The custom business logic of the decorator goes in `wrapping_logic`.
834+
835+
Example usage:
836+
```py
837+
# Decorator to time the function and log it out
838+
def duration(func: Callable[P, R]) -> Callable[P, R]:
839+
@contextlib.contextmanager
840+
def _wrapping_logic(func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> Generator[None, None, None]:
841+
start_ts = time.time()
842+
try:
843+
yield
844+
finally:
845+
end_ts = time.time()
846+
duration = end_ts - start_ts
847+
logger.info("%s took %s seconds", func.__name__, duration)
848+
return _custom_sync_async_decorator(func, _wrapping_logic)
849+
```
831850
851+
Args:
852+
func: The function to be decorated
853+
wrapping_logic: The business logic of your custom decorator.
854+
This should be a ContextManager so you are able to run your logic
855+
before/after the function as desired.
832856
"""
833857

834-
def decorator(func: Callable[P, R]) -> Callable[P, R]:
835-
if opentracing is None:
836-
return func # type: ignore[unreachable]
858+
if inspect.iscoroutinefunction(func):
837859

838-
if inspect.iscoroutinefunction(func):
860+
@wraps(func)
861+
async def _wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
862+
with wrapping_logic(func, *args, **kwargs):
863+
return await func(*args, **kwargs) # type: ignore[misc]
839864

840-
@wraps(func)
841-
async def _trace_inner(*args: P.args, **kwargs: P.kwargs) -> R:
842-
with start_active_span(opname):
843-
return await func(*args, **kwargs) # type: ignore[misc]
865+
else:
866+
# The other case here handles both sync functions and those
867+
# decorated with inlineDeferred.
868+
@wraps(func)
869+
def _wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
870+
scope = wrapping_logic(func, *args, **kwargs)
871+
scope.__enter__()
844872

845-
else:
846-
# The other case here handles both sync functions and those
847-
# decorated with inlineDeferred.
848-
@wraps(func)
849-
def _trace_inner(*args: P.args, **kwargs: P.kwargs) -> R:
850-
scope = start_active_span(opname)
851-
scope.__enter__()
852-
853-
try:
854-
result = func(*args, **kwargs)
855-
if isinstance(result, defer.Deferred):
856-
857-
def call_back(result: R) -> R:
858-
scope.__exit__(None, None, None)
859-
return result
860-
861-
def err_back(result: R) -> R:
862-
scope.__exit__(None, None, None)
863-
return result
864-
865-
result.addCallbacks(call_back, err_back)
866-
867-
else:
868-
if inspect.isawaitable(result):
869-
logger.error(
870-
"@trace may not have wrapped %s correctly! "
871-
"The function is not async but returned a %s.",
872-
func.__qualname__,
873-
type(result).__name__,
874-
)
873+
try:
874+
result = func(*args, **kwargs)
875+
if isinstance(result, defer.Deferred):
876+
877+
def call_back(result: R) -> R:
878+
scope.__exit__(None, None, None)
879+
return result
875880

881+
def err_back(result: R) -> R:
876882
scope.__exit__(None, None, None)
883+
return result
884+
885+
result.addCallbacks(call_back, err_back)
886+
887+
else:
888+
if inspect.isawaitable(result):
889+
logger.error(
890+
"@trace may not have wrapped %s correctly! "
891+
"The function is not async but returned a %s.",
892+
func.__qualname__,
893+
type(result).__name__,
894+
)
895+
896+
scope.__exit__(None, None, None)
877897

878-
return result
898+
return result
879899

880-
except Exception as e:
881-
scope.__exit__(type(e), None, e.__traceback__)
882-
raise
900+
except Exception as e:
901+
scope.__exit__(type(e), None, e.__traceback__)
902+
raise
883903

884-
return _trace_inner # type: ignore[return-value]
904+
return _wrapper # type: ignore[return-value]
885905

886-
return decorator
906+
907+
def trace_with_opname(
908+
opname: str,
909+
*,
910+
tracer: Optional["opentracing.Tracer"] = None,
911+
) -> Callable[[Callable[P, R]], Callable[P, R]]:
912+
"""
913+
Decorator to trace a function with a custom opname.
914+
See the module's doc string for usage examples.
915+
"""
916+
917+
# type-ignore: mypy bug, see https://github.com/python/mypy/issues/12909
918+
@contextlib.contextmanager # type: ignore[arg-type]
919+
def _wrapping_logic(
920+
func: Callable[P, R], *args: P.args, **kwargs: P.kwargs
921+
) -> Generator[None, None, None]:
922+
with start_active_span(opname, tracer=tracer):
923+
yield
924+
925+
def _decorator(func: Callable[P, R]) -> Callable[P, R]:
926+
if not opentracing:
927+
return func
928+
929+
return _custom_sync_async_decorator(func, _wrapping_logic)
930+
931+
return _decorator
887932

888933

889934
def trace(func: Callable[P, R]) -> Callable[P, R]:
890935
"""
891936
Decorator to trace a function.
892-
893937
Sets the operation name to that of the function's name.
894-
895938
See the module's doc string for usage examples.
896939
"""
897940

@@ -900,7 +943,7 @@ def trace(func: Callable[P, R]) -> Callable[P, R]:
900943

901944
def tag_args(func: Callable[P, R]) -> Callable[P, R]:
902945
"""
903-
Tags all of the args to the active span.
946+
Decorator to tag all of the args to the active span.
904947
905948
Args:
906949
func: `func` is assumed to be a method taking a `self` parameter, or a
@@ -911,22 +954,25 @@ def tag_args(func: Callable[P, R]) -> Callable[P, R]:
911954
if not opentracing:
912955
return func
913956

914-
@wraps(func)
915-
def _tag_args_inner(*args: P.args, **kwargs: P.kwargs) -> R:
957+
# type-ignore: mypy bug, see https://github.com/python/mypy/issues/12909
958+
@contextlib.contextmanager # type: ignore[arg-type]
959+
def _wrapping_logic(
960+
func: Callable[P, R], *args: P.args, **kwargs: P.kwargs
961+
) -> Generator[None, None, None]:
916962
argspec = inspect.getfullargspec(func)
917963
# We use `[1:]` to skip the `self` object reference and `start=1` to
918964
# make the index line up with `argspec.args`.
919965
#
920-
# FIXME: We could update this handle any type of function by ignoring the
966+
# FIXME: We could update this to handle any type of function by ignoring the
921967
# first argument only if it's named `self` or `cls`. This isn't fool-proof
922968
# but handles the idiomatic cases.
923969
for i, arg in enumerate(args[1:], start=1): # type: ignore[index]
924970
set_tag("ARG_" + argspec.args[i], str(arg))
925971
set_tag("args", str(args[len(argspec.args) :])) # type: ignore[index]
926972
set_tag("kwargs", str(kwargs))
927-
return func(*args, **kwargs)
973+
yield
928974

929-
return _tag_args_inner
975+
return _custom_sync_async_decorator(func, _wrapping_logic)
930976

931977

932978
@contextlib.contextmanager

tests/logging/test_opentracing.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
from synapse.logging.opentracing import (
2626
start_active_span,
2727
start_active_span_follows_from,
28+
tag_args,
29+
trace_with_opname,
2830
)
2931
from synapse.util import Clock
3032

@@ -38,8 +40,12 @@
3840
except ImportError:
3941
jaeger_client = None # type: ignore
4042

43+
import logging
44+
4145
from tests.unittest import TestCase
4246

47+
logger = logging.getLogger(__name__)
48+
4349

4450
class LogContextScopeManagerTestCase(TestCase):
4551
"""
@@ -194,3 +200,80 @@ async def root():
194200
self._reporter.get_spans(),
195201
[scopes[1].span, scopes[2].span, scopes[0].span],
196202
)
203+
204+
def test_trace_decorator_sync(self) -> None:
205+
"""
206+
Test whether we can use `@trace_with_opname` (`@trace`) and `@tag_args`
207+
with sync functions
208+
"""
209+
with LoggingContext("root context"):
210+
211+
@trace_with_opname("fixture_sync_func", tracer=self._tracer)
212+
@tag_args
213+
def fixture_sync_func() -> str:
214+
return "foo"
215+
216+
result = fixture_sync_func()
217+
self.assertEqual(result, "foo")
218+
219+
# the span should have been reported
220+
self.assertEqual(
221+
[span.operation_name for span in self._reporter.get_spans()],
222+
["fixture_sync_func"],
223+
)
224+
225+
def test_trace_decorator_deferred(self) -> None:
226+
"""
227+
Test whether we can use `@trace_with_opname` (`@trace`) and `@tag_args`
228+
with functions that return deferreds
229+
"""
230+
reactor = MemoryReactorClock()
231+
232+
with LoggingContext("root context"):
233+
234+
@trace_with_opname("fixture_deferred_func", tracer=self._tracer)
235+
@tag_args
236+
def fixture_deferred_func() -> "defer.Deferred[str]":
237+
d1: defer.Deferred[str] = defer.Deferred()
238+
d1.callback("foo")
239+
return d1
240+
241+
result_d1 = fixture_deferred_func()
242+
243+
# let the tasks complete
244+
reactor.pump((2,) * 8)
245+
246+
self.assertEqual(self.successResultOf(result_d1), "foo")
247+
248+
# the span should have been reported
249+
self.assertEqual(
250+
[span.operation_name for span in self._reporter.get_spans()],
251+
["fixture_deferred_func"],
252+
)
253+
254+
def test_trace_decorator_async(self) -> None:
255+
"""
256+
Test whether we can use `@trace_with_opname` (`@trace`) and `@tag_args`
257+
with async functions
258+
"""
259+
reactor = MemoryReactorClock()
260+
261+
with LoggingContext("root context"):
262+
263+
@trace_with_opname("fixture_async_func", tracer=self._tracer)
264+
@tag_args
265+
async def fixture_async_func() -> str:
266+
return "foo"
267+
268+
d1 = defer.ensureDeferred(fixture_async_func())
269+
270+
# let the tasks complete
271+
reactor.pump((2,) * 8)
272+
273+
self.assertEqual(self.successResultOf(d1), "foo")
274+
275+
# the span should have been reported
276+
self.assertEqual(
277+
[span.operation_name for span in self._reporter.get_spans()],
278+
["fixture_async_func"],
279+
)

0 commit comments

Comments
 (0)