linksocks 1.7.1__cp311-cp311-macosx_14_0_universal2.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.
Potentially problematic release.
This version of linksocks might be problematic. Click here for more details.
- linksocks/__init__.py +9 -0
- linksocks/_base.py +230 -0
- linksocks/_cli.py +291 -0
- linksocks/_client.py +291 -0
- linksocks/_logging_config.py +182 -0
- linksocks/_server.py +355 -0
- linksocks/_utils.py +144 -0
- linksocks-1.7.1.dist-info/METADATA +493 -0
- linksocks-1.7.1.dist-info/RECORD +21 -0
- linksocks-1.7.1.dist-info/WHEEL +5 -0
- linksocks-1.7.1.dist-info/entry_points.txt +2 -0
- linksocks-1.7.1.dist-info/top_level.txt +2 -0
- linksockslib/__init__.py +0 -0
- linksockslib/_linksockslib.cpython-311-darwin.h +1101 -0
- linksockslib/_linksockslib.cpython-311-darwin.so +0 -0
- linksockslib/build.py +761 -0
- linksockslib/go.py +4023 -0
- linksockslib/linksocks.py +5756 -0
- linksockslib/linksockslib.c +14300 -0
- linksockslib/linksockslib.go +8238 -0
- linksockslib/linksockslib_go.h +1101 -0
linksocks/_client.py
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WebSocket SOCKS5 proxy client implementation.
|
|
3
|
+
|
|
4
|
+
This module provides the Client class for connecting to a SOCKS5 proxy server
|
|
5
|
+
over WebSocket and establishing proxy functionality.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import logging
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
# Underlying Go bindings module (generated)
|
|
15
|
+
from linksockslib import linksocks # type: ignore
|
|
16
|
+
|
|
17
|
+
from ._base import (
|
|
18
|
+
_SnakePassthrough,
|
|
19
|
+
_to_duration,
|
|
20
|
+
_logger,
|
|
21
|
+
BufferZerologLogger,
|
|
22
|
+
DurationLike,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Client(_SnakePassthrough):
|
|
27
|
+
"""WebSocket SOCKS5 proxy client.
|
|
28
|
+
|
|
29
|
+
The Client class connects to a WebSocket server and establishes SOCKS5 proxy
|
|
30
|
+
functionality. It supports both forward and reverse proxy modes.
|
|
31
|
+
|
|
32
|
+
In forward mode, the client runs a local SOCKS5 server and forwards connections
|
|
33
|
+
through WebSocket to the server, which then connects to targets.
|
|
34
|
+
|
|
35
|
+
In reverse mode, the client connects to targets directly and forwards data
|
|
36
|
+
back through WebSocket to a SOCKS5 server running on the server side.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
token: str,
|
|
42
|
+
*,
|
|
43
|
+
logger: Optional[logging.Logger] = None,
|
|
44
|
+
ws_url: Optional[str] = None,
|
|
45
|
+
reverse: Optional[bool] = None,
|
|
46
|
+
socks_host: Optional[str] = None,
|
|
47
|
+
socks_port: Optional[int] = None,
|
|
48
|
+
socks_username: Optional[str] = None,
|
|
49
|
+
socks_password: Optional[str] = None,
|
|
50
|
+
socks_wait_server: Optional[bool] = None,
|
|
51
|
+
reconnect: Optional[bool] = None,
|
|
52
|
+
reconnect_delay: Optional[DurationLike] = None,
|
|
53
|
+
buffer_size: Optional[int] = None,
|
|
54
|
+
channel_timeout: Optional[DurationLike] = None,
|
|
55
|
+
connect_timeout: Optional[DurationLike] = None,
|
|
56
|
+
threads: Optional[int] = None,
|
|
57
|
+
fast_open: Optional[bool] = None,
|
|
58
|
+
upstream_proxy: Optional[str] = None,
|
|
59
|
+
upstream_username: Optional[str] = None,
|
|
60
|
+
upstream_password: Optional[str] = None,
|
|
61
|
+
no_env_proxy: Optional[bool] = None,
|
|
62
|
+
) -> None:
|
|
63
|
+
"""Initialize the WebSocket SOCKS5 proxy client.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
token: Authentication token for WebSocket connection
|
|
67
|
+
logger: Python logger instance for this client
|
|
68
|
+
ws_url: WebSocket server URL to connect to
|
|
69
|
+
reverse: Whether to use reverse proxy mode
|
|
70
|
+
socks_host: SOCKS5 server listen address (for forward mode)
|
|
71
|
+
socks_port: SOCKS5 server listen port (for forward mode)
|
|
72
|
+
socks_username: SOCKS5 authentication username
|
|
73
|
+
socks_password: SOCKS5 authentication password
|
|
74
|
+
socks_wait_server: Whether to wait for server connection before starting SOCKS5
|
|
75
|
+
reconnect: Whether to automatically reconnect on disconnection
|
|
76
|
+
reconnect_delay: Delay between reconnection attempts
|
|
77
|
+
buffer_size: Buffer size for data transfer
|
|
78
|
+
channel_timeout: Timeout for WebSocket channels
|
|
79
|
+
connect_timeout: Timeout for outbound connections
|
|
80
|
+
threads: Number of threads for concurrent processing
|
|
81
|
+
fast_open: Assume connection success and allow data transfer immediately
|
|
82
|
+
upstream_proxy: Upstream proxy address for chaining
|
|
83
|
+
upstream_username: Username for upstream proxy authentication
|
|
84
|
+
upstream_password: Password for upstream proxy authentication
|
|
85
|
+
no_env_proxy: Whether to ignore proxy environment variables
|
|
86
|
+
"""
|
|
87
|
+
opt = linksocks.DefaultClientOption()
|
|
88
|
+
if logger is None:
|
|
89
|
+
logger = _logger
|
|
90
|
+
# Use buffer-based logger system
|
|
91
|
+
self._managed_logger = BufferZerologLogger(logger, f"client_{id(self)}")
|
|
92
|
+
opt.WithLogger(self._managed_logger.go_logger)
|
|
93
|
+
if ws_url is not None:
|
|
94
|
+
opt.WithWSURL(ws_url)
|
|
95
|
+
if reverse is not None:
|
|
96
|
+
opt.WithReverse(bool(reverse))
|
|
97
|
+
if socks_host is not None:
|
|
98
|
+
opt.WithSocksHost(socks_host)
|
|
99
|
+
if socks_port is not None:
|
|
100
|
+
opt.WithSocksPort(int(socks_port))
|
|
101
|
+
if socks_username is not None:
|
|
102
|
+
opt.WithSocksUsername(socks_username)
|
|
103
|
+
if socks_password is not None:
|
|
104
|
+
opt.WithSocksPassword(socks_password)
|
|
105
|
+
if socks_wait_server is not None:
|
|
106
|
+
opt.WithSocksWaitServer(bool(socks_wait_server))
|
|
107
|
+
if reconnect is not None:
|
|
108
|
+
opt.WithReconnect(bool(reconnect))
|
|
109
|
+
if reconnect_delay is not None:
|
|
110
|
+
opt.WithReconnectDelay(_to_duration(reconnect_delay))
|
|
111
|
+
if buffer_size is not None:
|
|
112
|
+
opt.WithBufferSize(int(buffer_size))
|
|
113
|
+
if channel_timeout is not None:
|
|
114
|
+
opt.WithChannelTimeout(_to_duration(channel_timeout))
|
|
115
|
+
if connect_timeout is not None:
|
|
116
|
+
opt.WithConnectTimeout(_to_duration(connect_timeout))
|
|
117
|
+
if threads is not None:
|
|
118
|
+
opt.WithThreads(int(threads))
|
|
119
|
+
if fast_open is not None:
|
|
120
|
+
opt.WithFastOpen(bool(fast_open))
|
|
121
|
+
if upstream_proxy is not None:
|
|
122
|
+
opt.WithUpstreamProxy(upstream_proxy)
|
|
123
|
+
if upstream_username or upstream_password:
|
|
124
|
+
opt.WithUpstreamAuth(upstream_username or "", upstream_password or "")
|
|
125
|
+
if no_env_proxy is not None:
|
|
126
|
+
opt.WithNoEnvProxy(bool(no_env_proxy))
|
|
127
|
+
|
|
128
|
+
self._raw = linksocks.NewLinkSocksClient(token, opt)
|
|
129
|
+
self._ctx = None
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def log(self) -> logging.Logger:
|
|
133
|
+
"""Access the Python logger for this client instance."""
|
|
134
|
+
return self._managed_logger.py_logger
|
|
135
|
+
|
|
136
|
+
def wait_ready(self, timeout: Optional[DurationLike] = None) -> None:
|
|
137
|
+
"""Wait for the client to be ready.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
timeout: Maximum time to wait, no timeout if None
|
|
141
|
+
"""
|
|
142
|
+
if not self._ctx:
|
|
143
|
+
self._ctx = linksocks.NewContextWithCancel()
|
|
144
|
+
timeout = _to_duration(timeout) if timeout is not None else 0
|
|
145
|
+
return self._raw.WaitReady(ctx=self._ctx.Context(), timeout=timeout)
|
|
146
|
+
|
|
147
|
+
async def async_wait_ready(self, timeout: Optional[DurationLike] = None) -> None:
|
|
148
|
+
"""Wait for the client to be ready asynchronously.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
timeout: Maximum time to wait, no timeout if None
|
|
152
|
+
"""
|
|
153
|
+
if not self._ctx:
|
|
154
|
+
self._ctx = linksocks.NewContextWithCancel()
|
|
155
|
+
timeout = _to_duration(timeout) if timeout is not None else 0
|
|
156
|
+
try:
|
|
157
|
+
return await asyncio.to_thread(self._raw.WaitReady, ctx=self._ctx.Context(), timeout=timeout)
|
|
158
|
+
except asyncio.CancelledError:
|
|
159
|
+
# Ensure the underlying Go client stops retrying/logging when the
|
|
160
|
+
# awaiting task is cancelled (e.g. Ctrl+C). We cancel the context
|
|
161
|
+
# we passed into Go and close the client, then re-raise.
|
|
162
|
+
try:
|
|
163
|
+
try:
|
|
164
|
+
self._ctx.Cancel()
|
|
165
|
+
except Exception:
|
|
166
|
+
pass
|
|
167
|
+
# Shield cleanup from further cancellation so it can complete
|
|
168
|
+
await asyncio.shield(asyncio.to_thread(self._raw.Close))
|
|
169
|
+
# Best-effort logger cleanup
|
|
170
|
+
if hasattr(self, '_managed_logger') and self._managed_logger:
|
|
171
|
+
try:
|
|
172
|
+
self._managed_logger.cleanup()
|
|
173
|
+
except Exception:
|
|
174
|
+
pass
|
|
175
|
+
finally:
|
|
176
|
+
raise
|
|
177
|
+
|
|
178
|
+
def add_connector(self, connector_token: Optional[str]) -> str:
|
|
179
|
+
"""Add a connector token for reverse proxy.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
connector_token: Connector token string, auto-generated if not provided
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
The connector token string (generated or provided)
|
|
186
|
+
"""
|
|
187
|
+
return self._raw.AddConnector(connector_token or "")
|
|
188
|
+
|
|
189
|
+
async def async_add_connector(self, connector_token: Optional[str]) -> str:
|
|
190
|
+
"""Add a connector token for reverse proxy asynchronously.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
connector_token: Connector token string, auto-generated if not provided
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
The connector token string (generated or provided)
|
|
197
|
+
"""
|
|
198
|
+
return await asyncio.to_thread(self._raw.AddConnector, connector_token or "")
|
|
199
|
+
|
|
200
|
+
@property
|
|
201
|
+
def is_connected(self) -> bool:
|
|
202
|
+
"""Check if the client is connected to the server.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
True if connected, False otherwise
|
|
206
|
+
"""
|
|
207
|
+
try:
|
|
208
|
+
return bool(self._raw.IsConnected)
|
|
209
|
+
except Exception:
|
|
210
|
+
# If not exposed as field, fall back to False
|
|
211
|
+
return False
|
|
212
|
+
|
|
213
|
+
@property
|
|
214
|
+
def socks_port(self) -> Optional[int]:
|
|
215
|
+
"""Get the SOCKS5 server port (for forward mode).
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
The port number if available, None otherwise
|
|
219
|
+
"""
|
|
220
|
+
try:
|
|
221
|
+
# Exposed field in bindings
|
|
222
|
+
port = getattr(self._raw, "SocksPort", None)
|
|
223
|
+
return int(port) if port is not None else None
|
|
224
|
+
except Exception:
|
|
225
|
+
return None
|
|
226
|
+
|
|
227
|
+
def close(self) -> None:
|
|
228
|
+
"""Close the client and clean up resources."""
|
|
229
|
+
# Close client
|
|
230
|
+
if hasattr(self, '_raw') and self._raw:
|
|
231
|
+
self._raw.Close()
|
|
232
|
+
# Clean up managed logger
|
|
233
|
+
if hasattr(self, '_managed_logger') and self._managed_logger:
|
|
234
|
+
try:
|
|
235
|
+
self._managed_logger.cleanup()
|
|
236
|
+
except:
|
|
237
|
+
# Ignore cleanup errors
|
|
238
|
+
pass
|
|
239
|
+
# Close context
|
|
240
|
+
if hasattr(self, '_ctx') and self._ctx:
|
|
241
|
+
try:
|
|
242
|
+
self._ctx.Cancel()
|
|
243
|
+
except Exception:
|
|
244
|
+
# Ignore errors during context close
|
|
245
|
+
pass
|
|
246
|
+
|
|
247
|
+
async def async_close(self) -> None:
|
|
248
|
+
"""Close the client and clean up resources asynchronously."""
|
|
249
|
+
# Close client
|
|
250
|
+
if hasattr(self, '_raw') and self._raw:
|
|
251
|
+
await asyncio.to_thread(self._raw.Close)
|
|
252
|
+
# Clean up managed logger
|
|
253
|
+
if hasattr(self, '_managed_logger') and self._managed_logger:
|
|
254
|
+
try:
|
|
255
|
+
self._managed_logger.cleanup()
|
|
256
|
+
except:
|
|
257
|
+
# Ignore cleanup errors
|
|
258
|
+
pass
|
|
259
|
+
# Close context
|
|
260
|
+
if hasattr(self, '_ctx') and self._ctx:
|
|
261
|
+
try:
|
|
262
|
+
self._ctx.Cancel()
|
|
263
|
+
except Exception:
|
|
264
|
+
# Ignore errors during context close
|
|
265
|
+
pass
|
|
266
|
+
|
|
267
|
+
# Context manager support
|
|
268
|
+
def __enter__(self) -> "Client":
|
|
269
|
+
"""Context manager entry."""
|
|
270
|
+
return self
|
|
271
|
+
|
|
272
|
+
def __exit__(self, exc_type, exc, tb) -> None:
|
|
273
|
+
"""Context manager exit."""
|
|
274
|
+
self.close()
|
|
275
|
+
|
|
276
|
+
async def __aenter__(self) -> "Client":
|
|
277
|
+
"""Async context manager entry."""
|
|
278
|
+
await self.async_wait_ready()
|
|
279
|
+
return self
|
|
280
|
+
|
|
281
|
+
async def __aexit__(self, exc_type, exc, tb) -> None:
|
|
282
|
+
"""Async context manager exit."""
|
|
283
|
+
await self.async_close()
|
|
284
|
+
|
|
285
|
+
def __del__(self):
|
|
286
|
+
"""Destructor - clean up resources."""
|
|
287
|
+
try:
|
|
288
|
+
self.close()
|
|
289
|
+
except Exception:
|
|
290
|
+
# Ignore errors during cleanup
|
|
291
|
+
pass
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Logging configuration module for linksocks CLI.
|
|
3
|
+
|
|
4
|
+
This module provides custom logging handlers and configuration utilities
|
|
5
|
+
for the linksocks command-line interface, including Rich-based formatting
|
|
6
|
+
and loguru integration.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import inspect
|
|
10
|
+
import logging
|
|
11
|
+
import asyncio
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from loguru import logger
|
|
15
|
+
from rich.text import Text
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
from rich.logging import RichHandler
|
|
18
|
+
|
|
19
|
+
# Global console instance for consistent output
|
|
20
|
+
console = Console(stderr=True)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CarriageReturnRichHandler(RichHandler):
|
|
24
|
+
"""
|
|
25
|
+
A custom RichHandler that outputs a carriage return (\\r) before emitting log messages.
|
|
26
|
+
|
|
27
|
+
This handler prevents Ctrl+C interruptions from disrupting the output by ensuring
|
|
28
|
+
the cursor is positioned at the beginning of the line before writing log messages.
|
|
29
|
+
This is particularly useful in CLI applications where clean output formatting
|
|
30
|
+
is important even during interruptions.
|
|
31
|
+
|
|
32
|
+
Attributes:
|
|
33
|
+
console: The Rich Console instance used for output formatting
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def emit(self, record: logging.LogRecord) -> None:
|
|
37
|
+
"""
|
|
38
|
+
Emit a log record with carriage return prefix.
|
|
39
|
+
|
|
40
|
+
This method overrides the parent emit method to first output a carriage
|
|
41
|
+
return character, moving the cursor to the beginning of the line before
|
|
42
|
+
writing the actual log message. This prevents Ctrl+C characters from
|
|
43
|
+
appearing in the middle of log output.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
record: The LogRecord instance containing the log message and metadata
|
|
47
|
+
|
|
48
|
+
Note:
|
|
49
|
+
The carriage return is only written if the console is a terminal.
|
|
50
|
+
Errors during emission are handled by the parent class's handleError method.
|
|
51
|
+
"""
|
|
52
|
+
try:
|
|
53
|
+
# Write carriage return to move cursor to line start
|
|
54
|
+
if self.console.is_terminal:
|
|
55
|
+
self.console.file.write('\r')
|
|
56
|
+
self.console.file.flush()
|
|
57
|
+
|
|
58
|
+
# Call the parent emit method to handle the actual log output
|
|
59
|
+
super().emit(record)
|
|
60
|
+
|
|
61
|
+
except Exception:
|
|
62
|
+
# Let the parent class handle any errors during log emission
|
|
63
|
+
self.handleError(record)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class InterceptHandler(logging.Handler):
|
|
67
|
+
"""
|
|
68
|
+
A logging handler that intercepts standard Python logging messages and redirects them to loguru.
|
|
69
|
+
|
|
70
|
+
This handler bridges the gap between Python's standard logging module and loguru,
|
|
71
|
+
allowing all log messages to be processed through loguru's enhanced formatting
|
|
72
|
+
and filtering capabilities while maintaining compatibility with existing code
|
|
73
|
+
that uses standard logging.
|
|
74
|
+
|
|
75
|
+
Attributes:
|
|
76
|
+
level: The minimum log level this handler will process
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
def __init__(self, level: int = 0) -> None:
|
|
80
|
+
"""
|
|
81
|
+
Initialize the InterceptHandler.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
level: The minimum logging level to handle (default: 0 for all levels)
|
|
85
|
+
"""
|
|
86
|
+
super().__init__(level)
|
|
87
|
+
|
|
88
|
+
def emit(self, record: logging.LogRecord) -> None:
|
|
89
|
+
"""
|
|
90
|
+
Intercept a logging record and redirect it to loguru.
|
|
91
|
+
|
|
92
|
+
This method converts standard Python logging records to loguru format,
|
|
93
|
+
preserving the original log level, message content, and exception information.
|
|
94
|
+
It also attempts to maintain the correct call stack depth for accurate
|
|
95
|
+
source location reporting.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
record: The LogRecord instance from Python's standard logging
|
|
99
|
+
"""
|
|
100
|
+
try:
|
|
101
|
+
# Try to map the logging level name to loguru's level system
|
|
102
|
+
level = logger.level(record.levelname).name
|
|
103
|
+
except ValueError:
|
|
104
|
+
# If the level name is not recognized, use the numeric level
|
|
105
|
+
level = record.levelno
|
|
106
|
+
|
|
107
|
+
# Find the correct frame in the call stack to report accurate source location
|
|
108
|
+
frame = inspect.currentframe()
|
|
109
|
+
depth = 0
|
|
110
|
+
while frame and (depth == 0 or frame.f_code.co_filename == logging.__file__):
|
|
111
|
+
frame = frame.f_back
|
|
112
|
+
depth += 1
|
|
113
|
+
|
|
114
|
+
# Extract and clean the log message text
|
|
115
|
+
text = record.getMessage()
|
|
116
|
+
# Convert ANSI escape sequences to plain text for consistent formatting
|
|
117
|
+
text = Text.from_ansi(text).plain
|
|
118
|
+
|
|
119
|
+
# Forward the message to loguru with preserved context
|
|
120
|
+
logger.opt(depth=depth, exception=record.exc_info).log(level, text)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def apply_logging_adapter(level: int = logging.INFO) -> None:
|
|
124
|
+
"""
|
|
125
|
+
Configure Python's standard logging to use the InterceptHandler.
|
|
126
|
+
|
|
127
|
+
This function replaces the standard logging configuration with our custom
|
|
128
|
+
InterceptHandler, ensuring all logging messages are processed through loguru.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
level: The minimum logging level to capture (default: logging.INFO)
|
|
132
|
+
|
|
133
|
+
Note:
|
|
134
|
+
This function uses force=True to override any existing logging configuration.
|
|
135
|
+
"""
|
|
136
|
+
logging.basicConfig(handlers=[InterceptHandler()], level=level, force=True)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def init_logging(level: int = logging.INFO, **kwargs: Any) -> None:
|
|
140
|
+
"""
|
|
141
|
+
Initialize the complete logging configuration for the CLI application.
|
|
142
|
+
|
|
143
|
+
This function sets up a comprehensive logging system that:
|
|
144
|
+
- Intercepts standard Python logging and routes it through loguru
|
|
145
|
+
- Configures Rich-based formatting for enhanced terminal output
|
|
146
|
+
- Supports custom log levels including trace-level debugging
|
|
147
|
+
- Provides consistent formatting with timestamps
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
level: The minimum logging level to display (default: logging.INFO)
|
|
151
|
+
**kwargs: Additional keyword arguments passed to CarriageReturnRichHandler
|
|
152
|
+
|
|
153
|
+
The logging system supports the following levels:
|
|
154
|
+
- logging.INFO (20): Standard informational messages
|
|
155
|
+
- logging.DEBUG (10): Detailed debugging information
|
|
156
|
+
- 5: Trace-level debugging (most verbose)
|
|
157
|
+
|
|
158
|
+
Example:
|
|
159
|
+
>>> init_logging(level=logging.DEBUG)
|
|
160
|
+
>>> logger.info("Application started")
|
|
161
|
+
>>> logger.debug("Debug information")
|
|
162
|
+
"""
|
|
163
|
+
# Set up the logging adapter to intercept standard logging
|
|
164
|
+
apply_logging_adapter(level)
|
|
165
|
+
|
|
166
|
+
# Remove any existing loguru handlers to start fresh
|
|
167
|
+
logger.remove()
|
|
168
|
+
|
|
169
|
+
# Create our custom Rich handler with carriage return support
|
|
170
|
+
handler = CarriageReturnRichHandler(
|
|
171
|
+
console=console,
|
|
172
|
+
markup=True,
|
|
173
|
+
rich_tracebacks=True,
|
|
174
|
+
tracebacks_suppress=[asyncio],
|
|
175
|
+
**kwargs
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# Set a simple timestamp format for log messages
|
|
179
|
+
handler.setFormatter(logging.Formatter(None, "[%m/%d %H:%M]"))
|
|
180
|
+
|
|
181
|
+
# Add the handler to loguru with minimal formatting (Rich handles the styling)
|
|
182
|
+
logger.add(handler, format="{message}", level=level)
|