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

Allow use of both @trace and @tag_args stacked on the same function #13453

Merged
merged 16 commits into from
Aug 9, 2022
Merged
1 change: 1 addition & 0 deletions changelog.d/13453.misc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Allow use of both `@trace` and `@tag_args` stacked on the same function (tracing).
139 changes: 84 additions & 55 deletions synapse/logging/opentracing.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ def set_fates(clotho, lachesis, atropos, father="Zues", mother="Themis"):
Any,
Callable,
Collection,
ContextManager,
Dict,
Generator,
Iterable,
Expand Down Expand Up @@ -823,75 +824,103 @@ def extract_text_map(carrier: Dict[str, str]) -> Optional["opentracing.SpanConte
# Tracing decorators


def trace_with_opname(opname: str) -> Callable[[Callable[P, R]], Callable[P, R]]:
def _create_decorator(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we rename this to _decorate, or similar?
In a sense, this method is the decorator: it takes func and returns a wrapped function with the same signature.

The docstring could also do with rewording: "Decorates a function that is..."

Copy link
Contributor Author

@MadLittleMods MadLittleMods Aug 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a descriptor we can add to it? I feel like _create_decorator and _decorate don't properly describe that this does something you probably want to use for all decorators (handle any function regardless if sync/async). But I don't know how to encapsulate that into a name like _fancy_decorate (_safe_decorate)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm also stuck thinking of a better name.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_create_sync_async_decorator? It feels like it needs more than just _create_decorator since not all decorators need to care about this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@clokep Better than current at least 👍 What about _custom_sync_async_decorator? "custom" to try to hint that you add your own business logic via wrapping_logic

func: Callable[P, R],
# TODO: What is the correct type for these `Any`? `P.args, P.kwargs` isn't allowed here
wrapping_logic: Callable[[Callable[P, R], Any, Any], ContextManager[None]],
squahtx marked this conversation as resolved.
Show resolved Hide resolved
) -> Callable[P, R]:
"""
Decorator to trace a function with a custom opname.

See the module's doc string for usage examples.

Creates a decorator that is able to handle sync functions, async functions
(coroutines), and inlineDeferred from Twisted.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic is generalized from our existing trace_with_opname decorator that handles all of these scenarios.

We now re-use it for both @trace_with_opname (which backs @trace) and @tag_args now ⏩

Example usage:
```py
# Decorator to time the function and log it out
def duration(func: Callable[P, R]) -> Callable[P, R]:
@contextlib.contextmanager
def _wrapping_logic(func: Callable[P, R], *args: P.args, **kwargs: P.kwargs):
start_ts = time.time()
yield
end_ts = time.time()
duration = end_ts - start_ts
logger.info("%s took %s seconds", func.__name__, duration)
return _create_decorator(func, _wrapping_logic)
```
Args:
func: The function to be decorated
wrapping_logic: The business logic of your custom decorator.
This should be a ContextManager so you are able to run your logic
before/after the function as desired.
"""

def decorator(func: Callable[P, R]) -> Callable[P, R]:
if opentracing is None:
return func # type: ignore[unreachable]

@wraps(func)
async def _wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
squahtx marked this conversation as resolved.
Show resolved Hide resolved
if inspect.iscoroutinefunction(func):

@wraps(func)
async def _trace_inner(*args: P.args, **kwargs: P.kwargs) -> R:
with start_active_span(opname):
return await func(*args, **kwargs) # type: ignore[misc]

with wrapping_logic(func, *args, **kwargs):
return await func(*args, **kwargs)
else:
# The other case here handles both sync functions and those
# decorated with inlineDeferred.
@wraps(func)
def _trace_inner(*args: P.args, **kwargs: P.kwargs) -> R:
scope = start_active_span(opname)
scope.__enter__()

try:
result = func(*args, **kwargs)
if isinstance(result, defer.Deferred):

def call_back(result: R) -> R:
scope.__exit__(None, None, None)
return result

def err_back(result: R) -> R:
scope.__exit__(None, None, None)
return result

result.addCallbacks(call_back, err_back)

else:
if inspect.isawaitable(result):
logger.error(
"@trace may not have wrapped %s correctly! "
"The function is not async but returned a %s.",
func.__qualname__,
type(result).__name__,
)
scope = wrapping_logic(func, *args, **kwargs)
scope.__enter__()

try:
result = func(*args, **kwargs)
if isinstance(result, defer.Deferred):

def call_back(result: R) -> R:
scope.__exit__(None, None, None)
return result

def err_back(result: R) -> R:
scope.__exit__(None, None, None)
return result

result.addCallbacks(call_back, err_back)

else:
if inspect.isawaitable(result):
logger.error(
"@trace may not have wrapped %s correctly! "
"The function is not async but returned a %s.",
func.__qualname__,
type(result).__name__,
)
squahtx marked this conversation as resolved.
Show resolved Hide resolved

return result
scope.__exit__(None, None, None)

except Exception as e:
scope.__exit__(type(e), None, e.__traceback__)
raise
return result

return _trace_inner # type: ignore[return-value]
except Exception as e:
scope.__exit__(type(e), None, e.__traceback__)
raise

return decorator
return _wrapper # type: ignore[return-value]
squahtx marked this conversation as resolved.
Show resolved Hide resolved


def trace_with_opname(opname: str) -> Callable[[Callable[P, R]], Callable[P, R]]:
"""
Decorator to trace a function with a custom opname.
See the module's doc string for usage examples.
"""

@contextlib.contextmanager
def _wrapping_logic(func: Callable[P, R], *args: P.args, **kwargs: P.kwargs):
MadLittleMods marked this conversation as resolved.
Show resolved Hide resolved
if opentracing is None:
return None
MadLittleMods marked this conversation as resolved.
Show resolved Hide resolved

with start_active_span(opname):
yield

def _decorator(func: Callable[P, R]):
MadLittleMods marked this conversation as resolved.
Show resolved Hide resolved
return _create_decorator(func, _wrapping_logic)

return _decorator


def trace(func: Callable[P, R]) -> Callable[P, R]:
"""
Decorator to trace a function.

Sets the operation name to that of the function's name.

See the module's doc string for usage examples.
"""

Expand All @@ -900,22 +929,22 @@ def trace(func: Callable[P, R]) -> Callable[P, R]:

def tag_args(func: Callable[P, R]) -> Callable[P, R]:
"""
Tags all of the args to the active span.
Decorator to tag all of the args to the active span.
"""

if not opentracing:
return func

@wraps(func)
def _tag_args_inner(*args: P.args, **kwargs: P.kwargs) -> R:
@contextlib.contextmanager
def _wrapping_logic(func: Callable[P, R], *args: P.args, **kwargs: P.kwargs):
argspec = inspect.getfullargspec(func)
for i, arg in enumerate(argspec.args[1:]):
MadLittleMods marked this conversation as resolved.
Show resolved Hide resolved
set_tag("ARG_" + arg, str(args[i])) # type: ignore[index]
set_tag("args", str(args[len(argspec.args) :])) # type: ignore[index]
set_tag("kwargs", str(kwargs))
return func(*args, **kwargs)
yield

return _tag_args_inner
return _create_decorator(func, _wrapping_logic)


@contextlib.contextmanager
Expand Down
3 changes: 3 additions & 0 deletions synapse/storage/databases/main/event_federation.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from synapse.api.errors import StoreError
from synapse.api.room_versions import EventFormatVersions, RoomVersion
from synapse.events import EventBase, make_event_from_dict
from synapse.logging.opentracing import tag_args, trace
MadLittleMods marked this conversation as resolved.
Show resolved Hide resolved
from synapse.metrics.background_process_metrics import wrap_as_background_process
from synapse.storage._base import SQLBaseStore, db_to_json, make_in_list_sql_clause
from synapse.storage.database import (
Expand Down Expand Up @@ -709,6 +710,8 @@ def _get_auth_chain_difference_txn(
# Return all events where not all sets can reach them.
return {eid for eid, n in event_to_missing_sets.items() if n}

@trace
@tag_args
MadLittleMods marked this conversation as resolved.
Show resolved Hide resolved
async def get_oldest_event_ids_with_depth_in_room(
self, room_id: str
) -> List[Tuple[str, int]]:
Expand Down