userver: en/testsuite/_internal/logreader.py Source File
Loading...
Searching...
No Matches
logreader.py
1import pathlib
2import sys
3import threading
4import typing
5
6
7class LogFile:
8 def __init__(self, path: pathlib.Path):
9 self._path = path
10 self._position = 0
11
12 @property
13 def path(self):
14 return self._path
15
16 @property
17 def position(self):
18 return self._position
19
20 def filesize(self) -> int:
21 try:
22 return self._path.stat().st_size
23 except FileNotFoundError:
24 return 0
25
26 def update_position(self):
27 self._position = self.filesize()
28 return self._position
29
30 def readlines(
31 self,
32 eof_handler: typing.Callable[[], bool] | None = None,
33 limit_position: bool = False,
34 ):
35 if limit_position:
36 max_position = self.filesize()
37 else:
38 max_position = None
39 first_skipped = False
40 for line, position in _raw_line_reader(
41 self._path,
42 self._position,
43 eof_handler=eof_handler,
44 ):
45 self._position = position
46 yield line
47 if max_position is not None and position >= max_position:
48 break
49
50
52 def __init__(self, *, delay: float = 0.05):
53 self._threads = {}
54 self._exiting = False
55 self._delay = delay
56 self._condition = threading.Condition()
57
58 def register_logfile(self, path: pathlib.Path, *, formatter):
59 if path in self._threads:
60 return
61 thread = threading.Thread(
62 target=self._logreader_thread,
63 args=(path,),
64 kwargs={'formatter': formatter},
65 )
66 self._threads[path] = thread
67 thread.start()
68
69 def join(self, timeout: float = 10):
70 with self._condition:
71 self._exiting = True
72 self._condition.notify_all()
73 for thread in self._threads.values():
74 thread.join(timeout=timeout)
75
76 def _logreader_thread(self, path: pathlib.Path, formatter):
77 while not path.exists():
78 # wait for file to appear
79 if self._eof_handler():
80 return
81 logfile = LogFile(path)
82 for line in logfile.readlines(eof_handler=self._eof_handler):
83 line = line.rstrip(b'\r\n')
84 line = formatter(line)
85 if line:
86 self._write_logline(line)
87
88 def _write_logline(self, line: str):
89 print(line, file=sys.stderr)
90
91 def _eof_handler(self) -> bool:
92 with self._condition:
93 if self._condition.wait_for(
94 lambda: self._exiting, timeout=self._delay
95 ):
96 return True
97 return False
98
99
100def _raw_line_reader(
101 path: pathlib.Path,
102 position: int = 0,
103 eof_handler: typing.Callable[[], bool] | None = None,
104) -> typing.Iterator[tuple[bytes, int]]:
105 if not path.exists():
106 return
107
108 with path.open('rb') as fp:
109 position = fp.seek(position)
110 partial = None
111 while True:
112 for line in fp:
113 if partial:
114 line = partial + line
115 partial = None
116 if line.endswith(b'\n'):
117 position += len(line)
118 yield line, position
119 else:
120 partial = line
121 if not eof_handler or eof_handler():
122 break