bear-utils 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bear_utils/__init__.py +51 -0
- bear_utils/__main__.py +14 -0
- bear_utils/_internal/__init__.py +0 -0
- bear_utils/_internal/_version.py +1 -0
- bear_utils/_internal/cli.py +119 -0
- bear_utils/_internal/debug.py +174 -0
- bear_utils/ai/__init__.py +30 -0
- bear_utils/ai/ai_helpers/__init__.py +136 -0
- bear_utils/ai/ai_helpers/_common.py +19 -0
- bear_utils/ai/ai_helpers/_config.py +24 -0
- bear_utils/ai/ai_helpers/_parsers.py +194 -0
- bear_utils/ai/ai_helpers/_types.py +15 -0
- bear_utils/cache/__init__.py +131 -0
- bear_utils/cli/__init__.py +22 -0
- bear_utils/cli/_args.py +12 -0
- bear_utils/cli/_get_version.py +207 -0
- bear_utils/cli/commands.py +105 -0
- bear_utils/cli/prompt_helpers.py +186 -0
- bear_utils/cli/shell/__init__.py +1 -0
- bear_utils/cli/shell/_base_command.py +81 -0
- bear_utils/cli/shell/_base_shell.py +430 -0
- bear_utils/cli/shell/_common.py +19 -0
- bear_utils/cli/typer_bridge.py +90 -0
- bear_utils/config/__init__.py +13 -0
- bear_utils/config/config_manager.py +229 -0
- bear_utils/config/dir_manager.py +69 -0
- bear_utils/config/settings_manager.py +179 -0
- bear_utils/constants/__init__.py +90 -0
- bear_utils/constants/_exceptions.py +8 -0
- bear_utils/constants/_exit_code.py +60 -0
- bear_utils/constants/_http_status_code.py +37 -0
- bear_utils/constants/_lazy_typing.py +15 -0
- bear_utils/constants/_meta.py +196 -0
- bear_utils/constants/date_related.py +25 -0
- bear_utils/constants/time_related.py +24 -0
- bear_utils/database/__init__.py +8 -0
- bear_utils/database/_db_manager.py +98 -0
- bear_utils/events/__init__.py +18 -0
- bear_utils/events/events_class.py +52 -0
- bear_utils/events/events_module.py +74 -0
- bear_utils/extras/__init__.py +28 -0
- bear_utils/extras/_async_helpers.py +67 -0
- bear_utils/extras/_tools.py +185 -0
- bear_utils/extras/_zapper.py +399 -0
- bear_utils/extras/platform_utils.py +57 -0
- bear_utils/extras/responses/__init__.py +5 -0
- bear_utils/extras/responses/function_response.py +451 -0
- bear_utils/extras/wrappers/__init__.py +1 -0
- bear_utils/extras/wrappers/add_methods.py +100 -0
- bear_utils/extras/wrappers/string_io.py +46 -0
- bear_utils/files/__init__.py +6 -0
- bear_utils/files/file_handlers/__init__.py +5 -0
- bear_utils/files/file_handlers/_base_file_handler.py +107 -0
- bear_utils/files/file_handlers/file_handler_factory.py +280 -0
- bear_utils/files/file_handlers/json_file_handler.py +71 -0
- bear_utils/files/file_handlers/log_file_handler.py +40 -0
- bear_utils/files/file_handlers/toml_file_handler.py +76 -0
- bear_utils/files/file_handlers/txt_file_handler.py +76 -0
- bear_utils/files/file_handlers/yaml_file_handler.py +64 -0
- bear_utils/files/ignore_parser.py +293 -0
- bear_utils/graphics/__init__.py +6 -0
- bear_utils/graphics/bear_gradient.py +145 -0
- bear_utils/graphics/font/__init__.py +13 -0
- bear_utils/graphics/font/_raw_block_letters.py +463 -0
- bear_utils/graphics/font/_theme.py +31 -0
- bear_utils/graphics/font/_utils.py +220 -0
- bear_utils/graphics/font/block_font.py +192 -0
- bear_utils/graphics/font/glitch_font.py +63 -0
- bear_utils/graphics/image_helpers.py +45 -0
- bear_utils/gui/__init__.py +8 -0
- bear_utils/gui/gui_tools/__init__.py +10 -0
- bear_utils/gui/gui_tools/_settings.py +36 -0
- bear_utils/gui/gui_tools/_types.py +12 -0
- bear_utils/gui/gui_tools/qt_app.py +150 -0
- bear_utils/gui/gui_tools/qt_color_picker.py +130 -0
- bear_utils/gui/gui_tools/qt_file_handler.py +130 -0
- bear_utils/gui/gui_tools/qt_input_dialog.py +303 -0
- bear_utils/logger_manager/__init__.py +109 -0
- bear_utils/logger_manager/_common.py +63 -0
- bear_utils/logger_manager/_console_junk.py +135 -0
- bear_utils/logger_manager/_log_level.py +50 -0
- bear_utils/logger_manager/_styles.py +95 -0
- bear_utils/logger_manager/logger_protocol.py +42 -0
- bear_utils/logger_manager/loggers/__init__.py +1 -0
- bear_utils/logger_manager/loggers/_console.py +223 -0
- bear_utils/logger_manager/loggers/_level_sin.py +61 -0
- bear_utils/logger_manager/loggers/_logger.py +19 -0
- bear_utils/logger_manager/loggers/base_logger.py +244 -0
- bear_utils/logger_manager/loggers/base_logger.pyi +51 -0
- bear_utils/logger_manager/loggers/basic_logger/__init__.py +5 -0
- bear_utils/logger_manager/loggers/basic_logger/logger.py +80 -0
- bear_utils/logger_manager/loggers/basic_logger/logger.pyi +19 -0
- bear_utils/logger_manager/loggers/buffer_logger.py +57 -0
- bear_utils/logger_manager/loggers/console_logger.py +278 -0
- bear_utils/logger_manager/loggers/console_logger.pyi +50 -0
- bear_utils/logger_manager/loggers/fastapi_logger.py +333 -0
- bear_utils/logger_manager/loggers/file_logger.py +151 -0
- bear_utils/logger_manager/loggers/simple_logger.py +98 -0
- bear_utils/logger_manager/loggers/sub_logger.py +105 -0
- bear_utils/logger_manager/loggers/sub_logger.pyi +23 -0
- bear_utils/monitoring/__init__.py +13 -0
- bear_utils/monitoring/_common.py +28 -0
- bear_utils/monitoring/host_monitor.py +346 -0
- bear_utils/time/__init__.py +59 -0
- bear_utils-0.0.1.dist-info/METADATA +305 -0
- bear_utils-0.0.1.dist-info/RECORD +107 -0
- bear_utils-0.0.1.dist-info/WHEEL +4 -0
@@ -0,0 +1,278 @@
|
|
1
|
+
"""ConsoleLogger: A comprehensive console logger that combines Python's logging framework with Rich console styling."""
|
2
|
+
|
3
|
+
# region Imports
|
4
|
+
from contextlib import suppress
|
5
|
+
from functools import cached_property
|
6
|
+
from logging import DEBUG, Formatter, Handler, Logger
|
7
|
+
from logging.handlers import QueueHandler, QueueListener, RotatingFileHandler
|
8
|
+
from queue import Queue
|
9
|
+
from typing import TYPE_CHECKING, override
|
10
|
+
|
11
|
+
from prompt_toolkit import PromptSession
|
12
|
+
from rich.text import Text
|
13
|
+
from rich.theme import Theme
|
14
|
+
|
15
|
+
from bear_utils.constants.date_related import DATE_TIME_FORMAT
|
16
|
+
from bear_utils.logger_manager._common import FIVE_MEGABYTES, VERBOSE_CONSOLE_FORMAT, VERBOSE_FORMAT, ExecValues
|
17
|
+
from bear_utils.logger_manager._console_junk import ConsoleBuffering, ConsoleFormatter, ConsoleHandler
|
18
|
+
|
19
|
+
from .base_logger import BaseLogger
|
20
|
+
|
21
|
+
if TYPE_CHECKING:
|
22
|
+
from rich.traceback import Traceback
|
23
|
+
|
24
|
+
# endregion Imports
|
25
|
+
|
26
|
+
|
27
|
+
class ConsoleLogger(Logger, BaseLogger):
|
28
|
+
"""A comprehensive console logger that combines Python's logging framework with Rich console styling.
|
29
|
+
|
30
|
+
This logger provides styled console output with configurable file logging, queue handling,
|
31
|
+
buffering, and interactive input capabilities. It dynamically creates logging methods
|
32
|
+
(info, error, debug, etc.) that forward to Rich's styled console printing.
|
33
|
+
|
34
|
+
Features:
|
35
|
+
- Rich styled console output with themes
|
36
|
+
- Optional file logging with rotation
|
37
|
+
- Queue-based async logging
|
38
|
+
- Message buffering capabilities
|
39
|
+
- Interactive prompt integration
|
40
|
+
- Exception tracebacks with local variables
|
41
|
+
|
42
|
+
Example:
|
43
|
+
logger = ConsoleLogger.get_instance(init=True, verbose=True, name="MyLogger", level=DEBUG)
|
44
|
+
logger.info("This is styled info")
|
45
|
+
logger.error("This is styled error")
|
46
|
+
logger.success("This is styled success")
|
47
|
+
"""
|
48
|
+
|
49
|
+
# region Setup
|
50
|
+
def __init__(
|
51
|
+
self,
|
52
|
+
theme: Theme | None = None,
|
53
|
+
name: str = "ConsoleLogger",
|
54
|
+
level: int = DEBUG,
|
55
|
+
disabled: bool = True,
|
56
|
+
console: bool = True,
|
57
|
+
file: bool = False,
|
58
|
+
queue_handler: bool = False,
|
59
|
+
buffering: bool = False,
|
60
|
+
*_,
|
61
|
+
**kwargs,
|
62
|
+
) -> None:
|
63
|
+
"""Initialize the ConsoleLogger with optional file, console, and buffering settings."""
|
64
|
+
Logger.__init__(self, name=name, level=level)
|
65
|
+
BaseLogger.__init__(
|
66
|
+
self,
|
67
|
+
output_handler=self._console_output,
|
68
|
+
theme=theme,
|
69
|
+
style_disabled=kwargs.get("style_disabled", False),
|
70
|
+
logger_mode=kwargs.get("logger_mode", True),
|
71
|
+
level=level,
|
72
|
+
)
|
73
|
+
self.name = name
|
74
|
+
self.level = level
|
75
|
+
self.setLevel(level)
|
76
|
+
self.session = None
|
77
|
+
self.disabled = disabled
|
78
|
+
self._handlers: list[Handler] = []
|
79
|
+
self.logger_mode: bool = kwargs.pop("logger_mode", True)
|
80
|
+
if self.logger_mode:
|
81
|
+
self.disabled = False
|
82
|
+
self._handle_enable_booleans(
|
83
|
+
file=file,
|
84
|
+
console=console,
|
85
|
+
buffering=buffering,
|
86
|
+
queue_handler=queue_handler,
|
87
|
+
**kwargs,
|
88
|
+
)
|
89
|
+
|
90
|
+
def _handle_enable_booleans(
|
91
|
+
self,
|
92
|
+
file: bool,
|
93
|
+
console: bool,
|
94
|
+
buffering: bool,
|
95
|
+
queue_handler: bool,
|
96
|
+
**kwargs,
|
97
|
+
) -> None:
|
98
|
+
"""Configure logging handlers based on initialization parameters."""
|
99
|
+
if console or buffering:
|
100
|
+
self.console_handler: ConsoleHandler = ConsoleHandler(self.print, self.output_buffer)
|
101
|
+
self.console_handler.setFormatter(ConsoleFormatter(fmt=VERBOSE_CONSOLE_FORMAT, datefmt=DATE_TIME_FORMAT))
|
102
|
+
self.console_handler.setLevel(self.level)
|
103
|
+
if console:
|
104
|
+
self._handlers.append(self.console_handler)
|
105
|
+
if buffering:
|
106
|
+
self.buffer_handler: ConsoleBuffering = ConsoleBuffering(console_handler=self.console_handler)
|
107
|
+
self.addHandler(self.buffer_handler)
|
108
|
+
if file:
|
109
|
+
self.file_handler: RotatingFileHandler = RotatingFileHandler(
|
110
|
+
filename=kwargs.get("file_path", "console.log"),
|
111
|
+
maxBytes=kwargs.get("max_bytes", FIVE_MEGABYTES),
|
112
|
+
backupCount=kwargs.get("backup_count", 5),
|
113
|
+
)
|
114
|
+
self.file_handler.setFormatter(Formatter(fmt=VERBOSE_FORMAT, datefmt=DATE_TIME_FORMAT))
|
115
|
+
self.file_handler.setLevel(self.level)
|
116
|
+
self._handlers.append(self.file_handler)
|
117
|
+
if queue_handler:
|
118
|
+
self.queue = Queue()
|
119
|
+
self.queue_handler = QueueHandler(self.queue)
|
120
|
+
self.addHandler(self.queue_handler)
|
121
|
+
self.listener = QueueListener(self.queue, *self._handlers)
|
122
|
+
self.listener.start()
|
123
|
+
else:
|
124
|
+
for handler in self._handlers:
|
125
|
+
self.addHandler(handler)
|
126
|
+
|
127
|
+
def stop_queue_listener(self) -> None:
|
128
|
+
"""Stop the queue listener if it exists and clean up resources."""
|
129
|
+
if hasattr(self, "listener"):
|
130
|
+
self.verbose("ConsoleLogger: QueueListener stopped and cleaned up.")
|
131
|
+
self.listener.stop()
|
132
|
+
del self.listener
|
133
|
+
del self.queue
|
134
|
+
del self.queue_handler
|
135
|
+
|
136
|
+
def trigger_buffer_flush(self) -> Text:
|
137
|
+
"""Flush buffered messages to console output."""
|
138
|
+
if hasattr(self, "buffer_handler"):
|
139
|
+
return self.buffer_handler.flush_to_output()
|
140
|
+
return Text("No buffering handler available.", style="bold red")
|
141
|
+
|
142
|
+
def set_base_level(self, level: int) -> None:
|
143
|
+
"""Set the base logging level for the console logger."""
|
144
|
+
super().set_base_level(level)
|
145
|
+
self.setLevel(level)
|
146
|
+
if hasattr(self, "console_handler"):
|
147
|
+
self.console_handler.setLevel(level)
|
148
|
+
if hasattr(self, "buffer_handler"):
|
149
|
+
self.buffer_handler.setLevel(level)
|
150
|
+
if hasattr(self, "queue_handler"):
|
151
|
+
self.queue_handler.setLevel(level)
|
152
|
+
|
153
|
+
def _console_output(self, msg: object, extra: dict, *args, **kwargs) -> None:
|
154
|
+
"""Console-specific output handler that integrates with logging module."""
|
155
|
+
if not self.logger_mode:
|
156
|
+
self.print(msg, *args, **kwargs)
|
157
|
+
else:
|
158
|
+
kwargs.pop("style", None)
|
159
|
+
self.log(
|
160
|
+
extra.get("log_level", DEBUG),
|
161
|
+
msg,
|
162
|
+
*args,
|
163
|
+
extra=extra,
|
164
|
+
**kwargs,
|
165
|
+
)
|
166
|
+
|
167
|
+
# endregion Setup
|
168
|
+
|
169
|
+
# region Utility Methods
|
170
|
+
|
171
|
+
async def input(self, msg: str, style: str = "info", **kwargs) -> str:
|
172
|
+
"""Display a styled prompt and return user input asynchronously."""
|
173
|
+
if not self.session:
|
174
|
+
self.session = PromptSession(**kwargs)
|
175
|
+
self.print(msg, style=style)
|
176
|
+
return await self.session.prompt_async()
|
177
|
+
|
178
|
+
def output_buffer(
|
179
|
+
self,
|
180
|
+
msg: object,
|
181
|
+
end: str = "\n",
|
182
|
+
exc_info: str | None = None,
|
183
|
+
exec_values: ExecValues | None = None,
|
184
|
+
*_,
|
185
|
+
**kwargs,
|
186
|
+
) -> str:
|
187
|
+
"""Capture console output to a string buffer without printing to terminal."""
|
188
|
+
if exc_info and exec_values:
|
189
|
+
exception: Traceback = self._get_exception(manual=True, exec_values=exec_values)
|
190
|
+
self.console.print(exception, end=end)
|
191
|
+
self.console.print(msg, end="", style=kwargs.get("style", "info"))
|
192
|
+
output = self.console_buffer.getvalue()
|
193
|
+
self._reset_buffer()
|
194
|
+
return output
|
195
|
+
|
196
|
+
# endregion Utility Methods
|
197
|
+
|
198
|
+
# region Enhanced Print Methods
|
199
|
+
|
200
|
+
def print(
|
201
|
+
self,
|
202
|
+
msg: object,
|
203
|
+
end: str = "\n",
|
204
|
+
exc_info: str | None = None,
|
205
|
+
extra: dict | None = None,
|
206
|
+
*args,
|
207
|
+
**kwargs,
|
208
|
+
) -> None | str:
|
209
|
+
"""Print styled messages with enhanced exception handling and JSON support.
|
210
|
+
|
211
|
+
Extends the base print method with proper exception tracebacks and
|
212
|
+
integrated JSON printing for structured data output.
|
213
|
+
"""
|
214
|
+
if exc_info is not None:
|
215
|
+
with suppress(ValueError):
|
216
|
+
self._print(self._get_exception(), end=end, width=100, show_locals=True, **kwargs)
|
217
|
+
|
218
|
+
self._print(msg, end, *args, **kwargs)
|
219
|
+
|
220
|
+
if extra:
|
221
|
+
self._print(msg=extra, end=end, json=True, indent=4)
|
222
|
+
|
223
|
+
@cached_property
|
224
|
+
def stack_level(self) -> int:
|
225
|
+
"""Cached property to retrieve the current stack level."""
|
226
|
+
return self.stack_tracker.record_end()
|
227
|
+
|
228
|
+
@override
|
229
|
+
def _log( # type: ignore[override]
|
230
|
+
self,
|
231
|
+
level: int,
|
232
|
+
msg: object,
|
233
|
+
args: tuple,
|
234
|
+
exc_info: str | None = None,
|
235
|
+
extra: dict | None = None,
|
236
|
+
stack_info: bool = False,
|
237
|
+
stacklevel: int | None = None,
|
238
|
+
) -> None:
|
239
|
+
"""Custom logging implementation with enhanced exception handling.
|
240
|
+
|
241
|
+
Overrides the standard logging._log method to provide better exception
|
242
|
+
value extraction for Rich traceback integration while respecting log levels.
|
243
|
+
"""
|
244
|
+
stacklevel = stacklevel or self.stack_level
|
245
|
+
try:
|
246
|
+
fn, lno, func, sinfo = self.findCaller(stack_info, stacklevel)
|
247
|
+
except ValueError:
|
248
|
+
fn, lno, func, sinfo = "(unknown file)", 0, "(unknown function)", None
|
249
|
+
final_extra = extra or {}
|
250
|
+
if exc_info is not None:
|
251
|
+
exec_values = self._extract_exception_values(exc_info)
|
252
|
+
if exec_values:
|
253
|
+
final_extra = {**final_extra, "exec_values": exec_values}
|
254
|
+
|
255
|
+
record = self.makeRecord(
|
256
|
+
name=self.name,
|
257
|
+
level=level,
|
258
|
+
fn=fn,
|
259
|
+
lno=lno,
|
260
|
+
msg=msg,
|
261
|
+
args=args,
|
262
|
+
exc_info=None,
|
263
|
+
func=func,
|
264
|
+
extra=final_extra,
|
265
|
+
sinfo=sinfo,
|
266
|
+
)
|
267
|
+
|
268
|
+
self.handle(record)
|
269
|
+
|
270
|
+
def exit(self) -> None:
|
271
|
+
"""Clean up resources including queue listeners and console buffers."""
|
272
|
+
if hasattr(self, "queue_handler"):
|
273
|
+
self.queue_handler.flush()
|
274
|
+
self.stop_queue_listener()
|
275
|
+
|
276
|
+
self.console_buffer.close()
|
277
|
+
|
278
|
+
# endregion Enhanced Print Methods
|
@@ -0,0 +1,50 @@
|
|
1
|
+
from logging import Logger
|
2
|
+
from logging.handlers import QueueHandler, RotatingFileHandler
|
3
|
+
from typing import Any
|
4
|
+
|
5
|
+
from rich.text import Text
|
6
|
+
from rich.theme import Theme
|
7
|
+
|
8
|
+
from bear_utils.logger_manager._console_junk import ConsoleBuffering, ConsoleHandler
|
9
|
+
|
10
|
+
from .base_logger import BaseLogger
|
11
|
+
from .sub_logger import SubConsoleLogger
|
12
|
+
|
13
|
+
class ConsoleLogger(Logger, BaseLogger):
|
14
|
+
name: str
|
15
|
+
level: int
|
16
|
+
file: bool
|
17
|
+
queue_handler: QueueHandler
|
18
|
+
buffer_handler: ConsoleBuffering
|
19
|
+
console_handler: ConsoleHandler
|
20
|
+
file_handler: RotatingFileHandler
|
21
|
+
sub_logger: dict[str, SubConsoleLogger[ConsoleLogger]]
|
22
|
+
|
23
|
+
def __init__(
|
24
|
+
self,
|
25
|
+
theme: Theme | None = None,
|
26
|
+
name: str = "ConsoleLogger",
|
27
|
+
level: int = ...,
|
28
|
+
disabled: bool = True,
|
29
|
+
queue_handler: bool = False,
|
30
|
+
buffering: bool = False,
|
31
|
+
file: bool = False,
|
32
|
+
console: bool = True,
|
33
|
+
style_disabled: bool = False,
|
34
|
+
logger_mode: bool = True,
|
35
|
+
*args,
|
36
|
+
**kwargs,
|
37
|
+
) -> None: ...
|
38
|
+
# fmt: off
|
39
|
+
def debug(self, msg: object, *args, **kwargs: Any) -> None: ...
|
40
|
+
def info(self, msg: object, *args, **kwargs: Any) -> None: ...
|
41
|
+
def warning(self, msg: object, *args, **kwargs: Any) -> None: ...
|
42
|
+
def error(self, msg: object, *args, **kwargs: Any) -> None: ...
|
43
|
+
def success(self, msg: object, *args, **kwargs: Any) -> None: ...
|
44
|
+
def failure(self, msg: object, *args, **kwargs: Any) -> None: ...
|
45
|
+
def verbose(self, msg: object, *args, **kwargs: Any) -> None: ...
|
46
|
+
def set_base_level(self, level: int) -> None: ...
|
47
|
+
def input(self, msg: str, style: str = "info") -> str: ...
|
48
|
+
def trigger_buffer_flush(self) -> str | Text: ...
|
49
|
+
def print(self, msg: object, end: str="\n", exc_info:str|None=None, extra: dict | None = None, *args, **kwargs) -> None | str: ...
|
50
|
+
def exit(self) -> None: ...
|
@@ -0,0 +1,333 @@
|
|
1
|
+
"""FastAPI-based local logging server and HTTP logger."""
|
2
|
+
|
3
|
+
import asyncio
|
4
|
+
from collections import deque
|
5
|
+
from pathlib import Path
|
6
|
+
import threading
|
7
|
+
from typing import TYPE_CHECKING, Any, Self, TextIO
|
8
|
+
|
9
|
+
from fastapi import FastAPI
|
10
|
+
from fastapi.responses import JSONResponse
|
11
|
+
from httpx import AsyncClient
|
12
|
+
from pydantic import BaseModel, Field, field_serializer
|
13
|
+
from singleton_base import SingletonBase
|
14
|
+
import uvicorn
|
15
|
+
|
16
|
+
from bear_utils.constants import DEVNULL, ExitCode, HTTPStatusCode
|
17
|
+
from bear_utils.logger_manager._log_level import LogLevel
|
18
|
+
from bear_utils.time import EpochTimestamp
|
19
|
+
|
20
|
+
if TYPE_CHECKING:
|
21
|
+
from httpx import Response
|
22
|
+
|
23
|
+
|
24
|
+
VERBOSE: LogLevel = LogLevel.from_name("VERBOSE")
|
25
|
+
DEBUG: LogLevel = LogLevel.from_name("DEBUG")
|
26
|
+
INFO: LogLevel = LogLevel.from_name("INFO")
|
27
|
+
WARNING: LogLevel = LogLevel.from_name("WARNING")
|
28
|
+
ERROR: LogLevel = LogLevel.from_name("ERROR")
|
29
|
+
FAILURE: LogLevel = LogLevel.from_name("FAILURE")
|
30
|
+
SUCCESS: LogLevel = LogLevel.from_name("SUCCESS")
|
31
|
+
|
32
|
+
|
33
|
+
class LogRequest(BaseModel):
|
34
|
+
"""Request model for logging messages."""
|
35
|
+
|
36
|
+
level: LogLevel | int | str = Field(default=DEBUG, description="Log level of the message")
|
37
|
+
message: str = Field(default="", description="Log message content")
|
38
|
+
args: tuple = Field(default=(), description="Arguments for the log message")
|
39
|
+
kwargs: dict[str, str] = Field(default_factory=dict, description="Keyword arguments for the log message")
|
40
|
+
|
41
|
+
model_config = {
|
42
|
+
"arbitrary_types_allowed": True,
|
43
|
+
}
|
44
|
+
|
45
|
+
@field_serializer("level")
|
46
|
+
def serialize_level(self, value: LogLevel | int | str) -> int:
|
47
|
+
"""Serialize the log level to an integer."""
|
48
|
+
return LogLevel.get(value, default=DEBUG).value
|
49
|
+
|
50
|
+
|
51
|
+
class LoggingServer[T: TextIO](SingletonBase):
|
52
|
+
"""A local server that writes logs to a file."""
|
53
|
+
|
54
|
+
def __init__(
|
55
|
+
self,
|
56
|
+
host: str = "localhost",
|
57
|
+
port: int = 8080,
|
58
|
+
log_file: str | Path = "server.log",
|
59
|
+
level: LogLevel | int | str = DEBUG,
|
60
|
+
file: T = DEVNULL, # Default to DEVNULL to discard console output
|
61
|
+
maxlen: int = 100,
|
62
|
+
) -> None:
|
63
|
+
"""Initialize the logging server."""
|
64
|
+
self.host: str = host
|
65
|
+
self.port: int = port
|
66
|
+
self.log_file: Path = Path(log_file)
|
67
|
+
self.log_file.parent.mkdir(parents=True, exist_ok=True)
|
68
|
+
self.level: LogLevel = LogLevel.get(level, default=DEBUG)
|
69
|
+
self.app = FastAPI()
|
70
|
+
self.server_thread = None
|
71
|
+
self._running = False
|
72
|
+
self.logs: deque[LogRequest] = deque(maxlen=maxlen)
|
73
|
+
self.file: T = file
|
74
|
+
self._setup_routes()
|
75
|
+
|
76
|
+
@property
|
77
|
+
def running(self) -> bool:
|
78
|
+
"""Check if the server is running."""
|
79
|
+
return self._running or (self.server_thread is not None and self.server_thread.is_alive())
|
80
|
+
|
81
|
+
def __len__(self) -> int:
|
82
|
+
"""Get the number of logged messages."""
|
83
|
+
return len(self.logs)
|
84
|
+
|
85
|
+
def get_logs(self) -> list[LogRequest]:
|
86
|
+
"""Get the list of logged messages."""
|
87
|
+
return list(self.logs)
|
88
|
+
|
89
|
+
def print(self, msg: object, end: str = "\n") -> None:
|
90
|
+
"""Print the message to the specified file with an optional end character."""
|
91
|
+
print(msg, end=end, file=self.file)
|
92
|
+
|
93
|
+
def response(
|
94
|
+
self,
|
95
|
+
status: str,
|
96
|
+
message: str = "",
|
97
|
+
status_code: HTTPStatusCode = HTTPStatusCode.SERVER_OK,
|
98
|
+
) -> JSONResponse:
|
99
|
+
"""Create a JSON response with the given content and status code."""
|
100
|
+
return JSONResponse(content={"status": status, "message": message}, status_code=status_code.value)
|
101
|
+
|
102
|
+
def _setup_routes(self) -> None:
|
103
|
+
"""Set up the FastAPI routes for logging and health check."""
|
104
|
+
|
105
|
+
@self.app.post("/log")
|
106
|
+
async def log_message(request: LogRequest | Any) -> JSONResponse:
|
107
|
+
"""Endpoint to log a message."""
|
108
|
+
request = LogRequest(
|
109
|
+
level=request["level"] if isinstance(request, dict) else request.level,
|
110
|
+
message=request["message"] if isinstance(request, dict) else request.message,
|
111
|
+
args=request["args"] if isinstance(request, dict) else request.args,
|
112
|
+
kwargs=request["kwargs"] if isinstance(request, dict) else request.kwargs,
|
113
|
+
)
|
114
|
+
level: LogLevel = LogLevel.get(request.level, default=DEBUG)
|
115
|
+
if level.value < self.level.value:
|
116
|
+
return self.response(status="ignored", message="Log level is lower than server's minimum level")
|
117
|
+
message = request.message
|
118
|
+
args = request.args
|
119
|
+
kwargs: dict[str, str] | Any = request.kwargs
|
120
|
+
success: ExitCode = self.write_log(level, message, *args, **kwargs)
|
121
|
+
self.logs.append(request)
|
122
|
+
if success != ExitCode.SUCCESS:
|
123
|
+
return self.response(
|
124
|
+
status="error", message="Failed to write log", status_code=HTTPStatusCode.SERVER_ERROR
|
125
|
+
)
|
126
|
+
return self.response(status="success", status_code=HTTPStatusCode.SERVER_OK)
|
127
|
+
|
128
|
+
@self.app.get("/health")
|
129
|
+
async def health_check() -> JSONResponse:
|
130
|
+
return JSONResponse(
|
131
|
+
content={"status": "healthy"},
|
132
|
+
status_code=HTTPStatusCode.SERVER_OK,
|
133
|
+
)
|
134
|
+
|
135
|
+
def write_log(
|
136
|
+
self,
|
137
|
+
level: LogLevel,
|
138
|
+
message: str,
|
139
|
+
end: str = "\n",
|
140
|
+
*args,
|
141
|
+
**kwargs,
|
142
|
+
) -> ExitCode:
|
143
|
+
"""Write a log entry to the file - same logic as original logger."""
|
144
|
+
timestamp: str = EpochTimestamp.now().to_string()
|
145
|
+
log_entry: str = f"[{timestamp}] {level}: {message}"
|
146
|
+
buffer = []
|
147
|
+
try:
|
148
|
+
buffer.append(log_entry)
|
149
|
+
if args:
|
150
|
+
buffer.append(f"{end}".join(str(arg) for arg in args))
|
151
|
+
if kwargs:
|
152
|
+
for key, value in kwargs.items():
|
153
|
+
buffer.append(f"{key}={value}{end}")
|
154
|
+
if kwargs.pop("console", False):
|
155
|
+
self.print(f"{end}".join(buffer))
|
156
|
+
with open(self.log_file, "a", encoding="utf-8") as f:
|
157
|
+
for line in buffer:
|
158
|
+
f.write(f"{line}{end}")
|
159
|
+
return ExitCode.SUCCESS
|
160
|
+
except Exception:
|
161
|
+
self.print(f"[{timestamp}] {level}: {message}")
|
162
|
+
return ExitCode.FAILURE
|
163
|
+
|
164
|
+
async def start(self) -> None:
|
165
|
+
"""Start the logging server in a separate thread."""
|
166
|
+
if self._running:
|
167
|
+
return
|
168
|
+
|
169
|
+
def _run_server() -> None:
|
170
|
+
"""Run the FastAPI server in a new event loop."""
|
171
|
+
uvicorn.run(self.app, host=self.host, port=self.port, log_level="error")
|
172
|
+
|
173
|
+
self.server_thread = threading.Thread(target=_run_server)
|
174
|
+
self.server_thread.daemon = True
|
175
|
+
self.server_thread.start()
|
176
|
+
self._running = True
|
177
|
+
self.write_log(DEBUG, f"Logging server started on {self.host}:{self.port}")
|
178
|
+
|
179
|
+
async def stop(self) -> None:
|
180
|
+
"""Stop the logging server."""
|
181
|
+
if self._running:
|
182
|
+
self._running = False
|
183
|
+
if self.server_thread is not None:
|
184
|
+
self.server_thread.join(timeout=1)
|
185
|
+
self.server_thread = None
|
186
|
+
self.write_log(DEBUG, "Logging server stopped")
|
187
|
+
|
188
|
+
async def __aenter__(self) -> Self:
|
189
|
+
"""Start the logging server."""
|
190
|
+
if not self.running:
|
191
|
+
await self.start()
|
192
|
+
else:
|
193
|
+
self.write_log(DEBUG, "Logging server is already running")
|
194
|
+
return self
|
195
|
+
|
196
|
+
async def __aexit__(self, exc_type: object, exc_val: object, exc_tb: object) -> None:
|
197
|
+
"""Stop the logging server."""
|
198
|
+
await self.stop()
|
199
|
+
|
200
|
+
|
201
|
+
class LoggingClient[T: TextIO]:
|
202
|
+
"""Logger that calls HTTP endpoints but behaves like SimpleLogger."""
|
203
|
+
|
204
|
+
def __init__(
|
205
|
+
self,
|
206
|
+
server_url: str | None = None,
|
207
|
+
host: str = "http://localhost",
|
208
|
+
port: int = 8080,
|
209
|
+
level: LogLevel | int | str = DEBUG,
|
210
|
+
file: T = DEVNULL, # Default to DEVNULL to discard console output
|
211
|
+
) -> None:
|
212
|
+
"""Initialize the ServerLogger."""
|
213
|
+
self.host: str = host
|
214
|
+
self.port: int = port
|
215
|
+
self.server_url: str = server_url or f"{self.host}:{self.port}"
|
216
|
+
self.level: LogLevel = LogLevel.get(level, default=DEBUG)
|
217
|
+
self.client: AsyncClient = AsyncClient(timeout=5.0)
|
218
|
+
self.file: T = file
|
219
|
+
|
220
|
+
async def post(self, url: str, json: dict) -> "Response":
|
221
|
+
"""Send a POST request to the server."""
|
222
|
+
return await self.client.post(url=url, json=json)
|
223
|
+
|
224
|
+
async def _log(self, request: LogRequest) -> None:
|
225
|
+
"""Same interface as SimpleLogger._log but calls HTTP endpoint."""
|
226
|
+
try:
|
227
|
+
response: Response = await self.post(url=f"{self.server_url}/log", json=request.model_dump())
|
228
|
+
if response.status_code != HTTPStatusCode.SERVER_OK:
|
229
|
+
await self._fallback_log(str(request.level), request.message, *request.args, **request.kwargs)
|
230
|
+
except Exception:
|
231
|
+
await self._fallback_log(str(request.level), request.message, *request.args, **request.kwargs)
|
232
|
+
|
233
|
+
async def log(self, level: LogLevel, msg: object, *args: Any, **kwargs: Any) -> None:
|
234
|
+
"""Log a message at the specified level."""
|
235
|
+
if level.value >= self.level.value:
|
236
|
+
request = LogRequest(level=level.value, message=str(msg), args=args, kwargs=kwargs)
|
237
|
+
await self._log(request)
|
238
|
+
|
239
|
+
async def _fallback_log(self, lvl: str, msg: object, *args: Any, **kwargs: Any) -> None:
|
240
|
+
"""Fallback - same as original SimpleLogger._log."""
|
241
|
+
timestamp: str = EpochTimestamp.now().to_string()
|
242
|
+
print(f"Fallback Logging: [{timestamp}] {lvl}: {msg}", file=self.file)
|
243
|
+
if args:
|
244
|
+
print(" ".join(str(arg) for arg in args), file=self.file)
|
245
|
+
if kwargs:
|
246
|
+
for key, value in kwargs.items():
|
247
|
+
print(f"{key}={value}", file=self.file)
|
248
|
+
|
249
|
+
async def verbose(self, msg: object, *args, **kwargs) -> None:
|
250
|
+
"""Log a verbose message."""
|
251
|
+
await self.log(VERBOSE, msg, *args, **kwargs)
|
252
|
+
|
253
|
+
async def debug(self, msg: object, *args, **kwargs) -> None:
|
254
|
+
"""Log a debug message."""
|
255
|
+
await self.log(DEBUG, msg, *args, **kwargs)
|
256
|
+
|
257
|
+
async def info(self, msg: object, *args, **kwargs) -> None:
|
258
|
+
"""Log an info message."""
|
259
|
+
await self.log(INFO, msg, *args, **kwargs)
|
260
|
+
|
261
|
+
async def warning(self, msg: object, *args, **kwargs) -> None:
|
262
|
+
"""Log a warning message."""
|
263
|
+
await self.log(WARNING, msg, *args, **kwargs)
|
264
|
+
|
265
|
+
async def error(self, msg: object, *args, **kwargs) -> None:
|
266
|
+
"""Log an error message."""
|
267
|
+
await self.log(ERROR, msg, *args, **kwargs)
|
268
|
+
|
269
|
+
async def failure(self, msg: object, *args, **kwargs) -> None:
|
270
|
+
"""Log a failure message."""
|
271
|
+
await self.log(FAILURE, msg, *args, **kwargs)
|
272
|
+
|
273
|
+
async def success(self, msg: object, *args, **kwargs) -> None:
|
274
|
+
"""Log a success message."""
|
275
|
+
await self.log(SUCCESS, msg, *args, **kwargs)
|
276
|
+
|
277
|
+
async def close(self) -> None:
|
278
|
+
"""Close the HTTP client."""
|
279
|
+
await self.client.aclose()
|
280
|
+
|
281
|
+
async def __aenter__(self) -> Self:
|
282
|
+
"""Enter the asynchronous context manager."""
|
283
|
+
return self
|
284
|
+
|
285
|
+
async def __aexit__(self, exc_type: object, exc_value: object, traceback: object) -> None:
|
286
|
+
"""Exit the asynchronous context manager."""
|
287
|
+
await self.close()
|
288
|
+
|
289
|
+
|
290
|
+
async def run_server(
|
291
|
+
host: str = "localhost",
|
292
|
+
port: int = 8080,
|
293
|
+
log_file: str = "server.log",
|
294
|
+
level: LogLevel | int | str = DEBUG,
|
295
|
+
file: TextIO = DEVNULL,
|
296
|
+
) -> ExitCode | int:
|
297
|
+
"""Run the local logging server."""
|
298
|
+
server: LoggingServer[TextIO] = LoggingServer(
|
299
|
+
host=host,
|
300
|
+
port=port,
|
301
|
+
log_file=log_file,
|
302
|
+
level=level,
|
303
|
+
file=file,
|
304
|
+
)
|
305
|
+
try:
|
306
|
+
while True:
|
307
|
+
await server.start()
|
308
|
+
except KeyboardInterrupt:
|
309
|
+
print("Stopping server...", file=server.file)
|
310
|
+
except Exception as e:
|
311
|
+
print(f"An error occurred: {e}", file=server.file)
|
312
|
+
return ExitCode.FAILURE
|
313
|
+
finally:
|
314
|
+
await server.stop()
|
315
|
+
return ExitCode.SUCCESS
|
316
|
+
|
317
|
+
|
318
|
+
def sync_server(
|
319
|
+
host: str = "localhost",
|
320
|
+
port: int = 8080,
|
321
|
+
log_file: str = "server.log",
|
322
|
+
level: LogLevel | int | str = DEBUG,
|
323
|
+
file: TextIO = DEVNULL,
|
324
|
+
) -> ExitCode | int:
|
325
|
+
"""Run the local logging server synchronously."""
|
326
|
+
loop = asyncio.get_event_loop()
|
327
|
+
asyncio.set_event_loop(loop)
|
328
|
+
return loop.run_until_complete(run_server(host, port, log_file, level, file))
|
329
|
+
|
330
|
+
|
331
|
+
# if __name__ == "__main__":
|
332
|
+
# exit_code = sync_server()
|
333
|
+
# sys.exit(exit_code)
|