logging revamp groundwork

This commit is contained in:
Eric 2022-09-01 10:47:59 -07:00
parent 2ddb0d6eb4
commit 3ef33a8467
No known key found for this signature in database
GPG Key ID: 89C93F0F8D6D5A98
8 changed files with 283 additions and 30 deletions

View File

@ -3995,26 +3995,26 @@
"assets/src/ba_data/python/ba/_generated/__init__.py": "https://files.ballistica.net/cache/ba1/ee/e8/cad05aa531c7faf7ff7b96db7f6e",
"assets/src/ba_data/python/ba/_generated/enums.py": "https://files.ballistica.net/cache/ba1/b2/e5/0ee0561e16257a32830645239f34",
"ballisticacore-windows/Generic/BallisticaCore.ico": "https://files.ballistica.net/cache/ba1/89/c0/e32c7d2a35dc9aef57cc73b0911a",
"build/prefab/full/linux_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/0a/56/252de9190ee6367ccbf37174783d",
"build/prefab/full/linux_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/f5/30/29f5a9d9cc5c6f5c76e3058d3621",
"build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/3e/e5/037d736cacd93a4b005cc93e72ad",
"build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/22/04/430aa3457c427f0814058c2b4483",
"build/prefab/full/linux_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/93/68/307719e44199480a5ee051d993f5",
"build/prefab/full/linux_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/9f/50/6f8a60e5375bf651bdebda617249",
"build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/cf/03/a5a5748fda33c876fbf3e8261b02",
"build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/82/3e/5725b87a8cc1e90f69bec58c65d5",
"build/prefab/full/mac_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/64/16/1589abfd35715bd2aa2915766148",
"build/prefab/full/mac_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/a8/36/584d685f3bea03753acf7344dfce",
"build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/3d/09/cbb451c2e8f856de61c0eafc5fdc",
"build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/b3/c9/9b3e221426dae6a047a893a4eb39",
"build/prefab/full/mac_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/61/2e/af3b07614ea2fb60f70b3d3b442a",
"build/prefab/full/mac_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/6e/fb/4b6e3e14ae9e329ae2a5c2eaab24",
"build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/86/3b/f8fc04eefa313d673ee98d20e360",
"build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/d1/83/544e088664612666bbaa6c1ff422",
"build/prefab/full/windows_x86_gui/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/79/13/29d322c6e8f7717ec87d5027bb20",
"build/prefab/full/windows_x86_gui/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/94/01/19f43fe2ee530d48f31665d22ff0",
"build/prefab/full/windows_x86_server/debug/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/17/e9/d6369d897f3595fbe03202887447",
"build/prefab/full/windows_x86_server/release/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/07/ff/cd46cba42a67cf31d6454b9eba7d",
"build/prefab/full/linux_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/8f/35/1a6fbc2bd9d367b5b5d8350199da",
"build/prefab/full/linux_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/e8/df/e7aae0645d3813227e32628a0ff3",
"build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/ce/43/1d18f5d73d3fe5d7f1ed4fdc472c",
"build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/cb/04/5dc6236fd0ebeafbd013299a4766",
"build/prefab/full/linux_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/e1/3a/bbe527aae553058d38a89a85e6b5",
"build/prefab/full/linux_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/60/1c/2e11dded6067b1cb27e6f6d48a0a",
"build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/59/fd/0f61ebccbcfb85d01693c431f5a6",
"build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/9b/94/8a16341d49d6de25102c07c70675",
"build/prefab/full/mac_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/ac/1d/d48d569a072d45d96ea86760b9f0",
"build/prefab/full/mac_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/d7/31/f3a671560f4efb8708430f0ce985",
"build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/6e/76/fa07d7183f1bd0d438657339d33a",
"build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/b0/8b/60a531e23f24bba638e3fc615ed3",
"build/prefab/full/mac_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/9c/1b/519b7ba8f1718787d8ab62f61222",
"build/prefab/full/mac_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/4b/a0/138ece248132798d69cc81bce581",
"build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/a1/58/18ba7845fb9524a5cfef4d14bf7f",
"build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/12/93/7e04d239fd333188b1412272c873",
"build/prefab/full/windows_x86_gui/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/44/cb/2144fb8e2fc054d605e8e3baea77",
"build/prefab/full/windows_x86_gui/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/89/90/a98081fbe24f8d062443ce84f3cc",
"build/prefab/full/windows_x86_server/debug/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/c9/80/1de60807e22d9a46c6902badbe7f",
"build/prefab/full/windows_x86_server/release/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/d7/f1/e2d6d8fdedd4ec4f3a6c0cc6bc14",
"build/prefab/lib/linux_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/3b/0c/2f4061ab877d415a1c30e0e736db",
"build/prefab/lib/linux_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/3c/5a/2b0714af254c64954ccfe51c70b3",
"build/prefab/lib/linux_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/1f/ae/c8a885b1a1868b6846b606cdb456",
@ -4031,14 +4031,14 @@
"build/prefab/lib/mac_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/44/df/efb51d1c226eac613d48e2cbf0b8",
"build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/1c/f6/357fe951c86c9fc5b1b737cd91ae",
"build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/04/17/e2de0ab5df6b938d828e8662ce6d",
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/69/dc/6fc1614b2548c6ac76c9e891c2e2",
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/c2/1b/263c5e001c6891d774d941f0bdfd",
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/7b/3a/f77ffca8d7c45b859d1e48c1b468",
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/88/15/1aa07f986d0bf7dac9a1f39635f2",
"build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/4b/e1/646d3095ab442e0b18d4c0de9689",
"build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/0f/fe/034c116781ddfe6cc89ada030056",
"build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/91/64/10fcd883cf0d15895d72a638e2ad",
"build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/1e/69/bf40bc8defe923cfa6d48cb5dd04",
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/97/5d/5255b7a90235bc570e71bfaf9f60",
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/07/0b/c65c6f6b009633a7cf66ea1c9e08",
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/81/09/10f7873ec315479806a9daa6e100",
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/0f/09/ab4219ed9d6b6b63439a33f38aac",
"build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/fd/ae/d0a4fb20969028322bab2ae2365d",
"build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/0b/cf/4b7529302b842bc75695f93c4172",
"build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/ad/b0/7d2ca14baad3fdb2aac296352570",
"build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/68/f8/156cbf9f5cf0fcbae9f12e8b18e8",
"src/ballistica/generated/python_embedded/binding.inc": "https://files.ballistica.net/cache/ba1/7d/3e/229a581cb2454ed856f1d8b564a7",
"src/ballistica/generated/python_embedded/bootstrap.inc": "https://files.ballistica.net/cache/ba1/aa/a5/3ddc86d1789b2bf1d376b7671a3d"
}

