Skip to content

Commit d7d02c0

Browse files
author
Martin Balaz
committed
Fix high memory usage in PyTests logging system
Polling functions like `wait_until...` often runs multiple iterations. When ODL returns large response body (e.g., PCEP topology dumps), logging every message leads to high memmory usage. Pytest and Allure retain the complete history of all logs in virtual memmory. This patch implements a "Deferred Logging" mechanism: 1. Logs are temporarily captured in a lightweight memory buffer during test execution. 2. If an attempt fails (and is not the last one), logs cpatured in the buffer are discarded immediately. 3. The buffer is "replayed" to the main logger only if the attempt succeeds or if it is the final failure. Also introduce variable MAX_HTTP_RESPONSE_BODY_LOG_SIZE for limitting maximum logged HTTP response body size. JIRA: BGPCEP-1066 Change-Id: I37146943a79622e796fa92b03fe4f4f03a614eac Signed-off-by: Martin Balaz <[email protected]>
1 parent ed52135 commit d7d02c0

File tree

4 files changed

+107
-9
lines changed

4 files changed

+107
-9
lines changed

tests/libraries/pcep.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
ODL_IP = variables.ODL_IP
2727
RESTCONF_PORT = variables.RESTCONF_PORT
2828
TOOLS_IP = variables.TOOLS_IP
29+
MAX_HTTP_RESPONSE_BODY_LOG_SIZE = variables.MAX_HTTP_RESPONSE_BODY_LOG_SIZE
2930
VARIABLES = pcep_variables.get_variables(TOOLS_IP)
3031

3132
log = logging.getLogger(__name__)
@@ -55,7 +56,6 @@ def get_pcep_topology() -> requests.Response:
5556
requests.Response:: Pcep topology
5657
"""
5758
rest_session = AuthStandalone.Init_Session(ODL_IP, "admin", "admin")
58-
# Not Logging content, as it may be huge.
5959
resp = AuthStandalone.Get_Using_Session(
6060
rest_session,
6161
(
@@ -68,7 +68,10 @@ def get_pcep_topology() -> requests.Response:
6868
f"Response code for get pcep topology does not meach expected 200, "
6969
f"but is {resp.status_code}"
7070
)
71-
log.debug(resp.text)
71+
resp_text = utils.truncate_long_text(resp.text, MAX_HTTP_RESPONSE_BODY_LOG_SIZE)
72+
log.debug(f"Response: {resp_text}")
73+
log.info(f"Response code: {resp.status_code}")
74+
log.debug(f"Response headers: {resp.headers}")
7275
return resp
7376

7477

tests/libraries/templated_requests.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
ODL_IP = variables.ODL_IP
2020
RESTCONF_PORT = variables.RESTCONF_PORT
21+
MAX_HTTP_RESPONSE_BODY_LOG_SIZE = variables.MAX_HTTP_RESPONSE_BODY_LOG_SIZE
2122
BASE_URL = f"http://{ODL_IP}:{RESTCONF_PORT}"
2223

2324
ALLOWED_STATUS_CODES = {200, 201, 204}
@@ -68,7 +69,8 @@ def get_from_uri(
6869
log.error(f"Response headers: {response.headers}")
6970
raise AssertionError("Unexpected failure in GET response.") from e
7071
else:
71-
log.debug(f"Response: {response.text}")
72+
resposne_text = utils.truncate_long_text(response.text, MAX_HTTP_RESPONSE_BODY_LOG_SIZE)
73+
log.debug(f"Response: {resposne_text}")
7274
log.info(f"Response code: {response.status_code}")
7375
log.debug(f"Response headers: {response.headers}")
7476

@@ -121,7 +123,8 @@ def put_to_uri_request(
121123
log.error(f"Response headers: {response.headers}")
122124
raise AssertionError("Unexpected failure in PUT response.") from e
123125
else:
124-
log.debug(f"Response: {response.text}")
126+
resposne_text = utils.truncate_long_text(response.text, MAX_HTTP_RESPONSE_BODY_LOG_SIZE)
127+
log.debug(f"Response: {resposne_text}")
125128
log.info(f"Response code: {response.status_code}")
126129
log.debug(f"Response headers: {response.headers}")
127130

@@ -174,7 +177,8 @@ def post_to_uri(
174177
log.error(f"Response headers: {response.headers}")
175178
raise AssertionError("Unexpected failure in POST response.") from e
176179
else:
177-
log.debug(f"Response: {response.text}")
180+
resposne_text = utils.truncate_long_text(response.text, MAX_HTTP_RESPONSE_BODY_LOG_SIZE)
181+
log.debug(f"Response: {resposne_text}")
178182
log.info(f"Response code: {response.status_code}")
179183
log.debug(f"Response headers: {response.headers}")
180184

@@ -220,7 +224,8 @@ def delete_from_uri_request(
220224
log.error(f"Response headers: {response.headers}")
221225
raise AssertionError("Unexpected failure in DELETE response.") from e
222226
else:
223-
log.debug(f"Response: {response.text}")
227+
resposne_text = utils.truncate_long_text(response.text, MAX_HTTP_RESPONSE_BODY_LOG_SIZE)
228+
log.debug(f"Response: {resposne_text}")
224229
log.info(f"Response code: {response.status_code}")
225230
log.debug(f"Response headers: {response.headers}")
226231

tests/libraries/utils.py

Lines changed: 92 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,96 @@
66
# and is available at http://www.eclipse.org/legal/epl-v10.html
77
#
88

9+
from contextlib import contextmanager
910
import difflib
1011
import logging
1112
import time
1213
from collections.abc import Callable
13-
from typing import Any, List, Tuple
14+
from typing import Any, Generator, List, Tuple
1415

1516
from libraries import infra
1617
from libraries import norm_json
1718

1819
log = logging.getLogger(__name__)
1920

2021

22+
class DeferredLogHandler(logging.Handler):
23+
"""Stores log records in memory instead of writing them immediately."""
24+
def __init__(self):
25+
"""Initialize the deferred handler with an empty record list."""
26+
super().__init__()
27+
self.records = []
28+
29+
def emit(self, record: logging.LogRecord):
30+
"""Store an emitted log record in local buffer.
31+
32+
Args:
33+
record (logging.LogRecord): Log record to be stored.
34+
35+
Returns:
36+
None
37+
"""
38+
self.records.append(record)
39+
40+
def flush_to_target(self, target_logger: logging.Logger):
41+
"""Replay buffered log records to the target logger.
42+
43+
Args:
44+
target_logger (logging.Logger): Logger to which the buffered records
45+
should be sent.
46+
47+
Returns:
48+
None
49+
"""
50+
for record in self.records:
51+
target_logger.handle(record)
52+
53+
54+
@contextmanager
55+
def deferred_logging() -> Generator['DeferredLogHandler', None, None]:
56+
"""Context manager for temporary log buffering.
57+
58+
This replaces the root logger's handlers with a temporary buffer. Logs
59+
generated within this context are stored in memory and can be either flushed
60+
to the original handlers or discarded. Usefull for functions which produces
61+
a lot of log records and it is not known in advanced if those log
62+
entries will be kept or discarded (e.g. wait until functions).
63+
64+
Args:
65+
None
66+
67+
Yields:
68+
DeferredLogHandler: Temporary handler for storing buffered logs.
69+
"""
70+
root_logger = logging.getLogger()
71+
original_handlers = root_logger.handlers[:]
72+
buffer_handler = DeferredLogHandler()
73+
root_logger.handlers = [buffer_handler]
74+
try:
75+
yield buffer_handler
76+
finally:
77+
root_logger.handlers = original_handlers
78+
79+
80+
def truncate_long_text(text: str, max_size: int) -> str:
81+
"""Truncates long text if it exceeds the maximum size.
82+
83+
Args:
84+
text (str): Text to be truncate.
85+
max_size (int): Maximum allowed length. If -1, text is not truncated.
86+
87+
Returns:
88+
str: Final truncated text.
89+
"""
90+
if max_size == -1:
91+
return text
92+
93+
if len(text) > max_size:
94+
text = text[:max_size] + " ... (truncated long output)"
95+
96+
return text
97+
98+
2199
def verify_jsons_match(
22100
json1: str,
23101
json2: str,
@@ -170,11 +248,14 @@ def wait_until_function_returns_value_with_custom_value_validator(
170248
Any: Return value returend by last successful function call.
171249
"""
172250
last_exception = None
251+
logger_buffer = None
173252

