Skip to content

Commit 6cc6cfe

Browse files
committed
add LoggerStream to morebuiltins.logs to redirect sys.stdout/sys.stderr with prefix.
1 parent 6c11da4 commit 6cc6cfe

File tree

4 files changed

+301
-3
lines changed

4 files changed

+301
-3
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
1. fix writer.wait_closed() ConnectionResetError in ipc.py, proxy_checker.py, log_server.py
55
2. add `morebuiltins.funcs.debounce` decorator to debounce function calls.
66
3. add `sep` arg to `morebuiltins.utils.gen_id` to customize the separator between date and time.
7+
4. add `LoggerStream` to `morebuiltins.logs` to redirect sys.stdout/sys.stderr with prefix.
8+
1. Use `sys.stdout = LoggerStream()` to redirect all stdout to LoggerStream, which sends logs to the logging system / file / custom writer.
9+
2. `LoggerStream.replace_print_ctx`: A context manager to temporarily replace the print function within a block of code.
10+
1. add time prefix to each print line.
711

812
### 1.3.4 (2025-10-15)
913
1. fix `morebuiltins.utils.gen_id` wrong length issue.

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,8 @@ print(morebuiltins.__file__)
294294

295295
19.6 `ContextFilter` - A logging filter that injects context variables into extra of log records. ContextVar is used to manage context-specific data in a thread-safe / async-safe manner.
296296

297+
19.7 `LoggerStream` - LoggerStream constructor.
298+
297299

298300
<!-- end -->
299301

doc.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3031,3 +3031,60 @@ Example::
30313031
---
30323032

30333033

3034+
3035+
19.7 `LoggerStream` - LoggerStream constructor.
3036+
3037+
3038+
```python
3039+
3040+
Args:
3041+
skip_same_head (bool, optional): Whether to skip the same log head. Defaults to True.
3042+
True:
3043+
24-08-10 19:30:07 This is a log message.
3044+
This is another log message.
3045+
24-08-10 19:30:08 This is a new log message.
3046+
False:
3047+
24-08-10 19:30:07 This is a log message.
3048+
24-08-10 19:30:07 This is another log message.
3049+
3050+
Example::
3051+
3052+
# 1. Basic usage
3053+
logger = LoggerStream(skip_same_head=True)
3054+
logger.write("This is a log message.\n")
3055+
logger.write("This is another log message.\n")
3056+
logger.write("This is a new log message.\n")
3057+
3058+
# 2. Redirect sys.stdout
3059+
import sys
3060+
sys.stdout = LoggerStream(skip_same_head=False)
3061+
print("This is a log message.")
3062+
print("This is a log message.")
3063+
# 24-08-10 19:30:07 This is a log message.
3064+
# 24-08-10 19:30:07 This is a log message.
3065+
3066+
# 3. Overwrite built-in print function
3067+
LoggerStream.install_print()
3068+
print(123)
3069+
print(123)
3070+
# 24-08-10 19:30:07 123
3071+
# 123
3072+
LoggerStream.restore_print()
3073+
LoggerStream.install_print(writer=open("log.txt", "a").write)
3074+
3075+
# 4. Subclass and override writer method
3076+
class CustomLoggerStream(LoggerStream):
3077+
def __init__(self, skip_same_head=True):
3078+
super().__init__(skip_same_head=skip_same_head)
3079+
self.logger = setup_your_logger_somehow()
3080+
3081+
def writer(self, msg: str):
3082+
# Custom implementation to write log message
3083+
self.logger.info(msg)
3084+
3085+
```
3086+
3087+
3088+
---
3089+
3090+

morebuiltins/logs.py

Lines changed: 238 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import sys
66
import time
77
import typing
8+
from contextlib import contextmanager
89
from contextvars import ContextVar, copy_context
910
from functools import partial
1011
from gzip import GzipFile
@@ -25,6 +26,7 @@
2526
"RotatingFileWriter",
2627
"SizedTimedRotatingFileHandler",
2728
"ContextFilter",
29+
"LoggerStream",
2830
]
2931

3032

