Rotary Logger  1.0.2
The middleware rotary logger
Loading...
Searching...
No Matches
entrypoint.py
Go to the documentation of this file.
1"""
2# +==== BEGIN rotary_logger =================+
3# LOGO:
4# ..........####...####..........
5# ......###.....#.#########......
6# ....##........#.###########....
7# ...#..........#.############...
8# ...#..........#.#####.######...
9# ..#.....##....#.###..#...####..
10# .#.....#.##...#.##..##########.
11# #.....##########....##...######
12# #.....#...##..#.##..####.######
13# .#...##....##.#.##..###..#####.
14# ..#.##......#.#.####...######..
15# ..#...........#.#############..
16# ..#...........#.#############..
17# ...##.........#.############...
18# ......#.......#.#########......
19# .......#......#.########.......
20# .........#####...#####.........
21# /STOP
22# PROJECT: rotary_logger
23# FILE: entrypoint.py
24# CREATION DATE: 29-10-2025
25# LAST Modified: 1:31:47 27-03-2026
26# DESCRIPTION:
27# A module that provides a universal python light on iops way of logging to files your program execution.
28# /STOP
29# COPYRIGHT: (c) Asperguide
30# PURPOSE: This is the code that will be called when the module is called as a program more than a library.
31# // AR
32# +==== END rotary_logger =================+
33"""
34import sys
35import signal
36import argparse
37from pathlib import Path
38
39try:
40 from . import constants as CONST
41 from .rotary_logger_cls import RotaryLogger
42except ImportError:
43 import constants as CONST
44 from rotary_logger_cls import RotaryLogger
45
46
47class Tee:
48 """Small helper used when running the package as a CLI entrypoint.
49
50 This convenience class wraps creation of a `RotaryLogger` when the
51 package is executed as a program (it mirrors behaviour of the
52 standalone `rotary` command). It also provides small runtime
53 configuration points such as whether to override existing logs,
54 to ignore keyboard interrupts, and which broken-pipe handling
55 policy to use.
56 """
57
59 self,
60 output_error: CONST.ErrorMode = CONST.ErrorMode.WARN_NO_PIPE
61 ) -> None:
62 """Initialise the entrypoint helper and configure the logger.
63
64 Parses CLI arguments, installs the SIGINT handler when requested,
65 and creates the underlying RotaryLogger instance.
66
67 Keyword Arguments:
68 output_error (CONST.ErrorMode): Policy for handling broken-pipe or stdout errors. Default: CONST.ErrorMode.WARN_NO_PIPE
69 """
70 self.output_error: CONST.ErrorMode = output_error
71 self.args = self._parse_args()
73
74 log_function_calls_stdin = False
75 log_function_calls_stdout = False
76 log_function_calls_stderr = False
77 program_log = False
78 program_debug_log = False
79 if self.args.verbose is True:
80 log_function_calls_stdin = True
81 log_function_calls_stdout = True
82 log_function_calls_stderr = True
83 program_log = True
84 program_debug_log = True
85
86 # Create and configure the logger. We do not start file logging here;
87 # the `run()` method will only call `start_logging()` when a folder
88 # or positional file argument was provided (matching traditional `tee`
89 # behaviour where no files => no file output).
90 self.rotary_logger: RotaryLogger = RotaryLogger(
91 log_to_file=False,
92 override=not self.args.append,
93 merge_streams=self.args.merge,
94 merge_stdin=self.args.merge_stdin,
95 capture_stdin=self.args.capture_stdin,
96 capture_stdout=self.args.capture_stdout,
97 capture_stderr=self.args.capture_stderr,
98 prefix_in_stream=self.args.prefix_in,
99 prefix_out_stream=self.args.prefix_out,
100 prefix_err_stream=self.args.prefix_err,
101 log_function_calls_stdin=log_function_calls_stdin,
102 log_function_calls_stdout=log_function_calls_stdout,
103 log_function_calls_stderr=log_function_calls_stderr,
104 program_log=program_log,
105 program_debug_log=program_debug_log,
106 )
107
108 def _parse_args(self):
109 """Parse command-line arguments for the tee entrypoint.
110
111 Returns:
112 The populated argparse.Namespace with the parsed arguments.
113 """
114 parser = argparse.ArgumentParser(
115 description="Python-powered tee replacement with rotation"
116 )
117 parser.add_argument(
118 "files", nargs="*", help="Destination log files (defaults to rotary_logger folder)"
119 )
120 parser.add_argument(
121 "-a", "--append", action="store_true",
122 help="Append to the output files instead of overwriting"
123 )
124 parser.add_argument(
125 "-m", "--merge", action="store_true",
126 help="Merge stdout and stderr into a single log file"
127 )
128 parser.add_argument(
129 "-mi", "--merge-stdin", action="store_true",
130 help="Merge stdin into the same file as stdout and stderr"
131 )
132 parser.add_argument(
133 "-i", "--ignore-interrupts", action="store_true",
134 help="Ignore Ctrl+C (SIGINT)"
135 )
136 parser.add_argument(
137 "-s", "--max-size", type=int, default=None,
138 help="Maximum log file size in MB before rotation"
139 )
140 parser.add_argument(
141 "-V", "--verbose", action="store_true",
142 help="Activate all debug logging options of the program"
143 )
144 # Additional options to control folder behaviour and prefixes
145 parser.add_argument(
146 "--log-folder", "-F", dest="log_folder", default=None,
147 help="Destination log folder (when omitted no file logging will occur)"
148 )
149 parser.add_argument(
150 "--create-folder", action="store_true", dest="create_folder", default=False,
151 help="Create the log folder if it does not exist (use with --log-folder or positional file)",
152 )
153
154 # Prefix toggles: disabled by default; use `--prefix-*` to enable
155 parser.set_defaults(
156 prefix_in=False, prefix_out=False, prefix_err=False)
157 parser.add_argument(
158 "--prefix-stdin", dest="prefix_in", action="store_true",
159 help="Prepend STDIN label to logged stdin entries"
160 )
161 parser.add_argument(
162 "--prefix-stdout", dest="prefix_out", action="store_true",
163 help="Prepend STDOUT label to logged stdout entries"
164 )
165 parser.add_argument(
166 "--prefix-stderr", dest="prefix_err", action="store_true",
167 help="Prepend STDERR label to logged stderr entries"
168 )
169
170 # Capture toggles: stdin capture is opt-in; stdout/stderr captured by default
171 parser.set_defaults(capture_stdout=True,
172 capture_stderr=True, capture_stdin=False)
173 parser.add_argument(
174 "--capture-stdin", dest="capture_stdin", action="store_true",
175 help="Capture stdin (wrap sys.stdin)"
176 )
177 parser.add_argument(
178 "--no-capture-stdout", dest="capture_stdout", action="store_false",
179 help="Do not capture stdout"
180 )
181 parser.add_argument(
182 "--no-capture-stderr", dest="capture_stderr", action="store_false",
183 help="Do not capture stderr"
184 )
185 return parser.parse_args()
186
188 """Ignore SIGINT (KeyboardInterrupt) when configured to do so.
189
190 Installs a SIG_IGN handler for SIGINT when the --ignore-interrupts
191 flag was passed on the command line. This is a no-op when interrupts
192 should be processed normally.
193 """
194 if self.args.ignore_interrupts:
195 signal.signal(signal.SIGINT, signal.SIG_IGN)
196
197 def _pipe_check(self) -> None:
198 """Apply the configured broken-pipe error policy.
199
200 Writes a warning to stderr or exits the process depending on the
201 value of self.output_error and whether stdout is currently a pipe.
202 """
203 if self.output_error == CONST.ErrorMode.WARN:
204 sys.stderr.write(CONST.BROKEN_PIPE_ERROR)
205 elif self.output_error == CONST.ErrorMode.EXIT:
206 sys.exit(CONST.ERROR)
207 elif self.output_error in (CONST.ErrorMode.WARN_NO_PIPE, CONST.ErrorMode.EXIT_NO_PIPE):
208 if not CONST.IS_PIPE:
209 if "WARN" in self.output_error.value:
210 sys.stderr.write(CONST.BROKEN_PIPE_ERROR)
211 else:
212 sys.exit(CONST.ERROR)
213
214 def run(self):
215 """Start logging and run the main stdin-to-stdout forwarding loop.
216
217 Behaves like UNIX tee: reads lines from stdin, prints them to
218 stdout (which is wrapped by RotaryLogger), and mirrors everything
219 to the configured log folder. Handles BrokenPipeError per the
220 configured error policy and KeyboardInterrupt when interrupts are
221 not suppressed.
222 """
223 # Determine whether file logging is requested. Behavior:
224 # - If `--log-folder` was provided, use it.
225 # - Else if a positional file was provided, use the first one.
226 # - Otherwise, do not enable file logging (matches `tee`).
227 if self.args.log_folder:
228 _log_to_file = True
229 _log_folder = Path(self.args.log_folder)
230 elif self.args.files:
231 _log_to_file = True
232 if len(self.args.files) > 1:
233 try:
234 sys.stderr.write(
235 f"{CONST.MODULE_NAME} Multiple destination files provided; using first: {self.args.files[0]}\n"
236 )
237 except OSError:
238 pass
239 _log_folder = Path(self.args.files[0])
240 else:
241 _log_to_file = False
242 _log_folder = None
243
244 # If a folder was requested but does not exist, either create it
245 # (when --create-folder) or warn and disable file logging.
246 if _log_to_file and _log_folder is not None and not _log_folder.exists():
247 if self.args.create_folder:
248 try:
249 _log_folder.mkdir(parents=True, exist_ok=True)
250 except OSError as e:
251 try:
252 sys.stderr.write(
253 f"{CONST.MODULE_NAME} Could not create log folder: {_log_folder} -> {e}\n")
254 except OSError:
255 pass
256 _log_to_file = False
257 _log_folder = None
258 else:
259 try:
260 sys.stderr.write(
261 f"{CONST.MODULE_NAME} Log folder does not exist: {_log_folder}. Use --create-folder to create it. File logging disabled.\n"
262 )
263 except OSError:
264 pass
265 _log_to_file = False
266 _log_folder = None
267
268 # Only start file logging when explicitly requested.
269 if _log_to_file:
270 self.rotary_logger.start_logging(
271 log_folder=_log_folder,
272 max_filesize=self.args.max_size,
273 merged=self.args.merge,
274 log_to_file=_log_to_file
275 )
276
277 try:
278 for line in sys.stdin:
279 try:
280 print(line, end="")
281 except BrokenPipeError:
282 self._pipe_check()
283 break
284 except KeyboardInterrupt:
285 if not self.args.ignore_interrupts:
286 raise
287 finally:
288 sys.stdout.flush()
289 sys.stderr.flush()
290
291
292def main():
293 """CLI entrypoint: create a Tee instance and run the forwarding loop."""
294 Tee().run()
295
296
297if __name__ == "__main__":
298 main()
None _handle_interrupts_if_required(self)
None __init__(self, CONST.ErrorMode output_error=CONST.ErrorMode.WARN_NO_PIPE)
Definition entrypoint.py:61
CONST.ErrorMode output_error
Definition entrypoint.py:70