pylance-mcp-server 1.0.0
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.
- package/LICENSE +21 -0
- package/README.md +213 -0
- package/bin/pylance-mcp.js +68 -0
- package/mcp_server/__init__.py +13 -0
- package/mcp_server/__pycache__/__init__.cpython-312.pyc +0 -0
- package/mcp_server/__pycache__/__init__.cpython-313.pyc +0 -0
- package/mcp_server/__pycache__/__init__.cpython-314.pyc +0 -0
- package/mcp_server/__pycache__/ai_features.cpython-313.pyc +0 -0
- package/mcp_server/__pycache__/api_routes.cpython-313.pyc +0 -0
- package/mcp_server/__pycache__/auth.cpython-313.pyc +0 -0
- package/mcp_server/__pycache__/cloud_sync.cpython-313.pyc +0 -0
- package/mcp_server/__pycache__/logging_db.cpython-312.pyc +0 -0
- package/mcp_server/__pycache__/logging_db.cpython-313.pyc +0 -0
- package/mcp_server/__pycache__/pylance_bridge.cpython-312.pyc +0 -0
- package/mcp_server/__pycache__/pylance_bridge.cpython-313.pyc +0 -0
- package/mcp_server/__pycache__/pylance_bridge.cpython-314.pyc +0 -0
- package/mcp_server/__pycache__/resources.cpython-312.pyc +0 -0
- package/mcp_server/__pycache__/resources.cpython-313.pyc +0 -0
- package/mcp_server/__pycache__/tools.cpython-312.pyc +0 -0
- package/mcp_server/__pycache__/tools.cpython-313.pyc +0 -0
- package/mcp_server/__pycache__/tracing.cpython-313.pyc +0 -0
- package/mcp_server/ai_features.py +274 -0
- package/mcp_server/api_routes.py +429 -0
- package/mcp_server/auth.py +275 -0
- package/mcp_server/cloud_sync.py +427 -0
- package/mcp_server/logging_db.py +403 -0
- package/mcp_server/pylance_bridge.py +579 -0
- package/mcp_server/resources.py +174 -0
- package/mcp_server/tools.py +642 -0
- package/mcp_server/tracing.py +84 -0
- package/package.json +53 -0
- package/requirements.txt +29 -0
- package/scripts/check-python.js +57 -0
- package/server.py +1228 -0
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Conversation Logging Database
|
|
3
|
+
Logs all user prompts, LLM responses, and tool calls for training data collection.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sqlite3
|
|
7
|
+
import json
|
|
8
|
+
import time
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional, Dict, Any, List
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from contextlib import contextmanager
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ConversationLogger:
|
|
16
|
+
"""SQLite database for logging MCP conversations and tool usage."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, db_path: Optional[str] = None):
|
|
19
|
+
"""
|
|
20
|
+
Initialize conversation logger.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
db_path: Path to SQLite database file. Defaults to ~/.pylance_mcp/conversations.db
|
|
24
|
+
"""
|
|
25
|
+
if db_path is None:
|
|
26
|
+
db_dir = Path.home() / ".pylance_mcp"
|
|
27
|
+
db_dir.mkdir(parents=True, exist_ok=True)
|
|
28
|
+
db_path = str(db_dir / "conversations.db")
|
|
29
|
+
|
|
30
|
+
self.db_path = db_path
|
|
31
|
+
self._init_database()
|
|
32
|
+
|
|
33
|
+
def _init_database(self):
|
|
34
|
+
"""Create database tables if they don't exist."""
|
|
35
|
+
with self._get_connection() as conn:
|
|
36
|
+
cursor = conn.cursor()
|
|
37
|
+
|
|
38
|
+
# Conversations table - tracks user sessions
|
|
39
|
+
cursor.execute("""
|
|
40
|
+
CREATE TABLE IF NOT EXISTS conversations (
|
|
41
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
42
|
+
session_id TEXT UNIQUE NOT NULL,
|
|
43
|
+
workspace_root TEXT,
|
|
44
|
+
python_version TEXT,
|
|
45
|
+
venv_path TEXT,
|
|
46
|
+
started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
47
|
+
ended_at TIMESTAMP,
|
|
48
|
+
total_messages INTEGER DEFAULT 0,
|
|
49
|
+
total_tool_calls INTEGER DEFAULT 0,
|
|
50
|
+
metadata TEXT
|
|
51
|
+
)
|
|
52
|
+
""")
|
|
53
|
+
|
|
54
|
+
# Messages table - logs every user prompt and LLM response
|
|
55
|
+
cursor.execute("""
|
|
56
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
57
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
58
|
+
conversation_id INTEGER NOT NULL,
|
|
59
|
+
message_type TEXT NOT NULL,
|
|
60
|
+
role TEXT NOT NULL,
|
|
61
|
+
content TEXT NOT NULL,
|
|
62
|
+
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
63
|
+
token_count INTEGER,
|
|
64
|
+
model_name TEXT,
|
|
65
|
+
metadata TEXT,
|
|
66
|
+
FOREIGN KEY (conversation_id) REFERENCES conversations(id)
|
|
67
|
+
)
|
|
68
|
+
""")
|
|
69
|
+
|
|
70
|
+
# Tool calls table - logs every MCP tool invocation
|
|
71
|
+
cursor.execute("""
|
|
72
|
+
CREATE TABLE IF NOT EXISTS tool_calls (
|
|
73
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
74
|
+
message_id INTEGER,
|
|
75
|
+
conversation_id INTEGER NOT NULL,
|
|
76
|
+
tool_name TEXT NOT NULL,
|
|
77
|
+
arguments TEXT NOT NULL,
|
|
78
|
+
result TEXT,
|
|
79
|
+
success BOOLEAN,
|
|
80
|
+
error_message TEXT,
|
|
81
|
+
execution_time_ms REAL,
|
|
82
|
+
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
83
|
+
FOREIGN KEY (message_id) REFERENCES messages(id),
|
|
84
|
+
FOREIGN KEY (conversation_id) REFERENCES conversations(id)
|
|
85
|
+
)
|
|
86
|
+
""")
|
|
87
|
+
|
|
88
|
+
# Files modified table - tracks which files were changed
|
|
89
|
+
cursor.execute("""
|
|
90
|
+
CREATE TABLE IF NOT EXISTS files_modified (
|
|
91
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
92
|
+
tool_call_id INTEGER NOT NULL,
|
|
93
|
+
file_path TEXT NOT NULL,
|
|
94
|
+
operation TEXT NOT NULL,
|
|
95
|
+
lines_added INTEGER DEFAULT 0,
|
|
96
|
+
lines_removed INTEGER DEFAULT 0,
|
|
97
|
+
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
98
|
+
FOREIGN KEY (tool_call_id) REFERENCES tool_calls(id)
|
|
99
|
+
)
|
|
100
|
+
""")
|
|
101
|
+
|
|
102
|
+
# Training data view - optimized for LLM training
|
|
103
|
+
cursor.execute("""
|
|
104
|
+
CREATE VIEW IF NOT EXISTS training_data AS
|
|
105
|
+
SELECT
|
|
106
|
+
c.session_id,
|
|
107
|
+
c.workspace_root,
|
|
108
|
+
m.id as message_id,
|
|
109
|
+
m.role,
|
|
110
|
+
m.content,
|
|
111
|
+
m.timestamp,
|
|
112
|
+
m.model_name,
|
|
113
|
+
GROUP_CONCAT(tc.tool_name || '(' || tc.arguments || ')') as tools_used,
|
|
114
|
+
GROUP_CONCAT(fm.file_path) as files_modified,
|
|
115
|
+
COUNT(DISTINCT tc.id) as tool_call_count
|
|
116
|
+
FROM conversations c
|
|
117
|
+
JOIN messages m ON c.id = m.conversation_id
|
|
118
|
+
LEFT JOIN tool_calls tc ON m.id = tc.message_id
|
|
119
|
+
LEFT JOIN files_modified fm ON tc.id = fm.tool_call_id
|
|
120
|
+
GROUP BY m.id
|
|
121
|
+
ORDER BY m.timestamp
|
|
122
|
+
""")
|
|
123
|
+
|
|
124
|
+
# Create indexes for performance
|
|
125
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_messages_conversation ON messages(conversation_id)")
|
|
126
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_messages_timestamp ON messages(timestamp)")
|
|
127
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_tool_calls_conversation ON tool_calls(conversation_id)")
|
|
128
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_tool_calls_tool_name ON tool_calls(tool_name)")
|
|
129
|
+
|
|
130
|
+
conn.commit()
|
|
131
|
+
|
|
132
|
+
@contextmanager
|
|
133
|
+
def _get_connection(self):
|
|
134
|
+
"""Context manager for database connections."""
|
|
135
|
+
conn = sqlite3.connect(self.db_path)
|
|
136
|
+
conn.row_factory = sqlite3.Row
|
|
137
|
+
try:
|
|
138
|
+
yield conn
|
|
139
|
+
finally:
|
|
140
|
+
conn.close()
|
|
141
|
+
|
|
142
|
+
def start_conversation(self, session_id: str, workspace_root: str,
|
|
143
|
+
python_version: Optional[str] = None,
|
|
144
|
+
venv_path: Optional[str] = None,
|
|
145
|
+
metadata: Optional[Dict[str, Any]] = None) -> int:
|
|
146
|
+
"""
|
|
147
|
+
Start a new conversation session.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
session_id: Unique session identifier
|
|
151
|
+
workspace_root: Path to the workspace
|
|
152
|
+
python_version: Python version being used
|
|
153
|
+
venv_path: Virtual environment path
|
|
154
|
+
metadata: Additional metadata as dict
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
conversation_id: Database ID for this conversation
|
|
158
|
+
"""
|
|
159
|
+
with self._get_connection() as conn:
|
|
160
|
+
cursor = conn.cursor()
|
|
161
|
+
cursor.execute("""
|
|
162
|
+
INSERT INTO conversations (session_id, workspace_root, python_version, venv_path, metadata)
|
|
163
|
+
VALUES (?, ?, ?, ?, ?)
|
|
164
|
+
""", (session_id, workspace_root, python_version, venv_path,
|
|
165
|
+
json.dumps(metadata) if metadata else None))
|
|
166
|
+
conn.commit()
|
|
167
|
+
return cursor.lastrowid
|
|
168
|
+
|
|
169
|
+
def end_conversation(self, conversation_id: int):
|
|
170
|
+
"""Mark conversation as ended."""
|
|
171
|
+
with self._get_connection() as conn:
|
|
172
|
+
cursor = conn.cursor()
|
|
173
|
+
cursor.execute("""
|
|
174
|
+
UPDATE conversations
|
|
175
|
+
SET ended_at = CURRENT_TIMESTAMP,
|
|
176
|
+
total_messages = (SELECT COUNT(*) FROM messages WHERE conversation_id = ?),
|
|
177
|
+
total_tool_calls = (SELECT COUNT(*) FROM tool_calls WHERE conversation_id = ?)
|
|
178
|
+
WHERE id = ?
|
|
179
|
+
""", (conversation_id, conversation_id, conversation_id))
|
|
180
|
+
conn.commit()
|
|
181
|
+
|
|
182
|
+
def log_user_message(self, conversation_id: int, content: str,
|
|
183
|
+
metadata: Optional[Dict[str, Any]] = None) -> int:
|
|
184
|
+
"""
|
|
185
|
+
Log a user message (prompt).
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
conversation_id: ID of the conversation
|
|
189
|
+
content: User's message content
|
|
190
|
+
metadata: Additional metadata
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
message_id: Database ID for this message
|
|
194
|
+
"""
|
|
195
|
+
return self._log_message(conversation_id, "user_prompt", "user", content, metadata=metadata)
|
|
196
|
+
|
|
197
|
+
def log_assistant_message(self, conversation_id: int, content: str,
|
|
198
|
+
model_name: Optional[str] = None,
|
|
199
|
+
token_count: Optional[int] = None,
|
|
200
|
+
metadata: Optional[Dict[str, Any]] = None) -> int:
|
|
201
|
+
"""
|
|
202
|
+
Log an assistant message (LLM response).
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
conversation_id: ID of the conversation
|
|
206
|
+
content: Assistant's response content
|
|
207
|
+
model_name: Name of the LLM model used
|
|
208
|
+
token_count: Number of tokens in response
|
|
209
|
+
metadata: Additional metadata
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
message_id: Database ID for this message
|
|
213
|
+
"""
|
|
214
|
+
return self._log_message(
|
|
215
|
+
conversation_id, "assistant_response", "assistant", content,
|
|
216
|
+
model_name=model_name, token_count=token_count, metadata=metadata
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
def _log_message(self, conversation_id: int, message_type: str, role: str,
|
|
220
|
+
content: str, model_name: Optional[str] = None,
|
|
221
|
+
token_count: Optional[int] = None,
|
|
222
|
+
metadata: Optional[Dict[str, Any]] = None) -> int:
|
|
223
|
+
"""Internal method to log a message."""
|
|
224
|
+
with self._get_connection() as conn:
|
|
225
|
+
cursor = conn.cursor()
|
|
226
|
+
cursor.execute("""
|
|
227
|
+
INSERT INTO messages (conversation_id, message_type, role, content,
|
|
228
|
+
model_name, token_count, metadata)
|
|
229
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
230
|
+
""", (conversation_id, message_type, role, content, model_name, token_count,
|
|
231
|
+
json.dumps(metadata) if metadata else None))
|
|
232
|
+
conn.commit()
|
|
233
|
+
return cursor.lastrowid
|
|
234
|
+
|
|
235
|
+
def log_tool_call(self, conversation_id: int, tool_name: str, arguments: Dict[str, Any],
|
|
236
|
+
result: Optional[Any] = None, success: bool = True,
|
|
237
|
+
error_message: Optional[str] = None, execution_time_ms: Optional[float] = None,
|
|
238
|
+
message_id: Optional[int] = None) -> int:
|
|
239
|
+
"""
|
|
240
|
+
Log a tool call.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
conversation_id: ID of the conversation
|
|
244
|
+
tool_name: Name of the tool called
|
|
245
|
+
arguments: Tool arguments as dict
|
|
246
|
+
result: Tool execution result
|
|
247
|
+
success: Whether the tool call succeeded
|
|
248
|
+
error_message: Error message if failed
|
|
249
|
+
execution_time_ms: Execution time in milliseconds
|
|
250
|
+
message_id: Associated message ID (optional)
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
tool_call_id: Database ID for this tool call
|
|
254
|
+
"""
|
|
255
|
+
with self._get_connection() as conn:
|
|
256
|
+
cursor = conn.cursor()
|
|
257
|
+
cursor.execute("""
|
|
258
|
+
INSERT INTO tool_calls (conversation_id, message_id, tool_name, arguments,
|
|
259
|
+
result, success, error_message, execution_time_ms)
|
|
260
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
261
|
+
""", (conversation_id, message_id, tool_name, json.dumps(arguments),
|
|
262
|
+
json.dumps(result) if result else None, success, error_message, execution_time_ms))
|
|
263
|
+
conn.commit()
|
|
264
|
+
return cursor.lastrowid
|
|
265
|
+
|
|
266
|
+
def log_file_modification(self, tool_call_id: int, file_path: str, operation: str,
|
|
267
|
+
lines_added: int = 0, lines_removed: int = 0):
|
|
268
|
+
"""
|
|
269
|
+
Log a file modification.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
tool_call_id: ID of the tool call that modified the file
|
|
273
|
+
file_path: Path to the modified file
|
|
274
|
+
operation: Type of operation (edit, create, delete, rename)
|
|
275
|
+
lines_added: Number of lines added
|
|
276
|
+
lines_removed: Number of lines removed
|
|
277
|
+
"""
|
|
278
|
+
with self._get_connection() as conn:
|
|
279
|
+
cursor = conn.cursor()
|
|
280
|
+
cursor.execute("""
|
|
281
|
+
INSERT INTO files_modified (tool_call_id, file_path, operation,
|
|
282
|
+
lines_added, lines_removed)
|
|
283
|
+
VALUES (?, ?, ?, ?, ?)
|
|
284
|
+
""", (tool_call_id, file_path, operation, lines_added, lines_removed))
|
|
285
|
+
conn.commit()
|
|
286
|
+
|
|
287
|
+
def get_conversation(self, conversation_id: int) -> Optional[Dict[str, Any]]:
|
|
288
|
+
"""Get conversation details."""
|
|
289
|
+
with self._get_connection() as conn:
|
|
290
|
+
cursor = conn.cursor()
|
|
291
|
+
cursor.execute("SELECT * FROM conversations WHERE id = ?", (conversation_id,))
|
|
292
|
+
row = cursor.fetchone()
|
|
293
|
+
return dict(row) if row else None
|
|
294
|
+
|
|
295
|
+
def get_messages(self, conversation_id: int) -> List[Dict[str, Any]]:
|
|
296
|
+
"""Get all messages in a conversation."""
|
|
297
|
+
with self._get_connection() as conn:
|
|
298
|
+
cursor = conn.cursor()
|
|
299
|
+
cursor.execute("""
|
|
300
|
+
SELECT * FROM messages
|
|
301
|
+
WHERE conversation_id = ?
|
|
302
|
+
ORDER BY timestamp
|
|
303
|
+
""", (conversation_id,))
|
|
304
|
+
return [dict(row) for row in cursor.fetchall()]
|
|
305
|
+
|
|
306
|
+
def get_tool_calls(self, conversation_id: int) -> List[Dict[str, Any]]:
|
|
307
|
+
"""Get all tool calls in a conversation."""
|
|
308
|
+
with self._get_connection() as conn:
|
|
309
|
+
cursor = conn.cursor()
|
|
310
|
+
cursor.execute("""
|
|
311
|
+
SELECT * FROM tool_calls
|
|
312
|
+
WHERE conversation_id = ?
|
|
313
|
+
ORDER BY timestamp
|
|
314
|
+
""", (conversation_id,))
|
|
315
|
+
return [dict(row) for row in cursor.fetchall()]
|
|
316
|
+
|
|
317
|
+
def export_training_data(self, output_path: str, format: str = "jsonl"):
|
|
318
|
+
"""
|
|
319
|
+
Export conversation data for LLM training.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
output_path: Path to output file
|
|
323
|
+
format: Export format ("jsonl" or "json")
|
|
324
|
+
"""
|
|
325
|
+
with self._get_connection() as conn:
|
|
326
|
+
cursor = conn.cursor()
|
|
327
|
+
cursor.execute("SELECT * FROM training_data")
|
|
328
|
+
rows = cursor.fetchall()
|
|
329
|
+
|
|
330
|
+
training_examples = []
|
|
331
|
+
for row in rows:
|
|
332
|
+
example = dict(row)
|
|
333
|
+
training_examples.append(example)
|
|
334
|
+
|
|
335
|
+
output_file = Path(output_path)
|
|
336
|
+
if format == "jsonl":
|
|
337
|
+
with open(output_file, 'w', encoding='utf-8') as f:
|
|
338
|
+
for example in training_examples:
|
|
339
|
+
f.write(json.dumps(example) + '\n')
|
|
340
|
+
else: # json
|
|
341
|
+
with open(output_file, 'w', encoding='utf-8') as f:
|
|
342
|
+
json.dump(training_examples, f, indent=2)
|
|
343
|
+
|
|
344
|
+
def get_statistics(self) -> Dict[str, Any]:
|
|
345
|
+
"""Get overall statistics from the database."""
|
|
346
|
+
with self._get_connection() as conn:
|
|
347
|
+
cursor = conn.cursor()
|
|
348
|
+
|
|
349
|
+
stats = {}
|
|
350
|
+
|
|
351
|
+
# Total conversations
|
|
352
|
+
cursor.execute("SELECT COUNT(*) as count FROM conversations")
|
|
353
|
+
stats['total_conversations'] = cursor.fetchone()['count']
|
|
354
|
+
|
|
355
|
+
# Total messages
|
|
356
|
+
cursor.execute("SELECT COUNT(*) as count FROM messages")
|
|
357
|
+
stats['total_messages'] = cursor.fetchone()['count']
|
|
358
|
+
|
|
359
|
+
# Total tool calls
|
|
360
|
+
cursor.execute("SELECT COUNT(*) as count FROM tool_calls")
|
|
361
|
+
stats['total_tool_calls'] = cursor.fetchone()['count']
|
|
362
|
+
|
|
363
|
+
# Most used tools
|
|
364
|
+
cursor.execute("""
|
|
365
|
+
SELECT tool_name, COUNT(*) as count
|
|
366
|
+
FROM tool_calls
|
|
367
|
+
GROUP BY tool_name
|
|
368
|
+
ORDER BY count DESC
|
|
369
|
+
LIMIT 10
|
|
370
|
+
""")
|
|
371
|
+
stats['top_tools'] = [dict(row) for row in cursor.fetchall()]
|
|
372
|
+
|
|
373
|
+
# Average tools per conversation
|
|
374
|
+
cursor.execute("""
|
|
375
|
+
SELECT AVG(total_tool_calls) as avg_tools
|
|
376
|
+
FROM conversations
|
|
377
|
+
WHERE total_tool_calls > 0
|
|
378
|
+
""")
|
|
379
|
+
stats['avg_tools_per_conversation'] = cursor.fetchone()['avg_tools']
|
|
380
|
+
|
|
381
|
+
# Files modified count
|
|
382
|
+
cursor.execute("SELECT COUNT(DISTINCT file_path) as count FROM files_modified")
|
|
383
|
+
stats['unique_files_modified'] = cursor.fetchone()['count']
|
|
384
|
+
|
|
385
|
+
return stats
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
# Global logger instance
|
|
389
|
+
_logger: Optional[ConversationLogger] = None
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def get_logger() -> ConversationLogger:
|
|
393
|
+
"""Get or create the global conversation logger."""
|
|
394
|
+
global _logger
|
|
395
|
+
if _logger is None:
|
|
396
|
+
_logger = ConversationLogger()
|
|
397
|
+
return _logger
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def set_logger(logger: ConversationLogger):
|
|
401
|
+
"""Set a custom logger instance."""
|
|
402
|
+
global _logger
|
|
403
|
+
_logger = logger
|