unrealon 1.1.1__py3-none-any.whl → 1.1.4__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.
- unrealon/__init__.py +16 -6
- unrealon-1.1.4.dist-info/METADATA +658 -0
- unrealon-1.1.4.dist-info/RECORD +54 -0
- {unrealon-1.1.1.dist-info → unrealon-1.1.4.dist-info}/entry_points.txt +1 -1
- unrealon_browser/__init__.py +3 -6
- unrealon_browser/core/browser_manager.py +86 -84
- unrealon_browser/dto/models/config.py +2 -0
- unrealon_browser/managers/captcha.py +165 -185
- unrealon_browser/managers/cookies.py +57 -28
- unrealon_browser/managers/logger_bridge.py +94 -34
- unrealon_browser/managers/profile.py +186 -158
- unrealon_browser/managers/stealth.py +58 -47
- unrealon_driver/__init__.py +8 -21
- unrealon_driver/exceptions.py +5 -0
- unrealon_driver/html_analyzer/__init__.py +32 -0
- unrealon_driver/{parser/managers/html.py → html_analyzer/cleaner.py} +330 -405
- unrealon_driver/html_analyzer/config.py +64 -0
- unrealon_driver/html_analyzer/manager.py +247 -0
- unrealon_driver/html_analyzer/models.py +115 -0
- unrealon_driver/html_analyzer/websocket_analyzer.py +157 -0
- unrealon_driver/models/__init__.py +31 -0
- unrealon_driver/models/websocket.py +98 -0
- unrealon_driver/parser/__init__.py +4 -23
- unrealon_driver/parser/cli_manager.py +6 -5
- unrealon_driver/parser/daemon_manager.py +242 -66
- unrealon_driver/parser/managers/__init__.py +0 -21
- unrealon_driver/parser/managers/config.py +15 -3
- unrealon_driver/parser/parser_manager.py +225 -395
- unrealon_driver/smart_logging/__init__.py +24 -0
- unrealon_driver/smart_logging/models.py +44 -0
- unrealon_driver/smart_logging/smart_logger.py +406 -0
- unrealon_driver/smart_logging/unified_logger.py +525 -0
- unrealon_driver/websocket/__init__.py +31 -0
- unrealon_driver/websocket/client.py +249 -0
- unrealon_driver/websocket/config.py +188 -0
- unrealon_driver/websocket/manager.py +90 -0
- unrealon-1.1.1.dist-info/METADATA +0 -722
- unrealon-1.1.1.dist-info/RECORD +0 -82
- unrealon_bridge/__init__.py +0 -114
- unrealon_bridge/cli.py +0 -316
- unrealon_bridge/client/__init__.py +0 -93
- unrealon_bridge/client/base.py +0 -78
- unrealon_bridge/client/commands.py +0 -89
- unrealon_bridge/client/connection.py +0 -90
- unrealon_bridge/client/events.py +0 -65
- unrealon_bridge/client/health.py +0 -38
- unrealon_bridge/client/html_parser.py +0 -146
- unrealon_bridge/client/logging.py +0 -139
- unrealon_bridge/client/proxy.py +0 -70
- unrealon_bridge/client/scheduler.py +0 -450
- unrealon_bridge/client/session.py +0 -70
- unrealon_bridge/configs/__init__.py +0 -14
- unrealon_bridge/configs/bridge_config.py +0 -212
- unrealon_bridge/configs/bridge_config.yaml +0 -39
- unrealon_bridge/models/__init__.py +0 -138
- unrealon_bridge/models/base.py +0 -28
- unrealon_bridge/models/command.py +0 -41
- unrealon_bridge/models/events.py +0 -40
- unrealon_bridge/models/html_parser.py +0 -79
- unrealon_bridge/models/logging.py +0 -55
- unrealon_bridge/models/parser.py +0 -63
- unrealon_bridge/models/proxy.py +0 -41
- unrealon_bridge/models/requests.py +0 -95
- unrealon_bridge/models/responses.py +0 -88
- unrealon_bridge/models/scheduler.py +0 -592
- unrealon_bridge/models/session.py +0 -28
- unrealon_bridge/server/__init__.py +0 -91
- unrealon_bridge/server/base.py +0 -171
- unrealon_bridge/server/handlers/__init__.py +0 -23
- unrealon_bridge/server/handlers/command.py +0 -110
- unrealon_bridge/server/handlers/html_parser.py +0 -139
- unrealon_bridge/server/handlers/logging.py +0 -95
- unrealon_bridge/server/handlers/parser.py +0 -95
- unrealon_bridge/server/handlers/proxy.py +0 -75
- unrealon_bridge/server/handlers/scheduler.py +0 -545
- unrealon_bridge/server/handlers/session.py +0 -66
- unrealon_driver/browser/__init__.py +0 -8
- unrealon_driver/browser/config.py +0 -74
- unrealon_driver/browser/manager.py +0 -416
- unrealon_driver/parser/managers/browser.py +0 -51
- unrealon_driver/parser/managers/logging.py +0 -609
- {unrealon-1.1.1.dist-info → unrealon-1.1.4.dist-info}/WHEEL +0 -0
- {unrealon-1.1.1.dist-info → unrealon-1.1.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unified Logger: Smart + Rich + Structured.
|
|
3
|
+
|
|
4
|
+
Combines:
|
|
5
|
+
- SmartLogger: WebSocket batching, connection management
|
|
6
|
+
- Rich console: Beautiful colored output
|
|
7
|
+
- Structured logging: Context management and metadata
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import logging
|
|
12
|
+
import yaml
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Optional, Any
|
|
15
|
+
from pydantic import BaseModel, Field, ConfigDict
|
|
16
|
+
from datetime import datetime, timezone
|
|
17
|
+
|
|
18
|
+
from rich.console import Console
|
|
19
|
+
from rich.text import Text
|
|
20
|
+
|
|
21
|
+
from .smart_logger import ConnectionManager, LogBuffer
|
|
22
|
+
from .models import LogLevel, LogContext, LogEntry
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_project_root() -> Path:
|
|
26
|
+
"""
|
|
27
|
+
Get the project root directory (backend/unrealon-rpc/).
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Path to the project root directory
|
|
31
|
+
"""
|
|
32
|
+
# This file is in src/unrealon_driver/smart_logging/unified_logger.py
|
|
33
|
+
# Path levels: unified_logger.py -> smart_logging -> unrealon_driver -> src -> backend/unrealon-rpc/
|
|
34
|
+
return Path(__file__).parent.parent.parent.parent
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def resolve_log_path(relative_path: str) -> Path:
|
|
38
|
+
"""
|
|
39
|
+
Resolve a relative log path to an absolute path within the project.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
relative_path: Relative path like "logs/parser_name.log"
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Absolute path to the log file
|
|
46
|
+
"""
|
|
47
|
+
project_root = get_project_root()
|
|
48
|
+
return project_root / relative_path
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class UnifiedLoggerConfig(BaseModel):
|
|
52
|
+
"""Configuration for unified logger"""
|
|
53
|
+
model_config = ConfigDict(
|
|
54
|
+
validate_assignment=True,
|
|
55
|
+
extra="forbid"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Parser identity
|
|
59
|
+
parser_id: str = Field(..., min_length=1)
|
|
60
|
+
parser_name: str = Field(..., min_length=1)
|
|
61
|
+
|
|
62
|
+
# Local logging
|
|
63
|
+
log_file: Optional[Path] = Field(default=None)
|
|
64
|
+
console_enabled: bool = Field(default=True)
|
|
65
|
+
file_enabled: bool = Field(default=True)
|
|
66
|
+
|
|
67
|
+
# WebSocket logging
|
|
68
|
+
bridge_logs_url: Optional[str] = Field(default=None)
|
|
69
|
+
batch_interval: float = Field(default=5.0, gt=0.0)
|
|
70
|
+
daemon_mode: Optional[bool] = Field(default=None)
|
|
71
|
+
|
|
72
|
+
# Log levels
|
|
73
|
+
console_level: LogLevel = Field(default=LogLevel.INFO)
|
|
74
|
+
file_level: LogLevel = Field(default=LogLevel.DEBUG)
|
|
75
|
+
bridge_level: LogLevel = Field(default=LogLevel.INFO)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class UnifiedLogger:
|
|
79
|
+
"""
|
|
80
|
+
🚀 Unified Logger: Smart + Rich + Structured
|
|
81
|
+
|
|
82
|
+
Features from SmartLogger:
|
|
83
|
+
- WebSocket batching and transport
|
|
84
|
+
- Smart connection management
|
|
85
|
+
- Daemon vs script mode detection
|
|
86
|
+
|
|
87
|
+
Features from LegacyManager:
|
|
88
|
+
- Rich console output with colors
|
|
89
|
+
- Structured logging with context
|
|
90
|
+
- Multiple output destinations
|
|
91
|
+
|
|
92
|
+
Developer Experience:
|
|
93
|
+
- Simple API: logger.info("message")
|
|
94
|
+
- Context management: logger.set_session("123")
|
|
95
|
+
- Automatic batching and fallback
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
def __init__(self, config: UnifiedLoggerConfig):
|
|
99
|
+
self.config = config
|
|
100
|
+
self._context = LogContext()
|
|
101
|
+
|
|
102
|
+
# Rich console
|
|
103
|
+
self.console = Console() if config.console_enabled else None
|
|
104
|
+
|
|
105
|
+
# Local file logger
|
|
106
|
+
self.file_logger = self._setup_file_logger() if config.file_enabled else None
|
|
107
|
+
|
|
108
|
+
# SmartLogger components (WebSocket)
|
|
109
|
+
self.bridge_enabled = config.bridge_logs_url is not None
|
|
110
|
+
if self.bridge_enabled:
|
|
111
|
+
self.log_buffer = LogBuffer()
|
|
112
|
+
self.connection_manager = ConnectionManager(
|
|
113
|
+
config.bridge_logs_url,
|
|
114
|
+
config.parser_id
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Detect daemon mode
|
|
118
|
+
daemon_mode = config.daemon_mode
|
|
119
|
+
if daemon_mode is None:
|
|
120
|
+
daemon_mode = self._detect_daemon_mode()
|
|
121
|
+
self.connection_manager.set_daemon_mode(daemon_mode)
|
|
122
|
+
|
|
123
|
+
# Start batch timer
|
|
124
|
+
self._batch_task = None
|
|
125
|
+
self._ensure_batch_timer()
|
|
126
|
+
else:
|
|
127
|
+
self.log_buffer = None
|
|
128
|
+
self.connection_manager = None
|
|
129
|
+
self._batch_task = None
|
|
130
|
+
|
|
131
|
+
# ==========================================
|
|
132
|
+
# PUBLIC LOGGING API
|
|
133
|
+
# ==========================================
|
|
134
|
+
|
|
135
|
+
def debug(self, message: str, **kwargs):
|
|
136
|
+
"""Log DEBUG message"""
|
|
137
|
+
self._log(LogLevel.DEBUG, message, **kwargs)
|
|
138
|
+
|
|
139
|
+
def info(self, message: str, **kwargs):
|
|
140
|
+
"""Log INFO message"""
|
|
141
|
+
self._log(LogLevel.INFO, message, **kwargs)
|
|
142
|
+
|
|
143
|
+
def warning(self, message: str, **kwargs):
|
|
144
|
+
"""Log WARNING message"""
|
|
145
|
+
self._log(LogLevel.WARNING, message, **kwargs)
|
|
146
|
+
|
|
147
|
+
def error(self, message: str, **kwargs):
|
|
148
|
+
"""Log ERROR message"""
|
|
149
|
+
self._log(LogLevel.ERROR, message, **kwargs)
|
|
150
|
+
|
|
151
|
+
def critical(self, message: str, **kwargs):
|
|
152
|
+
"""Log CRITICAL message"""
|
|
153
|
+
self._log(LogLevel.CRITICAL, message, **kwargs)
|
|
154
|
+
|
|
155
|
+
# Aliases
|
|
156
|
+
warn = warning
|
|
157
|
+
|
|
158
|
+
# ==========================================
|
|
159
|
+
# CONTEXT MANAGEMENT
|
|
160
|
+
# ==========================================
|
|
161
|
+
|
|
162
|
+
def set_session(self, session_id: str):
|
|
163
|
+
"""Set session ID for all future logs"""
|
|
164
|
+
if not session_id.strip():
|
|
165
|
+
raise ValueError("Session ID cannot be empty")
|
|
166
|
+
self._context.session_id = session_id
|
|
167
|
+
|
|
168
|
+
def set_operation(self, operation: str):
|
|
169
|
+
"""Set current operation"""
|
|
170
|
+
if not operation.strip():
|
|
171
|
+
raise ValueError("Operation cannot be empty")
|
|
172
|
+
self._context.operation = operation
|
|
173
|
+
|
|
174
|
+
def set_url(self, url: str):
|
|
175
|
+
"""Set current URL"""
|
|
176
|
+
if not url.strip():
|
|
177
|
+
raise ValueError("URL cannot be empty")
|
|
178
|
+
self._context.url = url
|
|
179
|
+
|
|
180
|
+
def add_context_data(self, key: str, value: str):
|
|
181
|
+
"""Add additional context data"""
|
|
182
|
+
if not key.strip():
|
|
183
|
+
raise ValueError("Context key cannot be empty")
|
|
184
|
+
self._context.additional_data[key] = str(value)
|
|
185
|
+
|
|
186
|
+
def clear_context(self):
|
|
187
|
+
"""Clear all context"""
|
|
188
|
+
self._context = LogContext()
|
|
189
|
+
|
|
190
|
+
# ==========================================
|
|
191
|
+
# SPECIALIZED LOGGING METHODS
|
|
192
|
+
# ==========================================
|
|
193
|
+
|
|
194
|
+
def start_operation(self, operation: str, **kwargs):
|
|
195
|
+
"""Log start of operation"""
|
|
196
|
+
self.set_operation(operation)
|
|
197
|
+
self.info(f"🚀 Starting {operation}", **kwargs)
|
|
198
|
+
|
|
199
|
+
def end_operation(self, operation: str, duration: Optional[float] = None, **kwargs):
|
|
200
|
+
"""Log end of operation"""
|
|
201
|
+
if duration is not None:
|
|
202
|
+
self.info(f"✅ Completed {operation} in {duration:.2f}s", duration=str(duration), **kwargs)
|
|
203
|
+
else:
|
|
204
|
+
self.info(f"✅ Completed {operation}", **kwargs)
|
|
205
|
+
|
|
206
|
+
def fail_operation(self, operation: str, error: str, **kwargs):
|
|
207
|
+
"""Log failed operation"""
|
|
208
|
+
self.error(f"❌ Failed {operation}: {error}", error=error, **kwargs)
|
|
209
|
+
|
|
210
|
+
def url_access(self, url: str, status: str = "accessing", **kwargs):
|
|
211
|
+
"""Log URL access"""
|
|
212
|
+
self.set_url(url)
|
|
213
|
+
self.info(f"🌐 {status.title()} URL: {url}", status=status, **kwargs)
|
|
214
|
+
|
|
215
|
+
def data_extracted(self, data_type: str, count: int, **kwargs):
|
|
216
|
+
"""Log data extraction"""
|
|
217
|
+
if count < 0:
|
|
218
|
+
raise ValueError("Count must be non-negative")
|
|
219
|
+
|
|
220
|
+
self.info(
|
|
221
|
+
f"📦 Extracted {count} {data_type}",
|
|
222
|
+
data_type=data_type,
|
|
223
|
+
count=str(count),
|
|
224
|
+
**kwargs
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# ==========================================
|
|
228
|
+
# INTERNAL IMPLEMENTATION
|
|
229
|
+
# ==========================================
|
|
230
|
+
|
|
231
|
+
def _format_value_for_console(self, value: Any) -> str:
|
|
232
|
+
"""Format value for console output with YAML for complex objects"""
|
|
233
|
+
if value is None:
|
|
234
|
+
return "None"
|
|
235
|
+
elif isinstance(value, (str, int, float, bool)):
|
|
236
|
+
return str(value)
|
|
237
|
+
elif isinstance(value, (dict, list, tuple)):
|
|
238
|
+
try:
|
|
239
|
+
# Use YAML for complex objects - more readable than JSON
|
|
240
|
+
yaml_str = yaml.dump(value, default_flow_style=True, allow_unicode=True)
|
|
241
|
+
# Remove trailing newline and make it one-line for console
|
|
242
|
+
return yaml_str.strip().replace('\n', ' ')
|
|
243
|
+
except Exception:
|
|
244
|
+
return str(value)
|
|
245
|
+
else:
|
|
246
|
+
# For custom objects, try to convert to dict first
|
|
247
|
+
try:
|
|
248
|
+
if hasattr(value, 'model_dump'): # Pydantic
|
|
249
|
+
dict_value = value.model_dump()
|
|
250
|
+
elif hasattr(value, '__dict__'): # Regular objects
|
|
251
|
+
dict_value = value.__dict__
|
|
252
|
+
else:
|
|
253
|
+
return str(value)
|
|
254
|
+
|
|
255
|
+
yaml_str = yaml.dump(dict_value, default_flow_style=True, allow_unicode=True)
|
|
256
|
+
return yaml_str.strip().replace('\n', ' ')
|
|
257
|
+
except Exception:
|
|
258
|
+
return str(value)
|
|
259
|
+
|
|
260
|
+
def _log(self, level: LogLevel, message: str, **kwargs):
|
|
261
|
+
"""Internal logging method"""
|
|
262
|
+
# Create context from current context + kwargs
|
|
263
|
+
context = self._create_context(**kwargs)
|
|
264
|
+
|
|
265
|
+
# Create log entry
|
|
266
|
+
log_entry = LogEntry(
|
|
267
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
268
|
+
level=level.value,
|
|
269
|
+
message=message,
|
|
270
|
+
parser_id=self.config.parser_id,
|
|
271
|
+
session_id=context.session_id,
|
|
272
|
+
url=context.url,
|
|
273
|
+
operation=context.operation,
|
|
274
|
+
extra=context.additional_data if context.additional_data else None
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
# Console output (Rich)
|
|
278
|
+
if self.console and self._should_log_to_console(level):
|
|
279
|
+
self._log_to_console(log_entry, level)
|
|
280
|
+
|
|
281
|
+
# File output
|
|
282
|
+
if self.file_logger and self._should_log_to_file(level):
|
|
283
|
+
self._log_to_file(log_entry, level)
|
|
284
|
+
|
|
285
|
+
# WebSocket output (batched) - ALWAYS send to bridge regardless of level
|
|
286
|
+
if self.bridge_enabled:
|
|
287
|
+
self._log_to_bridge(log_entry)
|
|
288
|
+
|
|
289
|
+
def _create_context(self, **kwargs) -> LogContext:
|
|
290
|
+
"""Create log context from current context and kwargs"""
|
|
291
|
+
context_data = self._context.additional_data.copy()
|
|
292
|
+
|
|
293
|
+
# Add kwargs - keep original types for console formatting
|
|
294
|
+
for key, value in kwargs.items():
|
|
295
|
+
if key not in ['session_id', 'command_id', 'operation', 'url']:
|
|
296
|
+
context_data[key] = value # Keep original type
|
|
297
|
+
|
|
298
|
+
return LogContext(
|
|
299
|
+
session_id=kwargs.get('session_id') or self._context.session_id,
|
|
300
|
+
command_id=kwargs.get('command_id') or self._context.command_id,
|
|
301
|
+
operation=kwargs.get('operation') or self._context.operation,
|
|
302
|
+
url=kwargs.get('url') or self._context.url,
|
|
303
|
+
additional_data=context_data
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
def _should_log_to_console(self, level: LogLevel) -> bool:
|
|
307
|
+
"""Check if should log to console"""
|
|
308
|
+
level_value = getattr(logging, level.value)
|
|
309
|
+
console_level_value = getattr(logging, self.config.console_level.value)
|
|
310
|
+
return level_value >= console_level_value
|
|
311
|
+
|
|
312
|
+
def _should_log_to_file(self, level: LogLevel) -> bool:
|
|
313
|
+
"""Check if should log to file"""
|
|
314
|
+
level_value = getattr(logging, level.value)
|
|
315
|
+
file_level_value = getattr(logging, self.config.file_level.value)
|
|
316
|
+
return level_value >= file_level_value
|
|
317
|
+
|
|
318
|
+
def _should_log_to_bridge(self, level: LogLevel) -> bool:
|
|
319
|
+
"""Check if should log to bridge - ALWAYS True now"""
|
|
320
|
+
return True # Always send to bridge regardless of level
|
|
321
|
+
|
|
322
|
+
def _log_to_console(self, log_entry: LogEntry, level: LogLevel):
|
|
323
|
+
"""Log to console with Rich formatting"""
|
|
324
|
+
if not self.console:
|
|
325
|
+
return
|
|
326
|
+
|
|
327
|
+
# Parse timestamp for display
|
|
328
|
+
try:
|
|
329
|
+
from datetime import datetime
|
|
330
|
+
timestamp = datetime.fromisoformat(log_entry.timestamp.replace('Z', '+00:00'))
|
|
331
|
+
time_str = timestamp.strftime('%H:%M:%S')
|
|
332
|
+
except:
|
|
333
|
+
time_str = "00:00:00"
|
|
334
|
+
|
|
335
|
+
# Color based on level
|
|
336
|
+
level_colors = {
|
|
337
|
+
LogLevel.DEBUG: "dim white",
|
|
338
|
+
LogLevel.INFO: "bright_blue",
|
|
339
|
+
LogLevel.WARNING: "yellow",
|
|
340
|
+
LogLevel.ERROR: "red",
|
|
341
|
+
LogLevel.CRITICAL: "bold red"
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
level_color = level_colors.get(level, "white")
|
|
345
|
+
|
|
346
|
+
# Format message
|
|
347
|
+
formatted_message = Text()
|
|
348
|
+
formatted_message.append(f"[{time_str}] ", style="dim")
|
|
349
|
+
formatted_message.append(f"{level.value}", style=level_color)
|
|
350
|
+
formatted_message.append(f" - {self.config.parser_name} - ", style="dim")
|
|
351
|
+
formatted_message.append(log_entry.message, style="white")
|
|
352
|
+
|
|
353
|
+
# Add context if available
|
|
354
|
+
context_parts = []
|
|
355
|
+
if log_entry.extra:
|
|
356
|
+
for key, value in log_entry.extra.items():
|
|
357
|
+
context_parts.append(f"{key}={value}")
|
|
358
|
+
|
|
359
|
+
if context_parts:
|
|
360
|
+
formatted_message.append(f" ({', '.join(context_parts)})", style="dim")
|
|
361
|
+
|
|
362
|
+
self.console.print(formatted_message)
|
|
363
|
+
|
|
364
|
+
def _log_to_file(self, log_entry: LogEntry, level: LogLevel):
|
|
365
|
+
"""Log to file"""
|
|
366
|
+
if not self.file_logger:
|
|
367
|
+
return
|
|
368
|
+
|
|
369
|
+
try:
|
|
370
|
+
# Create log message with context
|
|
371
|
+
extra_info = ""
|
|
372
|
+
if log_entry.extra:
|
|
373
|
+
context_parts = [f"{k}={v}" for k, v in log_entry.extra.items()]
|
|
374
|
+
extra_info = f" - {', '.join(context_parts)}"
|
|
375
|
+
|
|
376
|
+
full_message = f"{log_entry.message}{extra_info}"
|
|
377
|
+
|
|
378
|
+
# Log to file
|
|
379
|
+
log_level = getattr(logging, level.value)
|
|
380
|
+
self.file_logger.log(log_level, full_message)
|
|
381
|
+
|
|
382
|
+
except Exception:
|
|
383
|
+
# Fail silently for file logging
|
|
384
|
+
pass
|
|
385
|
+
|
|
386
|
+
def _log_to_bridge(self, log_entry: LogEntry):
|
|
387
|
+
"""Log to bridge via WebSocket (batched)"""
|
|
388
|
+
if not self.log_buffer:
|
|
389
|
+
return
|
|
390
|
+
|
|
391
|
+
# Add to buffer (non-blocking)
|
|
392
|
+
try:
|
|
393
|
+
asyncio.create_task(self.log_buffer.add(log_entry))
|
|
394
|
+
except RuntimeError:
|
|
395
|
+
# No event loop, skip bridge logging
|
|
396
|
+
pass
|
|
397
|
+
|
|
398
|
+
def _setup_file_logger(self) -> Optional[logging.Logger]:
|
|
399
|
+
"""Setup file logger"""
|
|
400
|
+
if not self.config.log_file:
|
|
401
|
+
return None
|
|
402
|
+
|
|
403
|
+
# Ensure log directory exists
|
|
404
|
+
self.config.log_file.parent.mkdir(parents=True, exist_ok=True)
|
|
405
|
+
|
|
406
|
+
# Create file logger
|
|
407
|
+
logger_name = f"unified_{self.config.parser_id}"
|
|
408
|
+
logger = logging.getLogger(logger_name)
|
|
409
|
+
logger.setLevel(getattr(logging, self.config.file_level.value))
|
|
410
|
+
|
|
411
|
+
# Remove existing handlers
|
|
412
|
+
for handler in logger.handlers[:]:
|
|
413
|
+
logger.removeHandler(handler)
|
|
414
|
+
|
|
415
|
+
# Add file handler
|
|
416
|
+
file_handler = logging.FileHandler(self.config.log_file, encoding='utf-8')
|
|
417
|
+
file_handler.setLevel(getattr(logging, self.config.file_level.value))
|
|
418
|
+
|
|
419
|
+
# Format for file logging
|
|
420
|
+
formatter = logging.Formatter(
|
|
421
|
+
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
422
|
+
datefmt='%Y-%m-%d %H:%M:%S'
|
|
423
|
+
)
|
|
424
|
+
file_handler.setFormatter(formatter)
|
|
425
|
+
|
|
426
|
+
logger.addHandler(file_handler)
|
|
427
|
+
logger.propagate = False
|
|
428
|
+
|
|
429
|
+
return logger
|
|
430
|
+
|
|
431
|
+
def _detect_daemon_mode(self) -> bool:
|
|
432
|
+
"""Auto-detect daemon vs script mode"""
|
|
433
|
+
try:
|
|
434
|
+
# If there's an active event loop, likely daemon mode
|
|
435
|
+
asyncio.get_running_loop()
|
|
436
|
+
return True
|
|
437
|
+
except RuntimeError:
|
|
438
|
+
return False
|
|
439
|
+
|
|
440
|
+
def _ensure_batch_timer(self):
|
|
441
|
+
"""Ensure batch timer is started"""
|
|
442
|
+
if self._batch_task is None or self._batch_task.done():
|
|
443
|
+
try:
|
|
444
|
+
asyncio.get_running_loop()
|
|
445
|
+
self._batch_task = asyncio.create_task(self._batch_loop())
|
|
446
|
+
except RuntimeError:
|
|
447
|
+
# No event loop, will start later
|
|
448
|
+
pass
|
|
449
|
+
|
|
450
|
+
async def _batch_loop(self):
|
|
451
|
+
"""Main batch sending loop"""
|
|
452
|
+
try:
|
|
453
|
+
while True:
|
|
454
|
+
await asyncio.sleep(self.config.batch_interval)
|
|
455
|
+
await self._send_batch()
|
|
456
|
+
except asyncio.CancelledError:
|
|
457
|
+
# Final batch send on cancellation
|
|
458
|
+
await self._send_batch()
|
|
459
|
+
raise
|
|
460
|
+
|
|
461
|
+
async def _send_batch(self):
|
|
462
|
+
"""Send accumulated logs"""
|
|
463
|
+
if not self.log_buffer or not self.connection_manager:
|
|
464
|
+
return
|
|
465
|
+
|
|
466
|
+
entries = await self.log_buffer.flush()
|
|
467
|
+
if entries:
|
|
468
|
+
await self.connection_manager.send_batch(entries)
|
|
469
|
+
|
|
470
|
+
async def flush(self):
|
|
471
|
+
"""Force send all accumulated logs"""
|
|
472
|
+
if self.bridge_enabled:
|
|
473
|
+
await self._send_batch()
|
|
474
|
+
|
|
475
|
+
async def close(self):
|
|
476
|
+
"""Close logger and cleanup resources"""
|
|
477
|
+
# Send remaining logs
|
|
478
|
+
await self.flush()
|
|
479
|
+
|
|
480
|
+
# Stop batch timer
|
|
481
|
+
if self._batch_task and not self._batch_task.done():
|
|
482
|
+
self._batch_task.cancel()
|
|
483
|
+
try:
|
|
484
|
+
await self._batch_task
|
|
485
|
+
except asyncio.CancelledError:
|
|
486
|
+
pass
|
|
487
|
+
|
|
488
|
+
# Close connection
|
|
489
|
+
if self.connection_manager:
|
|
490
|
+
await self.connection_manager.close()
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def create_unified_logger(
|
|
494
|
+
parser_id: str,
|
|
495
|
+
parser_name: str,
|
|
496
|
+
bridge_logs_url: Optional[str] = None,
|
|
497
|
+
log_file: Optional[Path] = None,
|
|
498
|
+
**kwargs
|
|
499
|
+
) -> UnifiedLogger:
|
|
500
|
+
"""
|
|
501
|
+
Create unified logger with optimal settings.
|
|
502
|
+
|
|
503
|
+
Args:
|
|
504
|
+
parser_id: Parser identifier
|
|
505
|
+
parser_name: Parser name
|
|
506
|
+
bridge_logs_url: WebSocket URL for Bridge logs
|
|
507
|
+
log_file: Local log file path (if None, creates default path)
|
|
508
|
+
**kwargs: Additional configuration
|
|
509
|
+
|
|
510
|
+
Returns:
|
|
511
|
+
Configured UnifiedLogger instance
|
|
512
|
+
"""
|
|
513
|
+
# Auto-create log file path if not provided
|
|
514
|
+
if log_file is None and kwargs.get('file_enabled', True):
|
|
515
|
+
log_file = resolve_log_path(f"logs/{parser_name}.log")
|
|
516
|
+
|
|
517
|
+
config = UnifiedLoggerConfig(
|
|
518
|
+
parser_id=parser_id,
|
|
519
|
+
parser_name=parser_name,
|
|
520
|
+
bridge_logs_url=bridge_logs_url,
|
|
521
|
+
log_file=log_file,
|
|
522
|
+
**kwargs
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
return UnifiedLogger(config)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WebSocket module for unrealon_driver.
|
|
3
|
+
|
|
4
|
+
Provides independent WebSocket connectivity for:
|
|
5
|
+
- Logging transport
|
|
6
|
+
- HTML analysis requests
|
|
7
|
+
- Other driver-server communication
|
|
8
|
+
|
|
9
|
+
Features automatic URL detection - no need to specify URLs in config files!
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from .client import WebSocketClient, WebSocketConfig
|
|
13
|
+
from .manager import WebSocketManager
|
|
14
|
+
from .config import (
|
|
15
|
+
GlobalWebSocketConfig, Environment, global_websocket_config,
|
|
16
|
+
get_websocket_url, get_environment, set_environment,
|
|
17
|
+
get_debug_info, is_production, is_development, is_local
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# Global websocket manager instance
|
|
21
|
+
websocket_manager = WebSocketManager()
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
# Client and manager
|
|
25
|
+
"WebSocketClient", "WebSocketConfig", "WebSocketManager", "websocket_manager",
|
|
26
|
+
# Global configuration
|
|
27
|
+
"GlobalWebSocketConfig", "Environment", "global_websocket_config",
|
|
28
|
+
# Convenience functions
|
|
29
|
+
"get_websocket_url", "get_environment", "set_environment",
|
|
30
|
+
"get_debug_info", "is_production", "is_development", "is_local"
|
|
31
|
+
]
|