Skip to content

Commit b309891

Browse files
authored
Migrate AWX utility for slow method logging to DAB (ansible#660)
Followup from ansible#356 The intent is that AWX will delete this method and import from DAB.
1 parent 832e4a8 commit b309891

File tree

4 files changed

+139
-0
lines changed

4 files changed

+139
-0
lines changed

ansible_base/jwt_consumer/common/auth.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
from ansible_base.jwt_consumer.common.cache import JWTCache
1515
from ansible_base.jwt_consumer.common.cert import JWTCert, JWTCertException
16+
from ansible_base.lib.logging.runtime import log_excess_runtime
1617
from ansible_base.lib.utils.auth import get_user_by_ansible_id
1718
from ansible_base.lib.utils.translations import translatableConditionally as _
1819
from ansible_base.resource_registry.models import Resource, ResourceType
@@ -50,6 +51,7 @@ def __init__(self, user_fields=default_mapped_user_fields) -> None:
5051
self.user = None
5152
self.token = None
5253

54+
@log_excess_runtime(logger, debug_cutoff=0.01)
5355
def parse_jwt_token(self, request):
5456
"""
5557
parses the given request setting self.user and self.token
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import functools
2+
import time
3+
from collections.abc import Callable
4+
from logging import Logger, getLogger
5+
6+
__all__ = ['log_excess_runtime']
7+
8+
9+
# This allows for configuring all logs from this to go to a particular destination
10+
# this is not a child of ansible_base so it can be configured separately, and default to not configured
11+
dab_logger = getLogger('dab_runtime_logger')
12+
13+
14+
DEFAULT_MSG = 'Running {name} took {delta:.2f}s'
15+
16+
17+
def log_excess_runtime(ext_logger: Logger, cutoff: float = 5.0, debug_cutoff: float = 2.0, msg: str = DEFAULT_MSG, add_log_data: bool = False):
18+
"""Utility to write logs to the passed-in logger if the function it decorates takes too long.
19+
20+
Call this with the configuration options to return a decorator."""
21+
22+
def log_excess_runtime_decorator(func: Callable):
23+
"""Instantiated decorator for the DAB excess runtime logger utility"""
24+
25+
@functools.wraps(func)
26+
def _new_func(*args, **kwargs):
27+
log_data = {'name': repr(func.__name__)}
28+
start_time = time.time()
29+
30+
if add_log_data:
31+
return_value = func(*args, log_data=log_data, **kwargs)
32+
else:
33+
return_value = func(*args, **kwargs)
34+
35+
log_data['delta'] = time.time() - start_time
36+
37+
formatted_msg = msg.format(**log_data)
38+
39+
for logger in (dab_logger, ext_logger):
40+
if log_data['delta'] > cutoff:
41+
logger.info(formatted_msg)
42+
elif log_data['delta'] > debug_cutoff:
43+
logger.debug(formatted_msg)
44+
45+
return return_value
46+
47+
return _new_func
48+
49+
return log_excess_runtime_decorator

docs/logging.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,50 @@ Within those logs, you should be able to see the culprit:
6969
test_app-1 | File "/src/test_app/views.py", line 185, in timeout_view
7070
test_app-1 | time.sleep(60*10) # 10 minutes
7171
```
72+
73+
## Method Runtime Helper
74+
75+
As a general utility, sometimes you want to log if a particular method takes longer than a certain time.
76+
This is true in almost any case where a method is doing something known to be performance-sensitive,
77+
such as processing Ansible facts in AWX.
78+
79+
```python
80+
import logging
81+
from ansible_base.lib.logging.runtime import log_excess_runtime
82+
83+
logger = logging.getLogger('my_app.tasks.cleanup')
84+
85+
@log_excess_runtime(logger, cutoff=2.0)
86+
def cleanup(self):
87+
# Do stuff that could take a long time
88+
```
89+
90+
Then if the `cleaup` method takes over 2.0 seconds, you should see a log like this:
91+
92+
```
93+
DEBUG dab_runtime_logger:runtime.py:42 Running 'cleanup' took 2.30s
94+
```
95+
96+
Because you are passing in your own logger, you can control everything about how those logs are handed.
97+
If you need to customize the thresholds or log message, you can pass kwards to
98+
the `@log_excess_runtime` call.
99+
- `cutoff` - time cutoff, any time greater than this causes an INFO level log
100+
- `debug_cutoff` - any time greater than this causes a DEBUG level log
101+
- `msg` - the log message with format strings in it
102+
- `add_log_data` - add a kwarg `log_data` to the method call as a dict that the method can attach additional log data to
103+
104+
The `msg` and `add_log_data` are intended to interact.
105+
For an example, imagine that the `cleanup` method deletes files an keeps a count, `deleted_count`.
106+
The number of deleted fields could be added to the log message with this.
107+
108+
```python
109+
import logging
110+
from ansible_base.lib.logging.runtime import log_excess_runtime
111+
112+
logger = logging.getLogger('my_app.tasks.cleanup')
113+
114+
@log_excess_runtime(logger, cutoff=2.0, msg='Cleanup took {delta:.2f}s, deleted {files_deleted}')
115+
def cleanup(self, log_data):
116+
# Do stuff that could take a long time
117+
log_data['files_deleted'] = deleted_count
118+
```
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import logging
2+
import time
3+
4+
from ansible_base.lib.logging.runtime import log_excess_runtime
5+
6+
logger = logging.getLogger('test_app.tests.lib.logging')
7+
8+
9+
def sleep_for(delta):
10+
time.sleep(delta)
11+
12+
13+
def test_no_log_needed(caplog):
14+
df = log_excess_runtime(logger)(sleep_for)
15+
df(0)
16+
assert caplog.text == ''
17+
18+
19+
def test_debug_log(caplog):
20+
df = log_excess_runtime(logger, debug_cutoff=0.0)(sleep_for)
21+
with caplog.at_level(logging.DEBUG):
22+
df(0)
23+
assert "Running 'sleep_for' took " in caplog.text
24+
25+
26+
def test_info_log(caplog):
27+
df = log_excess_runtime(logger, debug_cutoff=0.0, cutoff=0.05)(sleep_for)
28+
with caplog.at_level(logging.INFO):
29+
df(0.1)
30+
assert "Running 'sleep_for' took " in caplog.text
31+
32+
33+
def extra_message(log_data):
34+
log_data['foo'] = 'bar'
35+
36+
37+
def test_extra_msg_and_data(caplog):
38+
df = log_excess_runtime(logger, debug_cutoff=0.0, add_log_data=True, msg='extra_message log foo={foo}')(extra_message)
39+
with caplog.at_level(logging.DEBUG):
40+
df()
41+
assert "extra_message log foo=bar" in caplog.text

0 commit comments

Comments
 (0)