View File

@ -337,6 +337,7 @@
<w>capturetheflag</w>
<w>carentity</w>
<w>cashregistersound</w>
<w>cbegin</w>
<w>cbgn</w>
<w>cbits</w>
<w>cbot</w>
@ -699,6 +700,7 @@
<w>eachother</w>
<w>eaddrnotavail</w>
<w>easteregghunt</w>
<w>echofile</w>
<w>edcc</w>
<w>editcontroller</w>
<w>editgame</w>
@ -891,6 +893,7 @@
<w>floofcls</w>
<w>floooff</w>
<w>floop</w>
<w>flushhhhh</w>
<w>flycheck</w>
<w>fmod</w>
<w>fname</w>
@ -1391,6 +1394,7 @@
<w>listvalidconfigs</w>
<w>lival</w>
<w>llzma</w>
<w>lmap</w>
<w>lmerged</w>
<w>lmod</w>
<w>lmodfile</w>

View File

@ -1,4 +1,4 @@
### 1.7.7 (build 20723, api 7, 2022-08-26)
### 1.7.7 (build 20725, api 7, 2022-09-01)
- Added `ba.app.meta.load_exported_classes()` for loading classes discovered by the meta subsystem cleanly in a background thread.
- Improved logging of missing playlist game types.
- Some ba.Lstr functionality can now be used in background threads.

View File

@ -511,6 +511,7 @@
"ba_data/python/efro/__pycache__/__init__.cpython-310.opt-1.pyc",
"ba_data/python/efro/__pycache__/call.cpython-310.opt-1.pyc",
"ba_data/python/efro/__pycache__/error.cpython-310.opt-1.pyc",
"ba_data/python/efro/__pycache__/log.cpython-310.opt-1.pyc",
"ba_data/python/efro/__pycache__/rpc.cpython-310.opt-1.pyc",
"ba_data/python/efro/__pycache__/terminal.cpython-310.opt-1.pyc",
"ba_data/python/efro/__pycache__/util.cpython-310.opt-1.pyc",
@ -532,6 +533,7 @@
"ba_data/python/efro/dataclassio/_prep.py",
"ba_data/python/efro/dataclassio/extras.py",
"ba_data/python/efro/error.py",
"ba_data/python/efro/log.py",
"ba_data/python/efro/message/__init__.py",
"ba_data/python/efro/message/__pycache__/__init__.cpython-310.opt-1.pyc",
"ba_data/python/efro/message/__pycache__/_message.cpython-310.opt-1.pyc",

View File

