Patterns for including Telemetry #572
-
First Check
Commit to Help
Example Codeimport typer
from rich.console import Console
app: typer.Typer = typer.Typer(
no_args_is_help=True,
rich_markup_mode="rich",
)
stdout: Console = Console()
stderr: Console = Console(stderr=True)
@app.callback()
def _callback() -> None:
pass
@app.command()
def do_something() -> None:
...
if __name__ == "__main__":
app() DescriptionI am wondering what the recommended / better practice patterns are for including telemetry within a typer based cli? For example, if I had a decorator like: def log_execution_time(func: Callable[..., Any]) -> Callable[..., Any]:
"""Log execution time and process time."""
def wrapper( # noqa: ANN202
*args: tuple,
**kwargs, # noqa: ANN003
):
"""
Execute the decorated function and log its execution and process time.
Args:
*args: Variable length argument list.
**kwargs: Arbitrary keyword arguments.
Returns:
Any: The return value of the decorated function.
"""
start_execution_time: float = time.monotonic()
start_process_time: float = time.process_time()
result: Any = func(*args, **kwargs)
execution_time: float = time.monotonic() - start_execution_time
process_time: float = time.process_time() - start_cpu_time
logger.debug(
f"{func.__name__} took {execution_time:.6f} seconds to execute and {process_time:.6f} seconds of process time",
)
return result
return wrapper How could like be applied to a method in a typer cli? I've tried @log_execute_time
@app.command()
def do_something() -> None:
... This doesn't fail but equally doesn't log anything. Doing it the opposite way: @app.command()
@log_execute_time
def do_something() -> None:
... Raises error Operating SystemmacOS Operating System DetailsMonterey 12.6.3 Typer Version0.7.0 Python Version3.11.2 Additional ContextI'm currently testing it by using the logging module but ultimately it'll use OpenTelemetry. |
Beta Was this translation helpful? Give feedback.
Replies: 4 comments 1 reply
-
@adam-moss did you ever get this working? I'm in the same boat, wanting to instrument all CLI commands (also for OTEL). |
Beta Was this translation helpful? Give feedback.
-
Have you considered using functools.wraps in your decorator? https://docs.python.org/3/library/functools.html#functools.wraps |
Beta Was this translation helpful? Give feedback.
-
I've had a chance to test this and I concur that putting the decorator above the from functools import wraps
def test_wrap(func: Callable[..., Any]) -> Callable[..., Any]:
@wraps(func)
def wrapper(*args: tuple, **kwargs):
print("Starting wrapper")
return func(*args, **kwargs)
return wrapper |
Beta Was this translation helpful? Give feedback.
-
I concur on using However, I may suggest a different approach in case you have many commands you need to decorate: you may subclass class TyperWithTelemetry(typer.Typer):
@log_execution_time
def __call__(self, *args, **kwargs):
return super().__call__(*args, **kwargs) |
Beta Was this translation helpful? Give feedback.
I've had a chance to test this and I concur that putting the decorator above the
@app.command()
seems to have the effect that the wrapper function is never called. If I change the wrapper to the following, I am able to put the wrapper below the@app.command()
and have it successfully run: