Files
scylladb/test/pylib/log_browsing.py
Gleb Natapov 0a7101923c test: return file mark from wait_for that points after the found string
Returning file mark allows to start searching from the point where the
previous string was found.
2024-04-30 15:06:32 +03:00

103 lines
4.2 KiB
Python

# Copyright 2023-present ScyllaDB
#
# SPDX-License-Identifier: AGPL-3.0-or-later
import asyncio
from concurrent.futures import ThreadPoolExecutor
import logging
from typing import Optional
import pytest
import os
import re
logger = logging.getLogger(__name__)
class ScyllaLogFile():
"""
Class for browsing a Scylla log file.
Based on scylla-ccm implementation of log browsing.
"""
def __init__(self, thread_pool: ThreadPoolExecutor, logfile_path: str):
self.thread_pool = thread_pool # used for asynchronous IO operations
self.file = logfile_path
if not os.path.isfile(self.file):
pytest.fail("Log file {} does not exist".format(self.file))
async def _run_in_executor(self, func, *args, loop=None):
if loop is None:
loop = asyncio.get_running_loop()
return await loop.run_in_executor(self.thread_pool, func, *args)
async def mark(self) -> int:
"""
Returns "a mark" to the current position of this node Scylla log.
This is for use with the from_mark parameter of watch_log_for method,
allowing to watch the log from the position when this method was called.
"""
with open(self.file, 'r') as f:
await self._run_in_executor(f.seek, 0, os.SEEK_END)
return await self._run_in_executor(f.tell)
async def wait_for(self, pattern: str | re.Pattern, from_mark: Optional[int] = None, timeout: int = 600) -> int:
"""
wait_for() checks if the log contains the given message.
Because it may take time for the log message to be flushed, and sometimes
we may want to look for messages about various delayed events, this
function doesn't give up when it reaches the end of file, and rather
retries until a given timeout. The timeout may be long, because successful
tests will not wait for it. Note, however, that long timeouts will make
xfailing tests slow.
The timeout is in seconds.
If from_mark is given, the log is searched from that position, otherwise
from the beginning.
"""
prog = re.compile(pattern)
loop = asyncio.get_running_loop()
line = ""
with open(self.file, 'r') as f:
if from_mark is not None:
await self._run_in_executor(f.seek, from_mark, loop=loop)
async with asyncio.timeout(timeout):
logger.debug("Waiting for log message: %s", pattern)
while True:
line += await self._run_in_executor(f.readline, loop=loop)
if line:
if prog.search(line):
logger.debug("Found log message: %s", line)
return await self._run_in_executor(f.tell)
elif line[-1] != '\n':
continue
line = ""
else:
await asyncio.sleep(0.01)
async def grep(self, expr: str | re.Pattern, filter_expr: Optional[str | re.Pattern] = None,
from_mark: Optional[int] = None) -> list[tuple[str, re.Match[str]]]:
"""
Returns a list of lines matching the regular expression in the Scylla log.
The list contains tuples of (line, match), where line is the full line
from the log file, and match is the re.Match object for the matching
expression.
If filter_expr is given, only lines which do not match it are returned.
If from_mark is given, the log is searched from that position, otherwise
from the beginning.
"""
matchings = []
pattern = re.compile(expr)
filter_pattern = re.compile(filter_expr) if filter_expr else None
loop = asyncio.get_running_loop()
with open(self.file) as f:
if from_mark:
await self._run_in_executor(f.seek, from_mark, loop=loop)
line = await self._run_in_executor(f.readline, loop=loop)
while line:
m = pattern.search(line)
if m and not (filter_pattern and re.search(filter_pattern, line)):
matchings.append((line, m))
line = await self._run_in_executor(f.readline, loop=loop)
return matchings