@ -664,6 +664,7 @@ SCRIPT_TARGETS_PY_PUBLIC_TOOLS = \
build/ba_data/python/efro/dataclassio/_prep.py \
build/ba_data/python/efro/dataclassio/extras.py \
build/ba_data/python/efro/error.py \
build/ba_data/python/efro/log.py \
build/ba_data/python/efro/message/__init__.py \
build/ba_data/python/efro/message/_message.py \
build/ba_data/python/efro/message/_module.py \
@ -694,6 +695,7 @@ SCRIPT_TARGETS_PYC_PUBLIC_TOOLS = \
build/ba_data/python/efro/dataclassio/__pycache__/_prep.cpython-310.opt-1.pyc \
build/ba_data/python/efro/dataclassio/__pycache__/extras.cpython-310.opt-1.pyc \
build/ba_data/python/efro/__pycache__/error.cpython-310.opt-1.pyc \
build/ba_data/python/efro/__pycache__/log.cpython-310.opt-1.pyc \
build/ba_data/python/efro/message/__pycache__/__init__.cpython-310.opt-1.pyc \
build/ba_data/python/efro/message/__pycache__/_message.cpython-310.opt-1.pyc \
build/ba_data/python/efro/message/__pycache__/_module.cpython-310.opt-1.pyc \

View File

@ -188,6 +188,7 @@
<w>cancelbtn</w>
<w>capitan</w>
<w>cargs</w>
<w>cbegin</w>
<w>cbgn</w>
<w>cbresults</w>
<w>cbtnoffs</w>
@ -362,6 +363,7 @@
<w>dxgi</w>
<w>dynamicdata</w>
<w>echidna</w>
<w>echofile</w>
<w>edef</w>
<w>effmult</w>
<w>efro</w>
@ -460,6 +462,7 @@
<w>floooff</w>
<w>floop</w>
<w>flopsy</w>
<w>flushhhhh</w>
<w>fname</w>
<w>fnode</w>
<w>fnumc</w>
@ -703,6 +706,7 @@
<w>linkstoryboards</w>
<w>listobj</w>
<w>llock</w>
<w>lmap</w>
<w>localmodlibs</w>
<w>localns</w>
<w>lockpath</w>

View File