@@ -72,9 +74,7 @@ class LogHelper:
7274
logger.info("This is an info message")
7375
"""
7476

75-
DEFAULT_FORMAT = (
76-
"%(asctime)s | %(name)s | %(levelname)-5s | %(filename)-8s(%(lineno)s) - %(message)s"
77-
)
77+
DEFAULT_FORMAT = "%(asctime)s | %(name)s | %(levelname)-5s | %(filename)-8s(%(lineno)s) - %(message)s"
7878
DEFAULT_FORMATTER = logging.Formatter(DEFAULT_FORMAT)
7979
FILENAME_HANDLER_MAP: Dict[str, logging.Handler] = {}
8080

@@ -819,6 +819,211 @@ def filter(self, record):
819819
return True
820820

821821

822+
class LoggerStream:
823+
r"""LoggerStream constructor.
824+
825+
Args:
826+
skip_same_head (bool, optional): Whether to skip the same log head. Defaults to True.
827+
True:
828+
24-08-10 19:30:07 This is a log message.
829+
This is another log message.
830+
24-08-10 19:30:08 This is a new log message.
831+
False:
832+
24-08-10 19:30:07 This is a log message.
833+
24-08-10 19:30:07 This is another log message.
834+
835+
Example::
836+
837+
# 1. Basic usage
838+
logger = LoggerStream(skip_same_head=True)
839+
logger.write("This is a log message.\n")
840+
logger.write("This is another log message.\n")
841+
logger.write("This is a new log message.\n")
842+
843+
# 2. Redirect sys.stdout
844+
import sys
845+
sys.stdout = LoggerStream(skip_same_head=False)
846+
print("This is a log message.")
847+
print("This is a log message.")
848+
# 24-08-10 19:30:07 This is a log message.
849+
# 24-08-10 19:30:07 This is a log message.
850+
851+
# 3. Overwrite built-in print function
852+
LoggerStream.install_print()
853+
print(123)
854+
print(123)
855+
# 24-08-10 19:30:07 123
856+
# 123
857+
LoggerStream.restore_print()
858+
LoggerStream.install_print(writer=open("log.txt", "a").write)
859+
860+
# 4. Subclass and override writer method
861+
class CustomLoggerStream(LoggerStream):
862+
def __init__(self, skip_same_head=True):
863+
super().__init__(skip_same_head=skip_same_head)
864+
self.logger = setup_your_logger_somehow()
865+
866+
def writer(self, msg: str):
867+
# Custom implementation to write log message
868+
self.logger.info(msg)
869+
"""
870+
871+
BUILTINS_STDOUT = sys.stdout
872+
BUILTINS_STDERR = sys.stderr
873+
BUILTINS_PRINT = print
874+
875+
def __init__(self, skip_same_head=True, writer=None, flush=None, get_prefix=None):
876+
self.skip_same_head = skip_same_head
877+
self.writer = writer or self.default_writer
878+
self.flush = flush or self.default_flush
879+
self.get_prefix = get_prefix or self.default_get_prefix
880+
self._is_newline = True
881+
self._last_head = ""
882+
883+
def default_get_prefix(self):
884+
return time.strftime("%y-%m-%d %H:%M:%S ")
885+
886+
@classmethod
887+
def default_writer(cls, msg: str):
888+
cls.BUILTINS_STDOUT.write(msg)
889+
cls.BUILTINS_STDOUT.flush()
890+
891+
def write(self, buf):
892+
if self._is_newline:
893+
head = self.get_prefix()
894+
if self.skip_same_head:
895+
if head == self._last_head:
896+
head = ""
897+
else:
898+
self._last_head = head
899+
else:
900+
head = ""
901+
if buf.endswith("\n"):
902+
self._is_newline = True
903+
else:
904+
self._is_newline = False
905+
if head:
906+
msg = f"{head}{buf}"
907+
else:
908+
msg = buf
909+
self.writer(msg)
910+
911+
def default_flush(self):
912+
pass
913+
914+
@classmethod
915+
@contextmanager
916+
def replace_print_ctx(
917+
cls,
918+
skip_same_head=True,
919+
writer=BUILTINS_STDERR.write,
920+
flush=BUILTINS_STDERR.flush,
921+
get_prefix=None,
922+
):
923+
"""Context manager to replace the built-in print function temporarily.
924+
925+
Args:
926+
skip_same_head (bool, optional): Whether to skip the same log head. Defaults to True.
927+
writer (_type_, optional): Writer function. Defaults to BUILTINS_STDERR.write.
928+
flush (_type_, optional): Flush function. Defaults to BUILTINS_STDERR.flush.
929+
930+
Example::
931+
932+
with LoggerStream.replace_print_ctx(skip_same_head=False):
933+
print("This is a log message.")
934+
print("This is another log message.")
935+
# 24-08-10 19:30:07 This is a log message.
936+
# 24-08-10 19:30:07 This is another log message.
937+
# Back to original print function
938+
print("This is a normal print message.")
939+
# This is a normal print message.
940+
"""
941+
try:
942+
cls.install_print(
943+
skip_same_head=skip_same_head,
944+
writer=writer,
945+
flush=flush,
946+
get_prefix=get_prefix,
947+
)
948+
yield
949+
finally:
950+
cls.restore_print()
951+
952+
@classmethod
953+
def restore_print(cls):
954+
import builtins
955+
956+
builtins.print = cls.BUILTINS_PRINT
957+
958+
@classmethod
959+
def install_print(
960+
cls,
961+
skip_same_head=True,
962+
writer=BUILTINS_STDERR.write,
963+
flush=BUILTINS_STDERR.flush,
964+
get_prefix=None,
965+
):
966+
"""Install a custom print function that writes to LoggerStream.
967+
968+
Args:
969+
skip_same_head (bool, optional): Whether to skip logging the same message head. Defaults to True.
970+
writer (_type_, optional): Writer function. Defaults to BUILTINS_STDERR.write.
971+
flush (_type_, optional): Flush function. Defaults to BUILTINS_STDERR.flush.
972+
973+
Example::
974+
975+
# 1. Basic usage
976+
LoggerStream.install_print()
977+
print("This is a log message.")
978+
print("This is another log message.")
979+
# 24-08-10 19:30:07 This is a log message.
980+
# This is another log message.
981+
LoggerStream.restore_print()
982+
983+
# 2. Log to a file
984+
log_file = open("log.txt", "a", encoding="utf-8")
985+
LoggerStream.install_print(writer=log_file.write, flush=log_file.flush)
986+
print("This is a log message to file.")
987+
LoggerStream.restore_print()
988+
989+
# 3. Log to a custom logger
990+
logger = logging.getLogger("my_logger")
991+
LoggerStream.install_print(writer=logger.info)
992+
print("This is a log message to logger.")
993+
LoggerStream.restore_print()
994+
995+
# 4. Using context manager
996+
with LoggerStream.replace_print_ctx(skip_same_head=False):
997+
print("This is a log message.")
998+
print("This is another log message.")
999+
# 24-08-10 19:30:07 This is a log message.
1000+
# 24-08-10 19:30:07 This is another log message
1001+
# Back to original print function
1002+
print("This is a normal print message.")
1003+
# This is a normal print message.
1004+
"""
1005+
import builtins
1006+
1007+
logger_stream = cls(
1008+
skip_same_head=skip_same_head,
1009+
writer=writer,
1010+
flush=flush,
1011+
get_prefix=get_prefix,
1012+
)
1013+
1014+
def custom_print(*values, sep=" ", end="\n", file=None, flush=False):
1015+
if file is None:
1016+
# redirect to our logger_stream only when file is None
1017+
message = f"{sep.join(str(arg) for arg in values)}{end}"
1018+
logger_stream.write(message)
1019+
if flush:
1020+
logger_stream.flush()
1021+
else:
1022+
cls.BUILTINS_PRINT(*values, sep=sep, end=end, file=file, flush=flush)
1023+
1024+
builtins.print = custom_print
1025+
1026+
8221027
def test_LogHelper():
8231028
logger = LogHelper.bind_handler(
8241029
"app_test", filename="app_test.log", maxBytes=1, backupCount=2
@@ -863,6 +1068,32 @@ async def _test():
8631068
assert asyncio.run(_test()) is True
8641069

8651070

1071+
def test_LoggerStream():
1072+
cache = []
1073+
1074+
def get_prefix():
1075+
return "PREFIX "
1076+
1077+
def writer(msg: str):
1078+
cache.append(msg)
1079+
1080+
with LoggerStream.replace_print_ctx(
1081+
skip_same_head=True, writer=writer, get_prefix=get_prefix
1082+
):
1083+
print("123")
1084+
print("456")
1085+
assert cache == ["PREFIX 123\n", "456\n"], cache
1086+
cache.clear()
1087+
1088+
with LoggerStream.replace_print_ctx(
1089+
skip_same_head=False, writer=writer, get_prefix=get_prefix
1090+
):
1091+
print("123")
1092+
print("456")
1093+
assert cache == ["PREFIX 123\n", "PREFIX 456\n"], cache
1094+
cache.clear()
1095+
1096+
8661097
def test_utils():
8671098
test_LogHelper()
8681099
print(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()), "test_LogHelper passed")
@@ -871,6 +1102,10 @@ def test_utils():
8711102
time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()),
8721103
"test_AsyncQueueListener passed",
8731104
)
1105+
test_LoggerStream()
1106+
print(
1107+
time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()), "test_LoggerStream passed"
1108+
)
8741109

8751110

8761111
def test():

0 commit comments

Comments
 (0)