174253
for retry_num in range(retry_count):
175254
try:
176-
result = function(*args, **kwargs)
255+
with deferred_logging() as logger_buffer:
256+
result = function(*args, **kwargs)
177257
if return_value_validator(result):
258+
logger_buffer.flush_to_target(log)
178259
return result
179260
else:
180261
raise AssertionError(
@@ -190,6 +271,8 @@ def wait_until_function_returns_value_with_custom_value_validator(
190271
log.debug(f"failed with: {e}")
191272
time.sleep(interval)
192273
else:
274+
if logger_buffer:
275+
logger_buffer.flush_to_target(log)
193276
raise AssertionError(
194277
f"Failed to execute "
195278
f"{function.__name__}({','.join([str(arg) for arg in args])} "
@@ -325,17 +408,22 @@ def verify_function_returns_value_which_passes_custom_value_validator_for_some_t
325408
Returns:
326409
Any: Return value returend by last successful function call.
327410
"""
411+
logger_buffer = None
412+
328413
for retry_num in range(retry_count):
329414
try:
330-
result = function(*args, **kwargs)
415+
with deferred_logging() as logger_buffer:
416+
result = function(*args, **kwargs)
331417
passed_value_validator = return_value_validator(result)
332418
except Exception as e:
419+
logger_buffer.flush_to_target(log)
333420
raise AssertionError(
334421
f"Function {function.__name__}"
335422
f"({','.join([str(arg) for arg in args])} {kwargs or ''}) "
336423
f"failed with the following error {e}"
337424
)
338425
if not passed_value_validator:
426+
logger_buffer.flush_to_target(log)
339427
raise AssertionError(
340428
f"Function {function.__name__}"
341429
f"({','.join([str(arg) for arg in args])} {kwargs or ''}) "
@@ -348,6 +436,7 @@ def verify_function_returns_value_which_passes_custom_value_validator_for_some_t
348436
)
349437
time.sleep(interval)
350438

439+
logger_buffer.flush_to_target(log)
351440
return result
352441

353442

tests/libraries/variables.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ def REST_API(self) -> str:
4040
TOPOLOGY_URL: str = "rests/data/network-topology:network-topology/topology"
4141
ENABLE_TCP_TW_REUSE: bool = False
4242
DEFAULT_PCEP_STATS_UPDATE_INTERVAL: int = 5
43+
MAX_HTTP_RESPONSE_BODY_LOG_SIZE: int = 500
4344

4445

4546
variables = Variables()

0 commit comments

Comments
 (0)