@ -21,7 +21,7 @@
namespace ballistica {
// These are set automatically via script; don't modify them here.
const int kAppBuildNumber = 20723;
const int kAppBuildNumber = 20725;
const char* kAppVersion = "1.7.7";
// Our standalone globals.

241
tools/efro/log.py Normal file
View File

@ -0,0 +1,241 @@
# Released under the MIT License. See LICENSE for details.
#
"""Logging functionality."""
from __future__ import annotations
import sys
import time
import logging
import datetime
import threading
from enum import Enum
from typing import TYPE_CHECKING, Annotated
from dataclasses import dataclass
from efro.util import utc_now
from efro.dataclassio import ioprepped, IOAttrs, dataclass_to_json
from efro.terminal import TerminalColor
if TYPE_CHECKING:
from typing import Any, Callable
from pathlib import Path
class LogLevel(Enum):
"""Severity level for a log entry.
Note: these are numeric values so they can be compared in severity.
"""
DEBUG = 0
INFO = 1
WARNING = 2
ERROR = 3
CRITICAL = 4
@ioprepped
@dataclass
class LogEntry:
"""Structured log entry."""
name: Annotated[str,
IOAttrs('n', soft_default='root', store_default=False)]
message: Annotated[str, IOAttrs('m')]
level: Annotated[LogLevel, IOAttrs('l')]
time: Annotated[datetime.datetime, IOAttrs('t')]
class StructuredLogHandler(logging.StreamHandler):
"""Fancy-pants handler for logging output.
Writes logs to disk in structured json format and echoes them
to stdout/stderr with pretty colors.
"""
def __init__(self,
path: str | Path | None,
echofile: Any,
suppress_non_root_debug: bool = False):
super().__init__()
# pylint: disable=consider-using-with
self._file = (None
if path is None else open(path, 'w', encoding='utf-8'))
self._echofile = echofile
self._callbacks: list[Callable[[LogEntry], None]] = []
self._suppress_non_root_debug = suppress_non_root_debug
def emit(self, record: logging.LogRecord) -> None:
if (self._suppress_non_root_debug and record.name != 'root'
and record.levelname == 'DEBUG'):
return
msg = self.format(record)
# Translate Python log levels to our own.
level = {
'DEBUG': LogLevel.DEBUG,
'INFO': LogLevel.INFO,
'WARNING': LogLevel.WARNING,
'ERROR': LogLevel.ERROR,
'CRITICAL': LogLevel.CRITICAL
}[record.levelname]
entry = LogEntry(message=msg,
name=record.name,
level=level,
time=datetime.datetime.fromtimestamp(
record.created, datetime.timezone.utc))
for call in self._callbacks:
call(entry)
# Also route log entries to the echo file (generally stdout)
if self._echofile is not None:
cbegin: str
cend: str
cbegin, cend = {
LogLevel.DEBUG:
(TerminalColor.CYAN.value, TerminalColor.RESET.value),
LogLevel.INFO: ('', ''),
LogLevel.WARNING:
(TerminalColor.YELLOW.value, TerminalColor.RESET.value),
LogLevel.ERROR:
(TerminalColor.RED.value, TerminalColor.RESET.value),
LogLevel.CRITICAL:
(TerminalColor.STRONG_MAGENTA.value +
TerminalColor.BOLD.value + TerminalColor.BG_BLACK.value,
TerminalColor.RESET.value),
}[level]
self._echofile.write(f'{cbegin}{msg}{cend}\n')
# Note to self: it sounds like logging wraps calls to us
# in a lock so we shouldn't have to worry about garbled
# json output due to multiple threads writing at once,
# but may be good to find out for sure?
if self._file is not None:
entry_s = dataclass_to_json(entry)
assert '\n' not in entry_s # make sure its a single line
print(entry_s, file=self._file, flush=True)
def emit_custom(self, name: str, message: str, level: LogLevel) -> None:
"""Custom emit call for our stdout/stderr redirection."""
entry = LogEntry(name=name,
message=message,
level=level,
time=utc_now())
# Inform anyone who wants to know about this log's level.
for call in self._callbacks:
call(entry)
if self._file is not None:
entry_s = dataclass_to_json(entry)
assert '\n' not in entry_s # Make sure its a single line.
print(entry_s, file=self._file, flush=True)
def add_callback(self, call: Callable[[LogEntry], None]) -> None:
"""Add a callback to be run for each added entry."""
self._callbacks.append(call)
class LogRedirect:
"""A file-like object for redirecting stdout/stderr to our log."""
def __init__(self, name: str, orig_out: Any,
log_handler: StructuredLogHandler, log_level: LogLevel):
self._name = name
self._orig_out = orig_out
self._log_handler = log_handler
self._log_level = log_level
self._chunk = ''
self._chunk_start_time = 0.0
self._lock = threading.Lock()
def write(self, s: str) -> None:
"""Write something to output."""
assert isinstance(s, str)
# First, ship it off to the original destination.
self._orig_out.write(s)
# Now add this to our chunk and ship completed chunks
# off to the logger.
# Let's consider a chunk completed when we're passed
# a single '\n' by itself. (print() statement will do
# this at the end by default).
# We may get some false positives/negatives this way
# but it should result in *most* big multi-line print
# statements being wrapped into a single log entry.
# Also, flush with only_old=True can be called periodically
# to dump any pending chunks that don't happen to fit
# this pattern.
with self._lock:
if s == '\n':
self._log_handler.emit_custom(name=self._name,
message=self._chunk,
level=self._log_level)
self._chunk = ''
else:
if self._chunk == '':
self._chunk_start_time = time.time()
self._chunk += s
def flush(self, only_old: bool = False) -> None:
"""Flushhhhh!"""
self._orig_out.flush()
if only_old and time.time() - self._chunk_start_time < 0.5:
return
with self._lock:
if self._chunk != '':
chunk = self._chunk
if chunk.endswith('\n'):
chunk = chunk[:-1]
self._log_handler.emit_custom(name=self._name,
message=chunk,
level=self._log_level)
self._chunk = ''
def setup_logging(
log_path: str | Path | None,
level: LogLevel,
suppress_non_root_debug: bool = False) -> StructuredLogHandler:
"""Set up our logging environment.
Returns the custom handler which can be used to fetch information
about logs that have passed through it. (worst log-levels, etc.).
"""
lmap = {
LogLevel.DEBUG: logging.DEBUG,
LogLevel.INFO: logging.INFO,
LogLevel.WARNING: logging.WARNING,
LogLevel.ERROR: logging.ERROR,
LogLevel.CRITICAL: logging.CRITICAL,
}
# Wire logger output to go to a structured log file.
# Also echo it to stderr IF we're running in a terminal.
loghandler = StructuredLogHandler(
log_path,
echofile=sys.stderr if sys.stderr.isatty() else None,
suppress_non_root_debug=suppress_non_root_debug)
logging.basicConfig(level=lmap[level],
format='%(message)s',
handlers=[loghandler])
# DISABLING THIS BIT FOR NOW - want to keep things as pure as possible.
if bool(False):
# Now wire Python stdout/stderr output to generate log entries
# in addition to its regular routing. Make sure to do this *after* we
# tell the log-handler to write to stderr, otherwise we get an infinite
# loop.
# NOTE: remember that this won't capture subcommands or other
# non-python stdout/stderr output.
sys.stdout = LogRedirect( # type: ignore
'stdout', sys.stdout, loghandler, LogLevel.INFO)
sys.stderr = LogRedirect( # type: ignore
'stderr', sys.stderr, loghandler, LogLevel.INFO)
return loghandler