2# +==== BEGIN rotary_logger =================+
4# ..........####...####..........
5# ......###.....#.#########......
6# ....##........#.###########....
7# ...#..........#.############...
8# ...#..........#.#####.######...
9# ..#.....##....#.###..#...####..
10# .#.....#.##...#.##..##########.
11# #.....##########....##...######
12# #.....#...##..#.##..####.######
13# .#...##....##.#.##..###..#####.
14# ..#.##......#.#.####...######..
15# ..#...........#.#############..
16# ..#...........#.#############..
17# ...##.........#.############...
18# ......#.......#.#########......
19# .......#......#.########.......
20# .........#####...#####.........
22# PROJECT: rotary_logger
23# FILE: rotary_logger.py
24# CREATION DATE: 29-10-2025
25# LAST Modified: 10:56:50 27-03-2026
27# A module that provides a universal python light on iops way of logging to files your program execution.
29# COPYRIGHT: (c) Asperguide
30# PURPOSE: This is the main file of the module, it contains the core code for the module.
32# +==== END rotary_logger =================+
39from warnings
import warn
40from pathlib
import Path
41from typing
import Any, Optional, List, Callable
42from threading
import RLock
45 from .
import constants
as CONST
46 from .tee_stream
import TeeStream
47 from .file_instance
import FileInstance
48 from .rogger
import Rogger, RI
50 import constants
as CONST
51 from tee_stream
import TeeStream
52 from file_instance
import FileInstance
53 from rogger
import Rogger, RI
57 """High-level coordinator that installs `TeeStream` wrappers.
60 - Validate and create the target log folder.
61 - Configure a `FileInstance` with encoding, prefix and rotation policy.
62 - Replace `sys.stdout` and `sys.stderr` with `TeeStream` instances.
67 log_to_file: bool = CONST.LOG_TO_FILE_ENV,
68 override: bool =
False,
69 raw_log_folder: str = CONST.RAW_LOG_FOLDER_ENV,
70 default_log_folder: Path = CONST.DEFAULT_LOG_FOLDER,
71 default_max_filesize: int = CONST.DEFAULT_LOG_MAX_FILE_SIZE,
72 merge_streams: bool =
True,
74 encoding: str = CONST.DEFAULT_ENCODING,
75 merge_stdin: bool =
False,
76 capture_stdin: bool =
False,
77 capture_stdout: bool =
True,
78 capture_stderr: bool =
True,
79 prefix_in_stream: bool =
True,
80 prefix_out_stream: bool =
True,
81 prefix_err_stream: bool =
True,
82 log_function_calls_stdin: bool =
False,
83 log_function_calls_stdout: bool =
False,
84 log_function_calls_stderr: bool =
False,
85 program_log: bool =
False,
86 program_debug_log: bool =
False,
87 suppress_program_warning_logs: bool =
False,
88 suppress_program_error_logs: bool =
False,
90 """Initialise a new RotaryLogger.
92 Does not start logging; call start_logging() to install TeeStream
93 wrappers and begin mirroring output.
96 log_to_file (bool): Whether file logging is enabled. Default: CONST.LOG_TO_FILE_ENV
97 override (bool): Whether existing log files may be overwritten. Default: False
98 raw_log_folder (str): Raw path string for the log folder. Default: CONST.RAW_LOG_FOLDER_ENV
99 default_log_folder (Path): Fallback log folder path. Default: CONST.DEFAULT_LOG_FOLDER
100 default_max_filesize (int): Maximum log file size in MB before rotation. Default: CONST.DEFAULT_LOG_MAX_FILE_SIZE
101 merge_streams (bool): Whether stdout and stderr share a single log file. Default: True
104 encoding (str): File encoding for log files. Default: CONST.DEFAULT_ENCODING
105 merge_stdin (bool): Whether stdin is merged into the shared log file. Default: False
106 capture_stdin (bool): Whether stdin is wrapped with a TeeStream. Default: False
107 capture_stdout (bool): Whether stdout is wrapped with a TeeStream. Default: True
108 capture_stderr (bool): Whether stderr is wrapped with a TeeStream. Default: True
109 prefix_in_stream (bool): Whether stdin entries are prefixed. Default: True
110 prefix_out_stream (bool): Whether stdout entries are prefixed. Default: True
111 prefix_err_stream (bool): Whether stderr entries are prefixed. Default: True
112 log_function_calls_stdin (bool): Whether TeeStream function calls on stdin are logged. Default: False
113 log_function_calls_stdout (bool): Whether TeeStream function calls on stdout are logged. Default: False
114 log_function_calls_stderr (bool): Whether TeeStream function calls on stderr are logged. Default: False
115 program_log (bool): Whether to let the module (rotary_logger) output status logs about what it is doing. Default: False
116 program_debug_log (bool): Whether to let the module (rotary_logger) output debug logs. Default: False
117 suppress_program_warning_logs (bool): Whether to prevent the module (rotary_logger) from outputing warnings (ex: initialising an already initialised stream). Default: False
118 suppress_program_error_logs (bool): Whether to prevent the module (rotary_logger) from outputing error (ex: a broken pipe). Default: False
127 self.
prefix: CONST.Prefix = CONST.Prefix()
128 self.
prefix.std_in = prefix_in_stream
129 self.
prefix.std_out = prefix_out_stream
130 self.
prefix.std_err = prefix_err_stream
137 self.
file_data.set_merge_stdin(merge_stdin)
170 self.
rogger.log_info(
"RotaryLogger initialized", stream=sys.stdout)
173 """Best-effort cleanup on object deletion.
175 Calls stop_logging() to restore original streams. Errors are not
176 raised since __del__ may run during interpreter shutdown.
180 def __call__(self, *args: Any, **kwds: Any) ->
None:
181 """Allow the instance to be called as a function to start logging.
183 Calling the instance is equivalent to calling start_logging() and
184 is provided for compact initialisation patterns.
187 *args (Any): Ignored positional arguments.
188 **kwds (Any): Ignored keyword arguments.
194 """Return the maximum log file size from the environment or the current default.
196 Reads the `LOG_MAX_SIZE` environment variable and coerces it to an
197 integer. Falls back to the value stored in `file_data` if the variable
198 is absent or non-numeric.
201 The resolved maximum log file size in bytes (as stored by `FileInstance`).
203 default_max_log_size: int = self.
file_data.get_max_size()
208 str(default_max_log_size)
212 f
"Resolved user max file size: {val}",
218 f
"Invalid LOG_MAX_SIZE env value, falling back to default: {default_max_log_size}",
221 return default_max_log_size
224 """Validate, resolve and ensure writability of the requested log folder.
226 Resolves relative paths against the package directory, appends the
227 standard base-folder name when missing, and performs a write-test.
228 Falls back to the default log folder on any validation failure.
231 raw_log_folder (Path): Candidate log folder path. Default: CONST.DEFAULT_LOG_FOLDER
234 RuntimeError: If both the requested path and the default fallback are not writable.
237 The validated, writable, resolved log folder path.
244 raw = Path(raw_log_folder)
245 if raw.is_absolute():
246 candidate = raw.resolve(strict=
False)
249 Path(__file__).parent /
251 ).resolve(strict=
False)
254 if candidate.name != CONST.LOG_FOLDER_BASE_NAME:
255 candidate = candidate / CONST.LOG_FOLDER_BASE_NAME
258 if len(str(candidate)) > 255:
259 raise ValueError(f
"{CONST.MODULE_NAME} Path too long")
264 candidate.mkdir(parents=
True, exist_ok=
True)
265 testfile = candidate /
".rotary_write_test"
266 with open(testfile,
"w", encoding=
"utf-8")
as fh:
270 f
"Verified writable log folder: {candidate}",
275 f
"Log folder not writable: {candidate} -> {e}",
279 f
"{CONST.MODULE_NAME} Path not writable: {e}")
from e
282 except ValueError
as e:
284 f
"Invalid LOG_FOLDER_NAME ({raw_log_folder!r}): {e}. Falling back to default.",
288 f
"{CONST.MODULE_NAME} [WARN] Invalid LOG_FOLDER_NAME ({raw_log_folder!r}): {e}. Falling back to default."
291 CONST.DEFAULT_LOG_FOLDER.mkdir(parents=
True, exist_ok=
True)
293 f
"Falling back to default log folder: {CONST.DEFAULT_LOG_FOLDER}",
296 except OSError
as err:
298 f
"{CONST.MODULE_NAME} The provided and default folder paths are not writable"
300 return CONST.DEFAULT_LOG_FOLDER
303 """Resolve and verify the final log folder to use.
305 Centralises the logic of falling back to the configured default and
306 delegates validation to _verify_user_log_path().
309 log_folder (Optional[Path]): Requested log folder, or None to use the default.
312 The validated, writable, resolved log folder path.
315 if log_folder
is None:
320 """Create `FileInstance` objects and store them in `self._file_stream_instances`.
322 Reads the current configuration snapshot from `self.file_data` (outside the
323 lock) and constructs either a single shared `FileInstance` (when `merged` is True)
324 or three separate per-stream instances for stdin, stdout, and stderr.
326 When merged, stdout and stderr share the same descriptor. stdin is also merged
327 into that file when `merge_stdin` is True; otherwise its own unmerged instance
328 is created. Merged/unmerged state is recorded in
329 `self._file_stream_instances.merged_streams`.
332 log_folder (Path): The validated, writable root folder for log files.
335 _override = self.
file_data.get_override()
336 _encoding = self.
file_data.get_encoding()
338 _max_size_mb = self.
file_data.get_max_size()
339 _flush_size_kb = self.
file_data.get_flush_size()
340 _merged_flag = self.
file_data.get_merged()
341 _merge_stdin_flag = self.
file_data.get_merge_stdin()
344 f
"Handling stream assignments (merged={_merged_flag}, merge_stdin={_merge_stdin_flag})",
349 file_path=log_folder,
354 max_size_mb=_max_size_mb,
355 flush_size_kb=_flush_size_kb,
357 merge_stdin=_merge_stdin_flag,
365 if _merge_stdin_flag:
370 file_path=log_folder,
375 max_size_mb=_max_size_mb,
376 flush_size_kb=_flush_size_kb,
377 folder_prefix=CONST.StdMode.STDIN,
382 f
"Created merged FileInstance for stdout/stderr at {log_folder}",
387 file_path=log_folder,
392 max_size_mb=_max_size_mb,
393 flush_size_kb=_flush_size_kb,
394 folder_prefix=CONST.StdMode.STDIN,
399 file_path=log_folder,
404 max_size_mb=_max_size_mb,
405 flush_size_kb=_flush_size_kb,
406 folder_prefix=CONST.StdMode.STDOUT,
407 merge_stdin=_merge_stdin_flag,
411 file_path=log_folder,
416 max_size_mb=_max_size_mb,
417 flush_size_kb=_flush_size_kb,
418 folder_prefix=CONST.StdMode.STDERR,
419 merge_stdin=_merge_stdin_flag,
427 f
"Created separate FileInstance objects for each stream at {log_folder}",
434 log_folder: Optional[Path] =
None,
435 max_filesize: Optional[int] =
None,
436 merged: Optional[bool] =
None,
437 log_to_file: bool =
True,
438 merge_stdin: Optional[bool] =
None,
439 skip_redirect_check_stdin: bool =
False,
440 skip_redirect_check_stdout: bool =
False,
441 skip_redirect_check_stderr: bool =
False,
443 """Start capturing stdout and stderr and configure file output.
445 Installs TeeStream wrappers for sys.stdout and sys.stderr so output
446 continues to appear on the terminal while being mirrored to rotating
447 files on disk. Configuration snapshots are taken under the internal
448 lock; filesystem operations (mkdir, write-test) are performed outside
449 it to keep critical sections short. The sys.* assignments are made
450 while holding the lock to keep the replacement atomic.
453 log_folder (Optional[Path]): Base folder to write logs; falls back to configured defaults. Default: None
454 max_filesize (Optional[int]): Override for the rotation size in MB. Default: None
455 merged (Optional[bool]): Whether to merge stdout and stderr into one file. Default: None
456 log_to_file (bool): Whether file writes are enabled. Default: True
457 merge_stdin (Optional[bool]): Whether to merge stdin into the shared log file. Default: None
458 skip_redirect_check_stdin (bool, optional): Skip the existing redirection check for stdin, allowing multiple logger instances to redirect the same stream (legacy behavior). Default: False
459 skip_redirect_check_stdout (bool, optional): Skip the existing redirection check for stdout, allowing multiple logger instances to redirect the same stream (legacy behavior). Default: False,
460 skip_redirect_check_stderr (bool, optional): Skip the existing redirection check for stderr, allowing multiple logger instances to redirect the same stream (legacy behavior). Default: False,
465 f
"start_logging called (log_folder={log_folder}, max_filesize={max_filesize}, merged={merged}, log_to_file={log_to_file})",
472 if log_folder
is None:
478 _raw_folder = log_folder
480 if max_filesize
is not None:
481 self.
file_data.set_max_size(max_filesize)
486 if merged
is not None:
488 if merge_stdin
is not None:
489 self.
file_data.set_merge_stdin(merge_stdin)
495 f
"Self Log to file = {self.log_to_file}, Log to file = {log_to_file}"
503 if log_to_file
is True:
505 _log_folder.mkdir(parents=
True, exist_ok=
True)
510 candidate = Path(_raw_folder)
511 if candidate.name != CONST.LOG_FOLDER_BASE_NAME:
512 candidate = candidate / CONST.LOG_FOLDER_BASE_NAME
513 _log_folder = candidate.resolve(strict=
False)
521 _stdin_stream: Optional[TeeStream] =
None
522 _stdout_stream: Optional[TeeStream] =
None
523 _stderr_stream: Optional[TeeStream] =
None
525 if skip_redirect_check_stdin:
527 "Skipping redirect check for stdin because 'skip_redirect_check_stdin' is True",
528 stream=CONST.RAW_STDERR
530 if not skip_redirect_check_stdin
and isinstance(sys.stdin, TeeStream):
532 "Stdin stream is already being redirected, skipping redirection",
533 stream=CONST.RAW_STDERR
535 _stdin_stream = sys.stdin
538 "Stdin is not yet being redirected, redirecting",
539 stream=CONST.RAW_STDOUT
542 f
"(stdin) Log to file: {log_to_file}"
547 mode=CONST.StdMode.STDIN,
548 log_to_file=log_to_file,
552 "Constructed TeeStream for stdin",
553 stream=CONST.RAW_STDOUT
556 if skip_redirect_check_stdout:
558 "Skipping redirect check for stdout because 'skip_redirect_check_stdin' is True",
559 stream=CONST.RAW_STDERR
561 if not skip_redirect_check_stdout
and isinstance(sys.stdout, TeeStream):
563 "Stdout stream is already being redirected, skipping redirection",
564 stream=CONST.RAW_STDERR
566 _stdout_stream = sys.stdout
569 "Stdout is not yet being redirected, redirecting",
570 stream=CONST.RAW_STDOUT
573 f
"(stdout) Log to file: {log_to_file}",
574 stream=CONST.RAW_STDOUT
579 mode=CONST.StdMode.STDOUT,
580 log_to_file=log_to_file,
584 "Constructed TeeStream for stdout",
588 if skip_redirect_check_stderr:
590 "Skipping redirect check for stderr because 'skip_redirect_check_stderr' is True",
591 stream=CONST.RAW_STDERR
593 if not skip_redirect_check_stderr
and isinstance(sys.stderr, TeeStream):
595 "Stderr stream is already being redirected, skipping redirection",
596 stream=CONST.RAW_STDERR
598 _stderr_stream = sys.stderr
601 "Stderr is not yet being redirected, redirecting",
602 stream=CONST.RAW_STDOUT
605 f
"(stderr) Log to file: {log_to_file}"
610 mode=CONST.StdMode.STDERR,
611 log_to_file=log_to_file,
615 "Constructed TeeStream for stderr",
616 stream=CONST.RAW_STDOUT
621 "redirecting streams",
622 stream=CONST.RAW_STDOUT
625 f
"Will assign streams: stdin={bool(_stdin_stream)}, stdout={bool(_stdout_stream)}, stderr={bool(_stderr_stream)}",
626 stream=CONST.RAW_STDOUT
629 sys.stdin = _stdin_stream
632 sys.stdout = _stdout_stream
635 sys.stderr = _stderr_stream
651 except (TypeError, AttributeError):
658 "Registered atexit flush handlers",
659 stream=CONST.RAW_STDOUT
663 """Restore TeeStream wrappers on sys.stdin/stdout/stderr.
665 Must be called while `self._file_lock` is already held. Sets
666 `self.paused` to False and reassigns `sys.stdout`, `sys.stderr`,
667 and `sys.stdin` to their respective TeeStream instances. Each
668 stream that is reinstalled is appended to `to_flush` so the
669 caller can flush them after releasing the lock.
672 to_flush (List[TeeStream]): Accumulator list; streams to flush after the lock is released.
676 "Resuming logging (reinstalling TeeStream wrappers)",
677 stream=CONST.RAW_STDOUT
693 "Reinstalled stdin TeeStream",
694 stream=CONST.RAW_STDOUT
698 """Replace TeeStream wrappers with the original standard streams.
700 Must be called while `self._file_lock` is already held. Sets
701 `self.paused` to True and reassigns `sys.stdout`, `sys.stderr`,
702 and `sys.stdin` back to their original (pre-TeeStream) counterparts.
703 Each stream that is uninstalled is appended to `to_flush` so the
704 caller can flush buffered data after releasing the lock.
707 to_flush (List[TeeStream]): Accumulator list; streams to flush after the lock is released.
711 "Pausing logging (restoring original streams)",
712 stream=CONST.RAW_STDOUT
719 sys.stdout = out.original_stream
722 "Uninstalled stdout TeeStream",
723 stream=CONST.RAW_STDOUT
726 sys.stderr = err.original_stream
729 "Uninstalled stderr TeeStream",
730 stream=CONST.RAW_STDOUT
733 sys.stdin = inn.original_stream
736 "Uninstalled stdin TeeStream",
737 stream=CONST.RAW_STDOUT
741 """Flush a list of TeeStream instances, suppressing expected I/O errors.
743 Iterates over `to_flush` and calls `flush()` on each stream. `OSError`
744 and `ValueError` (e.g. broken pipe, closed file descriptor) are caught
745 and silently ignored; all other exceptions propagate.
748 to_flush (List[TeeStream]): Streams to flush.
754 f
"Flushing stream: {getattr(s, 'mode', 'unknown')}",
755 stream=CONST.RAW_STDOUT
759 f
"Flushed stream: {getattr(s, 'mode', 'unknown')}",
760 stream=CONST.RAW_STDOUT
762 except (OSError, ValueError)
as e:
764 f
"Ignored flush error for stream: {getattr(s, 'mode', 'unknown')} -> {e}",
765 stream=CONST.RAW_STDERR
769 """Toggle the logger pause state.
771 When the logger is paused the TeeStream objects are uninstalled and
772 the original streams restored. When called again the TeeStream objects
773 are reinstalled. sys.* assignments are performed while holding the
774 internal lock; flushing is done afterwards to keep critical sections
778 toggle (bool): When True and the logger is currently running, pause it; when True and already paused, resume it. When False, always pause. Default: True
781 The new paused state (True when now paused, False when now resumed).
787 if toggle
is True and self.
paused is True:
789 "pause_logging: toggle requested and currently paused -> resume",
790 stream=CONST.RAW_STDOUT
795 "pause_logging: pausing or toggling into pause",
796 stream=CONST.RAW_STDOUT
804 """Explicitly resume logging (idempotent).
806 Equivalent to calling pause_logging() while paused, but provided as
807 a convenience. sys.* assignments are made under the internal lock;
808 flushing is done afterwards.
811 toggle (bool): When True and the logger is not paused, pause it instead of resuming. When False, always resume. Default: False
814 The paused state after the call (False when logging was resumed, True when toggled into pause).
818 if toggle
is True and self.
paused is False:
820 "resume_logging: toggle requested and currently running -> pause",
821 stream=CONST.RAW_STDOUT
826 "resume_logging: resuming logging",
827 stream=CONST.RAW_STDOUT
835 """Return whether the given standard stream is currently redirected.
837 Lightweight query; safe to call concurrently.
840 stream (CONST.StdMode): One of CONST.StdMode.STDOUT, STDERR, or STDIN.
843 True if the corresponding stream has a TeeStream installed, False otherwise.
845 _stderr_stream: Optional[TeeStream] =
None
846 _stdout_stream: Optional[TeeStream] =
None
847 _stdin_stream: Optional[TeeStream] =
None
852 if stream == CONST.StdMode.STDERR:
853 return _stderr_stream
is not None
854 if stream == CONST.StdMode.STDOUT:
855 return _stdout_stream
is not None
856 if stream == CONST.StdMode.STDIN:
857 return _stdin_stream
is not None
861 """Return True if logging is currently active (not paused).
863 Checks whether any TeeStream is installed and the logger is not
864 marked as paused. Safe to call concurrently.
867 True if at least one TeeStream is installed and the logger is not paused.
878 return has_stream
and (
not bool(self.
paused))
881 """Stop logging and restore the original standard streams.
883 Restores sys.stdout, sys.stderr, and sys.stdin to their original
884 values, attempts to unregister any atexit flush handlers registered
885 by start_logging(), and flushes remaining buffers. Stream replacement
886 and atexit unregistration are done under the internal lock; flushing
887 is performed afterwards.
905 if getattr(self,
"_atexit_registered",
False):
907 "Unregistering atexit flush handlers",
910 for f
in getattr(self,
"_registered_flushers", []):
915 except AttributeError:
920 "Unregistered atexit flush handlers",
921 stream=CONST.RAW_STDOUT
927 f
"stop_logging: flushing stream {getattr(s, 'mode', 'unknown')}",
928 stream=CONST.RAW_STDOUT
931 except (OSError, ValueError):
933 f
"stop_logging: ignored flush error for {getattr(s, 'mode', 'unknown')}",
934 stream=CONST.RAW_STDERR
938if __name__ ==
"__main__":
int _get_user_max_file_size(self)
None _resume_logging_locked(self, List[TeeStream] to_flush)
None _handle_stream_assignments(self, Path log_folder)
None __init__(self, bool log_to_file=CONST.LOG_TO_FILE_ENV, bool override=False, str raw_log_folder=CONST.RAW_LOG_FOLDER_ENV, Path default_log_folder=CONST.DEFAULT_LOG_FOLDER, int default_max_filesize=CONST.DEFAULT_LOG_MAX_FILE_SIZE, bool merge_streams=True, *, str encoding=CONST.DEFAULT_ENCODING, bool merge_stdin=False, bool capture_stdin=False, bool capture_stdout=True, bool capture_stderr=True, bool prefix_in_stream=True, bool prefix_out_stream=True, bool prefix_err_stream=True, bool log_function_calls_stdin=False, bool log_function_calls_stdout=False, bool log_function_calls_stderr=False, bool program_log=False, bool program_debug_log=False, bool suppress_program_warning_logs=False, bool suppress_program_error_logs=False)
suppress_program_warning_logs
list _registered_flushers
Path _resolve_log_folder(self, Optional[Path] log_folder)
Optional[TeeStream] stdin_stream
None _flush_streams(self, List[TeeStream] to_flush)
bool resume_logging(self, *, bool toggle=False)
Optional[TeeStream] stdout_stream
suppress_program_error_logs
CONST.FileStreamInstances _file_stream_instances
log_function_calls_stdout
bool pause_logging(self, *, bool toggle=True)
Optional[TeeStream] _file_lock
log_function_calls_stderr
Path _verify_user_log_path(self, Path raw_log_folder=CONST.DEFAULT_LOG_FOLDER)
None __call__(self, *Any args, **Any kwds)
None _pause_logging_locked(self, List[TeeStream] to_flush)
bool is_redirected(self, CONST.StdMode stream)
None start_logging(self, *, Optional[Path] log_folder=None, Optional[int] max_filesize=None, Optional[bool] merged=None, bool log_to_file=True, Optional[bool] merge_stdin=None, bool skip_redirect_check_stdin=False, bool skip_redirect_check_stdout=False, bool skip_redirect_check_stderr=False)
Optional[TeeStream] stderr_stream