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
"""
34
import
sys
35
import
signal
36
import
argparse
37
from
pathlib
import
Path
38
39
try
:
40
from
.
import
constants
as
CONST
41
from
.rotary_logger_cls
import
RotaryLogger
42
except
ImportError:
43
import
constants
as
CONST
44
from
rotary_logger_cls
import
RotaryLogger
45
46
47
class
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
58
def
__init__
(
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
()
72
self.
_handle_interrupts_if_required
()
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
187
def
_handle_interrupts_if_required
(self) -> None:
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
292
def
main
():
293
"""CLI entrypoint: create a Tee instance and run the forwarding loop."""
294
Tee
().run()
295
296
297
if
__name__ ==
"__main__"
:
298
main()
rotary_logger.entrypoint.Tee
Definition
entrypoint.py:47
rotary_logger.entrypoint.Tee._handle_interrupts_if_required
None _handle_interrupts_if_required(self)
Definition
entrypoint.py:187
rotary_logger.entrypoint.Tee._pipe_check
None _pipe_check(self)
Definition
entrypoint.py:197
rotary_logger.entrypoint.Tee._parse_args
_parse_args(self)
Definition
entrypoint.py:108
rotary_logger.entrypoint.Tee.run
run(self)
Definition
entrypoint.py:214
rotary_logger.entrypoint.Tee.__init__
None __init__(self, CONST.ErrorMode output_error=CONST.ErrorMode.WARN_NO_PIPE)
Definition
entrypoint.py:61
rotary_logger.entrypoint.Tee.args
args
Definition
entrypoint.py:71
rotary_logger.entrypoint.Tee.rotary_logger
RotaryLogger rotary_logger
Definition
entrypoint.py:90
rotary_logger.entrypoint.Tee.output_error
CONST.ErrorMode output_error
Definition
entrypoint.py:70
rotary_logger.rotary_logger_cls.RotaryLogger
Definition
rotary_logger_cls.py:56
rotary_logger.entrypoint.main
main()
Definition
entrypoint.py:292
rotary_logger
entrypoint.py
Generated on Fri Mar 27 2026 11:28:32 for Rotary Logger by
1.12.0