Gitlab - Argos ALM by PALO IT

refactor: Change root file pit.lib

parent 120cae2e
# log-call # log-call
`log-call` is a Python library that provides a decorator to log function calls. It is designed to make debugging and tracking your code execution easier by providing detailed logs of function calls, including their arguments, return values, and any exceptions that might occur. `log-call` is a Python library that provides a decorator to log function calls. It is designed to make debugging and
tracking your code execution easier by providing detailed logs of function calls, including their arguments, return
values, and any exceptions that might occur.
## Features ## Features
- **Function Call Logging**: The `@log_call` decorator logs the start and end of a function call, including its arguments and result. - **Function Call Logging**: The `@log_call` decorator logs the start and end of a function call, including its
- **Customizable Logging Level**: You can specify the logging level for each function using the `level` parameter in the decorator. arguments and result.
- **Argument and Response Logging**: You can control whether to log the function arguments and result by setting `log_args` and `log_response` parameters in the decorator. - **Customizable Logging Level**: You can specify the logging level for each function using the `level` parameter in the
decorator.
- **Argument and Response Logging**: You can control whether to log the function arguments and result by
setting `log_args` and `log_response` parameters in the decorator.
- **Asynchronous Support**: The decorator works seamlessly with both synchronous and asynchronous functions. - **Asynchronous Support**: The decorator works seamlessly with both synchronous and asynchronous functions.
- **Exception Logging**: Any exceptions thrown by the decorated function are logged and re-raised. - **Exception Logging**: Any exceptions thrown by the decorated function are logged and re-raised.
...@@ -21,7 +26,7 @@ pip install log-call ...@@ -21,7 +26,7 @@ pip install log-call
Then, import the `log_call` decorator in your Python script: Then, import the `log_call` decorator in your Python script:
```python ```python
from log_call.log_decorator import log_call from pit.lib.log_call.log_decorator import log_call
``` ```
## Usage ## Usage
...@@ -30,7 +35,7 @@ Decorate your function using `@log_call`: ...@@ -30,7 +35,7 @@ Decorate your function using `@log_call`:
```python ```python
import logging import logging
from log_call.log_decorator import log_call from pit.lib.log_call.log_decorator import log_call
@log_call(level=logging.INFO, log_args=True, log_response=True) @log_call(level=logging.INFO, log_args=True, log_response=True)
...@@ -38,15 +43,21 @@ def my_function(a, b): ...@@ -38,15 +43,21 @@ def my_function(a, b):
return a + b return a + b
``` ```
In the above example, the `my_function` will log its start and end, along with its arguments and result, at the INFO level. In the above example, the `my_function` will log its start and end, along with its arguments and result, at the INFO
level.
You can customize the logging level, and whether to log arguments and response, for each function individually. If `log_args` and `log_response` are not explicitly set, they default to `True` if the logger is in DEBUG mode. For higher log levels, these values have to be set explicitly. You can customize the logging level, and whether to log arguments and response, for each function individually.
If `log_args` and `log_response` are not explicitly set, they default to `True` if the logger is in DEBUG mode. For
higher log levels, these values have to be set explicitly.
For asynchronous functions, simply apply the decorator as you would with a synchronous function: For asynchronous functions, simply apply the decorator as you would with a synchronous function:
```python ```python
import logging import logging
from log_call.log_decorator import log_call
frompit.lib.log_call.log_decorator
import log_call
@log_call(level=logging.INFO, log_args=True, log_response=True) @log_call(level=logging.INFO, log_args=True, log_response=True)
async def my_async_function(a, b): async def my_async_function(a, b):
...@@ -56,30 +67,43 @@ async def my_async_function(a, b): ...@@ -56,30 +67,43 @@ async def my_async_function(a, b):
In case of an exception, the error will be logged and then re-raised. In case of an exception, the error will be logged and then re-raised.
## Notes ## Notes
- By default, when the logger is in `DEBUG` mode, the `log_args` and `log_response` parameters are set to `True`. For higher log levels, these values have to be set explicitly.
- The logger is taken from the module where the decorated function is defined. If you want to use a different logger, you can pass it as an argument to the decorator. - By default, when the logger is in `DEBUG` mode, the `log_args` and `log_response` parameters are set to `True`. For
higher log levels, these values have to be set explicitly.
- The logger is taken from the module where the decorated function is defined. If you want to use a different logger,
you can pass it as an argument to the decorator.
- The `@log_call` decorator can be used with both synchronous and asynchronous functions. - The `@log_call` decorator can be used with both synchronous and asynchronous functions.
- For performance reasons, the decorator validates if the log level is enabled for the logger. If the log level is not enabled, the decorator will not log the function call. - For performance reasons, the decorator validates if the log level is enabled for the logger. If the log level is not
enabled, the decorator will not log the function call.
- Logging configuration is not handled by the library. You need to configure the logging module in your application. - Logging configuration is not handled by the library. You need to configure the logging module in your application.
## Advices ## Advices
- Prefer using log levels like `INFO` or `DEBUG` for function call logging to avoid excessive logging in production environments.
- Prefer set as False the `log_args` and `log_response` parameters for functions that receive or return large amounts of data to avoid performance issues. - Prefer using log levels like `INFO` or `DEBUG` for function call logging to avoid excessive logging in production
- Take special attention with functions tha receive sensitive data or data that should not be logged, in this case, set as False the `log_args` and `log_response` parameters. environments.
- Prefer set as False the `log_args` and `log_response` parameters for functions that receive or return large amounts of
data to avoid performance issues.
- Take special attention with functions tha receive sensitive data or data that should not be logged, in this case, set
as False the `log_args` and `log_response` parameters.
- Take special attention when functions receive large collections - Take special attention when functions receive large collections
- Prefer pass the argument `_logger` to the decorator to have a better control of the log messages generated by the decorator and yours. By this way you can control the log level independently of the log level of the module where the function is defined. - Prefer pass the argument `_logger` to the decorator to have a better control of the log messages generated by the
decorator and yours. By this way you can control the log level independently of the log level of the module where the
function is defined.
````python ````python
import logging import logging
from log_call.log_decorator import log_call from pit.lib.log_call.log_decorator import log_call
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@log_call(level=logging.DEBUG, log_args=False, log_response=False, _logger="decorator_logger") @log_call(level=logging.DEBUG, log_args=False, log_response=False, _logger="decorator_logger")
def my_function(a, b): def my_function(a, b):
logger.log(logging.INFO, f"Summing {a} and {b}") logger.log(logging.INFO, f"Summing {a} and {b}")
return a + b return a + b
""" """
DEBUG : 2024-04-12 14:34:08 --- [decorator_logger ] : Start span:[RjhQ1tdyBfEzYoll] method:[sum_function] DEBUG : 2024-04-12 14:34:08 --- [decorator_logger ] : Start span:[RjhQ1tdyBfEzYoll] method:[sum_function]
INFO : 2024-04-12 14:34:08 --- [tests.test_log_decorator ] : Summing 5 and 6 INFO : 2024-04-12 14:34:08 --- [tests.test_log_decorator ] : Summing 5 and 6
......
...@@ -261,4 +261,4 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] ...@@ -261,4 +261,4 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"]
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.11" python-versions = "^3.11"
content-hash = "1418c83899fd61dd0bfe07ab9a80b2bc6254ad692afb61527cdff94fa0d53bf1" content-hash = "c1d781d68d5aec64efc48b9c6876ea2619ef3a359fe89b54ef860f7dc793d2e5"
[tool.poetry] [tool.poetry]
name = "log-call" name = "log-call"
version = "1.0.0" version = "2.0.0"
description = "log-call is a Python library that provides a decorator to log function calls." description = "log-call is a Python library that provides a decorator to log function calls."
authors = ["Miguel Galindo Rodriguez <mgalindo@palo-it.com>"] authors = ["Miguel Galindo Rodriguez <mgalindo@palo-it.com>"]
readme = "README.md" readme = "README.md"
packages = [{include = "log_call", from = "src"}] packages = [{ include = "pit", from = "src" }]
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.11" python = "^3.11"
......
import asyncio
import functools
import logging
import random
import string
start_message = "Start span:[%s] method:[%s]"
args_message = " with args [%s]"
end_message = "End span:[%s] method:[%s]"
result_message = " result [%s]"
exception_message = " exception [%s]"
__alphabet = string.ascii_letters + string.digits
def get_spam(length: int = 16) -> str:
"""
Generate a random string with numbers and letters
:param length: The length of the string
:return: A random string with numbers and letters
"""
return "".join(random.choices(__alphabet, k=length))
def log_error(entry_point, logger, level, spam_id, e):
if logger.isEnabledFor(level):
logger.log(
level, end_message + exception_message, spam_id, entry_point, repr(e)
)
def log_end(entry_point, log_response, is_verbose, logger, level, spam_id, result):
if logger.isEnabledFor(level):
should_log_response = log_response if log_response is not None else is_verbose
if should_log_response:
logger.log(
level, end_message + result_message, spam_id, entry_point, repr(result)
)
else:
logger.log(level, end_message, spam_id, entry_point)
def log_start(entry_point, log_args, is_verbose, logger, level, spam_id, args, kwargs):
if logger.isEnabledFor(level):
should_log_args = log_args if log_args is not None else is_verbose
if should_log_args:
args_repr = [repr(a) for a in args]
kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
signature = ", ".join(args_repr + kwargs_repr)
logger.log(
level, start_message + args_message, spam_id, entry_point, signature
)
else:
logger.log(level, start_message, spam_id, entry_point)
def log_call(
_func=None,
_logger: logging.Logger | str | None = None,
level: int = logging.DEBUG,
log_args: bool | None = None,
log_response: bool | None = None,
):
"""
Decorator to log the start and end of a function call, including its arguments and result.
:param _logger: logger where the logs will be written, if None the logger of the module will be used
:param _func: the function to decorate
:param level: logging level to use, default is logging.DEBUG
:param log_args: whether to log the function arguments, default is True if the logger is in DEBUG mode
, for higher log levels this value have to be set explicitly
:param log_response: whether to log the function result, default is True if the logger is in DEBUG mode
, for higher log levels this value have to be set explicitly
:return: the decorated function
"""
def log_decorator_info(__func):
def decorate_sync_async(___func):
if _logger is not None:
if isinstance(_logger, str):
logger = logging.getLogger(_logger)
else:
logger = _logger
else:
logger = logging.getLogger(___func.__module__)
spam_id = get_spam()
entry_point = ___func.__qualname__
is_verbose = level == logging.DEBUG
if asyncio.iscoroutinefunction(___func):
async def decorated(*args, **kwargs):
log_start(
entry_point,
log_args,
is_verbose,
logger,
level,
spam_id,
args,
kwargs,
)
try:
result = await ___func(*args, **kwargs)
log_end(
entry_point,
log_response,
is_verbose,
logger,
level,
spam_id,
result,
)
return result
except Exception as e:
log_error(entry_point, logger, level, spam_id, e)
raise e
else:
def decorated(*args, **kwargs):
log_start(
entry_point,
log_args,
is_verbose,
logger,
level,
spam_id,
args,
kwargs,
)
try:
result = ___func(*args, **kwargs)
log_end(
entry_point,
log_response,
is_verbose,
logger,
level,
spam_id,
result,
)
return result
except Exception as e:
log_error(entry_point, logger, level, spam_id, e)
raise e
return functools.wraps(___func)(decorated)
return decorate_sync_async(__func)
if _func is None:
# El decorador se utilizó con paréntesis, por lo que devolvemos el decorador real
return log_decorator_info
else:
# El decorador se utilizó sin paréntesis, por lo que decoramos la función directamente
return log_decorator_info(_func)
import logging
import pytest
from log_call.log_decorator import (
start_message,
log_call,
end_message,
args_message,
result_message,
exception_message,
)
@log_call(level=logging.INFO)
def div(a: float, b: float) -> float:
return a / b
@log_call(level=logging.INFO, log_args=True, log_response=True)
def div2(a: float, b: float) -> float:
return a / b
@log_call(level=logging.INFO, log_args=True, log_response=True)
def do_nothing():
pass
@log_call(level=logging.DEBUG)
def debug_function():
pass
@log_call(level=logging.DEBUG, log_args=False, log_response=False)
def debug_no_args():
pass
@log_call(level=logging.INFO, log_args=True, log_response=True)
async def async_function():
pass
@log_call(level=logging.INFO, log_args=False, log_response=False)
async def async_function_noargs():
pass
@log_call
def no_arguments() -> None:
pass
class Calculator:
@log_call(level=logging.INFO)
def div(self, a: float, b: float) -> float:
return a / b
def test_function_not_log_args(caplog):
caplog.set_level(logging.INFO)
assert div(1, 2) == 0.5
with pytest.raises(ZeroDivisionError):
div(1, 0)
assert caplog.records[0].msg == start_message
assert caplog.records[0].args[1] == "div"
assert caplog.records[1].msg == end_message
assert caplog.records[2].msg == start_message
assert caplog.records[3].msg == end_message + exception_message
def test_function_log_args(caplog):
caplog.set_level(logging.INFO)
assert div2(a=10, b=2) == 5
assert caplog.records[0].msg == start_message + args_message
assert caplog.records[0].args[1] == "div2"
assert "a=10" in caplog.records[0].args[2]
assert "b=2" in caplog.records[0].args[2]
assert caplog.records[1].msg == end_message + result_message
assert "5" in caplog.records[1].args[2]
def test_function_void_with_no_args_log_args_is_none(caplog):
caplog.set_level(logging.INFO)
do_nothing()
assert caplog.records[0].msg == start_message + args_message
assert caplog.records[0].args[1] == "do_nothing"
assert caplog.records[1].msg == end_message + result_message
assert "None" in caplog.records[1].args[2]
def test_debug_function_log_args_as_default(caplog):
caplog.set_level(logging.DEBUG)
debug_function()
assert caplog.records[0].msg == start_message + args_message
assert caplog.records[0].args[1] == "debug_function"
assert caplog.records[1].msg == end_message + result_message
assert "None" in caplog.records[1].args[2]
def test_debug_function_not_log_args(caplog):
caplog.set_level(logging.DEBUG)
debug_no_args()
assert caplog.records[0].msg == start_message
assert caplog.records[0].args[1] == "debug_no_args"
assert caplog.records[1].msg == end_message
def test_not_log_with_low_log_level(caplog):
caplog.set_level(logging.INFO)
debug_no_args()
assert len(caplog.records) == 0
def test_log_class_method(caplog):
caplog.set_level(logging.DEBUG)
calculator = Calculator()
assert calculator.div(10, 2) == 5
assert caplog.records[0].msg == start_message
assert caplog.records[0].args[1] == "Calculator.div"
assert caplog.records[1].msg == end_message
@pytest.mark.asyncio
async def test_log_async_function(caplog):
caplog.set_level(logging.INFO)
await async_function()
assert caplog.records[0].msg == start_message + args_message
assert caplog.records[0].args[1] == "async_function"
assert caplog.records[1].msg == end_message + result_message
assert "None" in caplog.records[1].args[2]
@pytest.mark.asyncio
async def test_log_async_function_no_args(caplog):
caplog.set_level(logging.INFO)
await async_function_noargs()
assert caplog.records[0].msg == start_message
assert caplog.records[0].args[1] == "async_function_noargs"
assert caplog.records[1].msg == end_message
def test_log_no_arguments(caplog):
caplog.set_level(logging.DEBUG)
no_arguments()
assert caplog.records[0].msg == start_message + args_message
assert caplog.records[0].args[1] == "no_arguments"
assert caplog.records[1].msg == end_message + result_message
assert "None" in caplog.records[1].args[2]
logger = logging.getLogger(__name__)
logger_decorator = logging.getLogger("decorator_logger")
@log_call(
level=logging.DEBUG, log_args=False, log_response=False, _logger="decorator_logger"
)
def sum_function(a, b):
logger.log(logging.INFO, f"Summing {a} and {b}")
return a + b
@log_call(
level=logging.DEBUG, log_args=False, log_response=False, _logger=logger_decorator
)
def sum_function2(a, b):
logger.log(logging.INFO, f"Summing {a} and {b}")
return a + b
def test_with_str_logger(caplog):
caplog.set_level(logging.DEBUG)
res = sum_function(5, 6)
assert res == 11
assert caplog.records[0].name == "decorator_logger"
assert caplog.records[1].name == "tests.test_log_decorator"
assert caplog.records[2].name == "decorator_logger"
def test_with_logger_instance_logger(caplog):
caplog.set_level(logging.DEBUG)
res = sum_function2(5, 6)
assert res == 11
assert caplog.records[0].name == "decorator_logger"
assert caplog.records[0].levelname == "DEBUG"
assert caplog.records[1].name == "tests.test_log_decorator"
assert caplog.records[1].levelname == "INFO"
assert caplog.records[2].name == "decorator_logger"
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment