matrixone-python-sdk 0.1.0__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.
- matrixone/__init__.py +155 -0
- matrixone/account.py +723 -0
- matrixone/async_client.py +3913 -0
- matrixone/async_metadata_manager.py +311 -0
- matrixone/async_orm.py +123 -0
- matrixone/async_vector_index_manager.py +633 -0
- matrixone/base_client.py +208 -0
- matrixone/client.py +4672 -0
- matrixone/config.py +452 -0
- matrixone/connection_hooks.py +286 -0
- matrixone/exceptions.py +89 -0
- matrixone/logger.py +782 -0
- matrixone/metadata.py +820 -0
- matrixone/moctl.py +219 -0
- matrixone/orm.py +2277 -0
- matrixone/pitr.py +646 -0
- matrixone/pubsub.py +771 -0
- matrixone/restore.py +411 -0
- matrixone/search_vector_index.py +1176 -0
- matrixone/snapshot.py +550 -0
- matrixone/sql_builder.py +844 -0
- matrixone/sqlalchemy_ext/__init__.py +161 -0
- matrixone/sqlalchemy_ext/adapters.py +163 -0
- matrixone/sqlalchemy_ext/dialect.py +534 -0
- matrixone/sqlalchemy_ext/fulltext_index.py +895 -0
- matrixone/sqlalchemy_ext/fulltext_search.py +1686 -0
- matrixone/sqlalchemy_ext/hnsw_config.py +194 -0
- matrixone/sqlalchemy_ext/ivf_config.py +252 -0
- matrixone/sqlalchemy_ext/table_builder.py +351 -0
- matrixone/sqlalchemy_ext/vector_index.py +1721 -0
- matrixone/sqlalchemy_ext/vector_type.py +948 -0
- matrixone/version.py +580 -0
- matrixone_python_sdk-0.1.0.dist-info/METADATA +706 -0
- matrixone_python_sdk-0.1.0.dist-info/RECORD +122 -0
- matrixone_python_sdk-0.1.0.dist-info/WHEEL +5 -0
- matrixone_python_sdk-0.1.0.dist-info/entry_points.txt +5 -0
- matrixone_python_sdk-0.1.0.dist-info/licenses/LICENSE +200 -0
- matrixone_python_sdk-0.1.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +19 -0
- tests/offline/__init__.py +20 -0
- tests/offline/conftest.py +77 -0
- tests/offline/test_account.py +703 -0
- tests/offline/test_async_client_query_comprehensive.py +1218 -0
- tests/offline/test_basic.py +54 -0
- tests/offline/test_case_sensitivity.py +227 -0
- tests/offline/test_connection_hooks_offline.py +287 -0
- tests/offline/test_dialect_schema_handling.py +609 -0
- tests/offline/test_explain_methods.py +346 -0
- tests/offline/test_filter_logical_in.py +237 -0
- tests/offline/test_fulltext_search_comprehensive.py +795 -0
- tests/offline/test_ivf_config.py +249 -0
- tests/offline/test_join_methods.py +281 -0
- tests/offline/test_join_sqlalchemy_compatibility.py +276 -0
- tests/offline/test_logical_in_method.py +237 -0
- tests/offline/test_matrixone_version_parsing.py +264 -0
- tests/offline/test_metadata_offline.py +557 -0
- tests/offline/test_moctl.py +300 -0
- tests/offline/test_moctl_simple.py +251 -0
- tests/offline/test_model_support_offline.py +359 -0
- tests/offline/test_model_support_simple.py +225 -0
- tests/offline/test_pinecone_filter_offline.py +377 -0
- tests/offline/test_pitr.py +585 -0
- tests/offline/test_pubsub.py +712 -0
- tests/offline/test_query_update.py +283 -0
- tests/offline/test_restore.py +445 -0
- tests/offline/test_snapshot_comprehensive.py +384 -0
- tests/offline/test_sql_escaping_edge_cases.py +551 -0
- tests/offline/test_sqlalchemy_integration.py +382 -0
- tests/offline/test_sqlalchemy_vector_integration.py +434 -0
- tests/offline/test_table_builder.py +198 -0
- tests/offline/test_unified_filter.py +398 -0
- tests/offline/test_unified_transaction.py +495 -0
- tests/offline/test_vector_index.py +238 -0
- tests/offline/test_vector_operations.py +688 -0
- tests/offline/test_vector_type.py +174 -0
- tests/offline/test_version_core.py +328 -0
- tests/offline/test_version_management.py +372 -0
- tests/offline/test_version_standalone.py +652 -0
- tests/online/__init__.py +20 -0
- tests/online/conftest.py +216 -0
- tests/online/test_account_management.py +194 -0
- tests/online/test_advanced_features.py +344 -0
- tests/online/test_async_client_interfaces.py +330 -0
- tests/online/test_async_client_online.py +285 -0
- tests/online/test_async_model_insert_online.py +293 -0
- tests/online/test_async_orm_online.py +300 -0
- tests/online/test_async_simple_query_online.py +802 -0
- tests/online/test_async_transaction_simple_query.py +300 -0
- tests/online/test_basic_connection.py +130 -0
- tests/online/test_client_online.py +238 -0
- tests/online/test_config.py +90 -0
- tests/online/test_config_validation.py +123 -0
- tests/online/test_connection_hooks_new_online.py +217 -0
- tests/online/test_dialect_schema_handling_online.py +331 -0
- tests/online/test_filter_logical_in_online.py +374 -0
- tests/online/test_fulltext_comprehensive.py +1773 -0
- tests/online/test_fulltext_label_online.py +433 -0
- tests/online/test_fulltext_search_online.py +842 -0
- tests/online/test_ivf_stats_online.py +506 -0
- tests/online/test_logger_integration.py +311 -0
- tests/online/test_matrixone_query_orm.py +540 -0
- tests/online/test_metadata_online.py +579 -0
- tests/online/test_model_insert_online.py +255 -0
- tests/online/test_mysql_driver_validation.py +213 -0
- tests/online/test_orm_advanced_features.py +2022 -0
- tests/online/test_orm_cte_integration.py +269 -0
- tests/online/test_orm_online.py +270 -0
- tests/online/test_pinecone_filter.py +708 -0
- tests/online/test_pubsub_operations.py +352 -0
- tests/online/test_query_methods.py +225 -0
- tests/online/test_query_update_online.py +433 -0
- tests/online/test_search_vector_index.py +557 -0
- tests/online/test_simple_fulltext_online.py +915 -0
- tests/online/test_snapshot_comprehensive.py +998 -0
- tests/online/test_sqlalchemy_engine_integration.py +336 -0
- tests/online/test_sqlalchemy_integration.py +425 -0
- tests/online/test_transaction_contexts.py +1219 -0
- tests/online/test_transaction_insert_methods.py +356 -0
- tests/online/test_transaction_query_methods.py +288 -0
- tests/online/test_unified_filter_online.py +529 -0
- tests/online/test_vector_comprehensive.py +706 -0
- tests/online/test_version_management.py +291 -0
matrixone/logger.py
ADDED
@@ -0,0 +1,782 @@
|
|
1
|
+
# Copyright 2021 - 2022 Matrix Origin
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
"""
|
16
|
+
MatrixOne Logger Module
|
17
|
+
|
18
|
+
Provides logging functionality for MatrixOne Python SDK with support for:
|
19
|
+
1. Default logger configuration
|
20
|
+
2. Custom logger integration
|
21
|
+
3. Structured logging
|
22
|
+
4. Performance logging
|
23
|
+
5. Error tracking
|
24
|
+
"""
|
25
|
+
|
26
|
+
import logging
|
27
|
+
import sys
|
28
|
+
from typing import Optional
|
29
|
+
|
30
|
+
|
31
|
+
class MatrixOneLogger:
|
32
|
+
"""
|
33
|
+
MatrixOne Logger class that provides structured logging for the SDK
|
34
|
+
|
35
|
+
Features:
|
36
|
+
- Default logger configuration
|
37
|
+
- Custom logger integration
|
38
|
+
- Performance logging
|
39
|
+
- Error tracking
|
40
|
+
- Structured log messages
|
41
|
+
"""
|
42
|
+
|
43
|
+
def __init__(
|
44
|
+
self,
|
45
|
+
logger: Optional[logging.Logger] = None,
|
46
|
+
level: int = logging.INFO,
|
47
|
+
format_string: Optional[str] = None,
|
48
|
+
sql_log_mode: str = "auto",
|
49
|
+
slow_query_threshold: float = 1.0,
|
50
|
+
max_sql_display_length: int = 500,
|
51
|
+
):
|
52
|
+
"""
|
53
|
+
Initialize MatrixOne logger
|
54
|
+
|
55
|
+
Args::
|
56
|
+
|
57
|
+
logger: Custom logger instance. If None, creates a default logger
|
58
|
+
level: Logging level (default: INFO)
|
59
|
+
format_string: Custom format string for log messages
|
60
|
+
sql_log_mode: SQL logging mode ('off', 'auto', 'simple', 'full')
|
61
|
+
- 'off': No SQL logging
|
62
|
+
- 'auto': Smart logging - short SQL shown fully, long SQL summarized (default)
|
63
|
+
- 'simple': Show operation summary only (e.g., "INSERT INTO table (5 rows)")
|
64
|
+
- 'full': Show complete SQL regardless of length
|
65
|
+
slow_query_threshold: Threshold in seconds for slow query warnings (default: 1.0)
|
66
|
+
max_sql_display_length: Maximum SQL length in auto mode before summarizing (default: 500)
|
67
|
+
"""
|
68
|
+
# Validate sql_log_mode
|
69
|
+
valid_modes = ['off', 'auto', 'simple', 'full']
|
70
|
+
if sql_log_mode not in valid_modes:
|
71
|
+
raise ValueError(f"Invalid sql_log_mode '{sql_log_mode}'. Must be one of {valid_modes}")
|
72
|
+
|
73
|
+
self.sql_log_mode = sql_log_mode
|
74
|
+
self.slow_query_threshold = slow_query_threshold
|
75
|
+
self.max_sql_display_length = max_sql_display_length
|
76
|
+
|
77
|
+
if logger is not None:
|
78
|
+
# Use provided logger
|
79
|
+
self.logger = logger
|
80
|
+
self._is_custom = True
|
81
|
+
else:
|
82
|
+
# Create default logger
|
83
|
+
self.logger = self._create_default_logger(level, format_string)
|
84
|
+
self._is_custom = False
|
85
|
+
|
86
|
+
def _create_default_logger(self, level: int, format_string: Optional[str]) -> logging.Logger:
|
87
|
+
"""Create default logger with standard configuration"""
|
88
|
+
logger = logging.getLogger("matrixone")
|
89
|
+
|
90
|
+
# Avoid adding multiple handlers
|
91
|
+
if logger.handlers:
|
92
|
+
return logger
|
93
|
+
|
94
|
+
# Set level
|
95
|
+
logger.setLevel(level)
|
96
|
+
|
97
|
+
# Create console handler
|
98
|
+
console_handler = logging.StreamHandler(sys.stdout)
|
99
|
+
console_handler.setLevel(level)
|
100
|
+
|
101
|
+
# Set format
|
102
|
+
if format_string is None:
|
103
|
+
format_string = "%(asctime)s|%(name)s|%(levelname)s|[%(filename)s:%(lineno)d]: %(message)s"
|
104
|
+
|
105
|
+
formatter = logging.Formatter(format_string)
|
106
|
+
console_handler.setFormatter(formatter)
|
107
|
+
|
108
|
+
# Add handler to logger
|
109
|
+
logger.addHandler(console_handler)
|
110
|
+
|
111
|
+
# Prevent propagation to root logger
|
112
|
+
logger.propagate = False
|
113
|
+
|
114
|
+
return logger
|
115
|
+
|
116
|
+
def debug(self, message: str, **kwargs):
|
117
|
+
"""Log debug message"""
|
118
|
+
# Use findCaller to get the actual caller's file and line
|
119
|
+
import sys
|
120
|
+
|
121
|
+
frame = sys._getframe(1)
|
122
|
+
filename = frame.f_code.co_filename
|
123
|
+
lineno = frame.f_lineno
|
124
|
+
# Create a new record with the caller's info
|
125
|
+
record = self.logger.makeRecord(
|
126
|
+
self.logger.name,
|
127
|
+
logging.DEBUG,
|
128
|
+
filename,
|
129
|
+
lineno,
|
130
|
+
self._format_message(message, **kwargs),
|
131
|
+
(),
|
132
|
+
None,
|
133
|
+
)
|
134
|
+
self.logger.handle(record)
|
135
|
+
|
136
|
+
def info(self, message: str, **kwargs):
|
137
|
+
"""Log info message"""
|
138
|
+
# Use findCaller to get the actual caller's file and line
|
139
|
+
import sys
|
140
|
+
|
141
|
+
frame = sys._getframe(1)
|
142
|
+
filename = frame.f_code.co_filename
|
143
|
+
lineno = frame.f_lineno
|
144
|
+
# Create a new record with the caller's info
|
145
|
+
record = self.logger.makeRecord(
|
146
|
+
self.logger.name,
|
147
|
+
logging.INFO,
|
148
|
+
filename,
|
149
|
+
lineno,
|
150
|
+
self._format_message(message, **kwargs),
|
151
|
+
(),
|
152
|
+
None,
|
153
|
+
)
|
154
|
+
self.logger.handle(record)
|
155
|
+
|
156
|
+
def warning(self, message: str, **kwargs):
|
157
|
+
"""Log warning message"""
|
158
|
+
# Use findCaller to get the actual caller's file and line
|
159
|
+
import sys
|
160
|
+
|
161
|
+
frame = sys._getframe(1)
|
162
|
+
filename = frame.f_code.co_filename
|
163
|
+
lineno = frame.f_lineno
|
164
|
+
# Create a new record with the caller's info
|
165
|
+
record = self.logger.makeRecord(
|
166
|
+
self.logger.name,
|
167
|
+
logging.WARNING,
|
168
|
+
filename,
|
169
|
+
lineno,
|
170
|
+
self._format_message(message, **kwargs),
|
171
|
+
(),
|
172
|
+
None,
|
173
|
+
)
|
174
|
+
self.logger.handle(record)
|
175
|
+
|
176
|
+
def error(self, message: str, **kwargs):
|
177
|
+
"""Log error message"""
|
178
|
+
# Use findCaller to get the actual caller's file and line
|
179
|
+
import sys
|
180
|
+
|
181
|
+
frame = sys._getframe(1)
|
182
|
+
filename = frame.f_code.co_filename
|
183
|
+
lineno = frame.f_lineno
|
184
|
+
# Create a new record with the caller's info
|
185
|
+
record = self.logger.makeRecord(
|
186
|
+
self.logger.name,
|
187
|
+
logging.ERROR,
|
188
|
+
filename,
|
189
|
+
lineno,
|
190
|
+
self._format_message(message, **kwargs),
|
191
|
+
(),
|
192
|
+
None,
|
193
|
+
)
|
194
|
+
self.logger.handle(record)
|
195
|
+
|
196
|
+
def critical(self, message: str, **kwargs):
|
197
|
+
"""Log critical message"""
|
198
|
+
# Use findCaller to get the actual caller's file and line
|
199
|
+
import sys
|
200
|
+
|
201
|
+
frame = sys._getframe(1)
|
202
|
+
filename = frame.f_code.co_filename
|
203
|
+
lineno = frame.f_lineno
|
204
|
+
# Create a new record with the caller's info
|
205
|
+
record = self.logger.makeRecord(
|
206
|
+
self.logger.name,
|
207
|
+
logging.CRITICAL,
|
208
|
+
filename,
|
209
|
+
lineno,
|
210
|
+
self._format_message(message, **kwargs),
|
211
|
+
(),
|
212
|
+
None,
|
213
|
+
)
|
214
|
+
self.logger.handle(record)
|
215
|
+
|
216
|
+
def _format_message(self, message: str, **kwargs) -> str:
|
217
|
+
"""Format message with additional context"""
|
218
|
+
if not kwargs:
|
219
|
+
return message
|
220
|
+
|
221
|
+
# Add context information
|
222
|
+
context_parts = []
|
223
|
+
for key, value in kwargs.items():
|
224
|
+
context_parts.append(f"{key}={value}")
|
225
|
+
|
226
|
+
context_str = " | ".join(context_parts)
|
227
|
+
return f"{message} | {context_str}"
|
228
|
+
|
229
|
+
def log_connection(self, host: str, port: int, user: str, database: str, success: bool = True):
|
230
|
+
"""Log connection events"""
|
231
|
+
status = "✓ Connected" if success else "✗ Connection failed"
|
232
|
+
# Use findCaller to get the actual caller's file and line
|
233
|
+
import sys
|
234
|
+
|
235
|
+
frame = sys._getframe(1)
|
236
|
+
filename = frame.f_code.co_filename
|
237
|
+
lineno = frame.f_lineno
|
238
|
+
# Create a new record with the caller's info
|
239
|
+
record = self.logger.makeRecord(
|
240
|
+
self.logger.name,
|
241
|
+
logging.INFO,
|
242
|
+
filename,
|
243
|
+
lineno,
|
244
|
+
self._format_message(f"{status} to MatrixOne", host=host, port=port, user=user, database=database),
|
245
|
+
(),
|
246
|
+
None,
|
247
|
+
)
|
248
|
+
self.logger.handle(record)
|
249
|
+
|
250
|
+
def log_disconnection(self, success: bool = True):
|
251
|
+
"""Log disconnection events"""
|
252
|
+
status = "✓ Disconnected" if success else "✗ Disconnection failed"
|
253
|
+
# Use findCaller to get the actual caller's file and line
|
254
|
+
import sys
|
255
|
+
|
256
|
+
frame = sys._getframe(1)
|
257
|
+
filename = frame.f_code.co_filename
|
258
|
+
lineno = frame.f_lineno
|
259
|
+
# Create a new record with the caller's info
|
260
|
+
record = self.logger.makeRecord(
|
261
|
+
self.logger.name, logging.INFO, filename, lineno, f"{status} from MatrixOne", (), None
|
262
|
+
)
|
263
|
+
self.logger.handle(record)
|
264
|
+
|
265
|
+
def _extract_sql_summary(self, sql: str) -> str:
|
266
|
+
"""
|
267
|
+
Extract operation summary from SQL query
|
268
|
+
|
269
|
+
Args::
|
270
|
+
|
271
|
+
sql: SQL query string
|
272
|
+
|
273
|
+
Returns::
|
274
|
+
|
275
|
+
Summary string describing the operation
|
276
|
+
"""
|
277
|
+
sql_stripped = sql.strip()
|
278
|
+
sql_upper = sql_stripped.upper()
|
279
|
+
|
280
|
+
# Extract operation type and key details
|
281
|
+
import re
|
282
|
+
|
283
|
+
# SELECT operations
|
284
|
+
if sql_upper.startswith('SELECT'):
|
285
|
+
match = re.search(r'FROM\s+([`"]?\w+[`"]?)', sql_upper, re.IGNORECASE)
|
286
|
+
table = match.group(1).strip('`"') if match else '?'
|
287
|
+
return f"SELECT FROM {table}"
|
288
|
+
|
289
|
+
# INSERT operations
|
290
|
+
elif sql_upper.startswith('INSERT'):
|
291
|
+
match = re.search(r'INSERT\s+INTO\s+([`"]?\w+[`"]?)', sql_upper, re.IGNORECASE)
|
292
|
+
table = match.group(1).strip('`"') if match else '?'
|
293
|
+
# Count rows in batch insert by counting VALUES groups
|
294
|
+
if 'VALUES' in sql_upper:
|
295
|
+
# Count the number of value groups by counting '),' patterns
|
296
|
+
values_count = sql_stripped.count('),(') + 1
|
297
|
+
if values_count > 1:
|
298
|
+
return f"BATCH INSERT INTO {table} ({values_count} rows)"
|
299
|
+
return f"INSERT INTO {table}"
|
300
|
+
|
301
|
+
# UPDATE operations
|
302
|
+
elif sql_upper.startswith('UPDATE'):
|
303
|
+
match = re.search(r'UPDATE\s+([`"]?\w+[`"]?)', sql_upper, re.IGNORECASE)
|
304
|
+
table = match.group(1).strip('`"') if match else '?'
|
305
|
+
return f"UPDATE {table}"
|
306
|
+
|
307
|
+
# DELETE operations
|
308
|
+
elif sql_upper.startswith('DELETE'):
|
309
|
+
match = re.search(r'DELETE\s+FROM\s+([`"]?\w+[`"]?)', sql_upper, re.IGNORECASE)
|
310
|
+
table = match.group(1).strip('`"') if match else '?'
|
311
|
+
return f"DELETE FROM {table}"
|
312
|
+
|
313
|
+
# CREATE operations
|
314
|
+
elif sql_upper.startswith('CREATE'):
|
315
|
+
if 'INDEX' in sql_upper:
|
316
|
+
match = re.search(r'ON\s+([`"]?\w+[`"]?)', sql_upper, re.IGNORECASE)
|
317
|
+
table = match.group(1).strip('`"') if match else '?'
|
318
|
+
return f"CREATE INDEX ON {table}"
|
319
|
+
elif 'TABLE' in sql_upper:
|
320
|
+
match = re.search(r'CREATE\s+TABLE\s+([`"]?\w+[`"]?)', sql_upper, re.IGNORECASE)
|
321
|
+
table = match.group(1).strip('`"') if match else '?'
|
322
|
+
return f"CREATE TABLE {table}"
|
323
|
+
return "CREATE"
|
324
|
+
|
325
|
+
# DROP operations
|
326
|
+
elif sql_upper.startswith('DROP'):
|
327
|
+
if 'TABLE' in sql_upper:
|
328
|
+
match = re.search(r'DROP\s+TABLE\s+(?:IF\s+EXISTS\s+)?([`"]?\w+[`"]?)', sql_upper, re.IGNORECASE)
|
329
|
+
table = match.group(1).strip('`"') if match else '?'
|
330
|
+
return f"DROP TABLE {table}"
|
331
|
+
elif 'INDEX' in sql_upper:
|
332
|
+
return "DROP INDEX"
|
333
|
+
return "DROP"
|
334
|
+
|
335
|
+
# SET operations
|
336
|
+
elif sql_upper.startswith('SET'):
|
337
|
+
return "SET VARIABLE"
|
338
|
+
|
339
|
+
# Other operations
|
340
|
+
else:
|
341
|
+
# Return first 50 characters as fallback
|
342
|
+
return sql_stripped[:50] + "..." if len(sql_stripped) > 50 else sql_stripped
|
343
|
+
|
344
|
+
def _format_sql_for_log(
|
345
|
+
self, sql: str, is_error: bool = False, is_slow: bool = False, override_mode: Optional[str] = None
|
346
|
+
) -> str:
|
347
|
+
"""
|
348
|
+
Format SQL query for logging based on mode and query characteristics
|
349
|
+
|
350
|
+
Args::
|
351
|
+
|
352
|
+
sql: SQL query string
|
353
|
+
is_error: Whether this is an error query
|
354
|
+
is_slow: Whether this is a slow query
|
355
|
+
override_mode: Optional mode override for this specific query
|
356
|
+
|
357
|
+
Returns::
|
358
|
+
|
359
|
+
Formatted SQL string for logging
|
360
|
+
"""
|
361
|
+
# Use override mode if provided, otherwise use instance mode
|
362
|
+
effective_mode = override_mode if override_mode is not None else self.sql_log_mode
|
363
|
+
|
364
|
+
# Mode 'off': Don't log SQL
|
365
|
+
if effective_mode == 'off':
|
366
|
+
return ""
|
367
|
+
|
368
|
+
# Errors and slow queries always show complete SQL for debugging
|
369
|
+
if is_error or is_slow:
|
370
|
+
return sql.strip()
|
371
|
+
|
372
|
+
# Mode 'full': Always show complete SQL
|
373
|
+
if effective_mode == 'full':
|
374
|
+
return sql.strip()
|
375
|
+
|
376
|
+
# Mode 'simple': Only show operation summary
|
377
|
+
if effective_mode == 'simple':
|
378
|
+
return self._extract_sql_summary(sql)
|
379
|
+
|
380
|
+
# Mode 'auto': Smart formatting based on length
|
381
|
+
if effective_mode == 'auto':
|
382
|
+
sql_stripped = sql.strip()
|
383
|
+
if len(sql_stripped) <= self.max_sql_display_length:
|
384
|
+
# Short SQL: show fully
|
385
|
+
return sql_stripped
|
386
|
+
else:
|
387
|
+
# Long SQL: show summary with length indicator
|
388
|
+
summary = self._extract_sql_summary(sql)
|
389
|
+
return f"{summary} [SQL length: {len(sql_stripped)} chars]"
|
390
|
+
|
391
|
+
return sql.strip()
|
392
|
+
|
393
|
+
def update_config(
|
394
|
+
self,
|
395
|
+
sql_log_mode: Optional[str] = None,
|
396
|
+
slow_query_threshold: Optional[float] = None,
|
397
|
+
max_sql_display_length: Optional[int] = None,
|
398
|
+
):
|
399
|
+
"""
|
400
|
+
Dynamically update logger configuration at runtime.
|
401
|
+
|
402
|
+
Args::
|
403
|
+
|
404
|
+
sql_log_mode: New SQL logging mode ('off', 'auto', 'simple', 'full')
|
405
|
+
slow_query_threshold: New threshold in seconds for slow query warnings
|
406
|
+
max_sql_display_length: New maximum SQL length in auto mode before summarizing
|
407
|
+
|
408
|
+
Example::
|
409
|
+
|
410
|
+
# Enable full SQL logging for debugging
|
411
|
+
client.logger.update_config(sql_log_mode='full')
|
412
|
+
|
413
|
+
# Update multiple settings
|
414
|
+
client.logger.update_config(
|
415
|
+
sql_log_mode='auto',
|
416
|
+
slow_query_threshold=2.0,
|
417
|
+
max_sql_display_length=1000
|
418
|
+
)
|
419
|
+
"""
|
420
|
+
if sql_log_mode is not None:
|
421
|
+
valid_modes = ['off', 'auto', 'simple', 'full']
|
422
|
+
if sql_log_mode not in valid_modes:
|
423
|
+
raise ValueError(f"Invalid sql_log_mode '{sql_log_mode}'. Must be one of {valid_modes}")
|
424
|
+
self.sql_log_mode = sql_log_mode
|
425
|
+
|
426
|
+
if slow_query_threshold is not None:
|
427
|
+
if slow_query_threshold < 0:
|
428
|
+
raise ValueError("slow_query_threshold must be non-negative")
|
429
|
+
self.slow_query_threshold = slow_query_threshold
|
430
|
+
|
431
|
+
if max_sql_display_length is not None:
|
432
|
+
if max_sql_display_length < 1:
|
433
|
+
raise ValueError("max_sql_display_length must be positive")
|
434
|
+
self.max_sql_display_length = max_sql_display_length
|
435
|
+
|
436
|
+
def log_query(
|
437
|
+
self,
|
438
|
+
query: str,
|
439
|
+
execution_time: Optional[float] = None,
|
440
|
+
affected_rows: Optional[int] = None,
|
441
|
+
success: bool = True,
|
442
|
+
override_sql_log_mode: Optional[str] = None,
|
443
|
+
):
|
444
|
+
"""
|
445
|
+
Log SQL query execution with smart formatting.
|
446
|
+
|
447
|
+
Args::
|
448
|
+
|
449
|
+
query: SQL query string
|
450
|
+
execution_time: Query execution time in seconds
|
451
|
+
affected_rows: Number of rows affected
|
452
|
+
success: Whether the query succeeded
|
453
|
+
override_sql_log_mode: Temporarily override sql_log_mode for this query only
|
454
|
+
"""
|
455
|
+
# Use override mode if provided, otherwise use instance mode
|
456
|
+
effective_mode = override_sql_log_mode if override_sql_log_mode is not None else self.sql_log_mode
|
457
|
+
|
458
|
+
# Skip logging if mode is 'off'
|
459
|
+
if effective_mode == 'off':
|
460
|
+
return
|
461
|
+
|
462
|
+
# Determine if this is a slow query or error
|
463
|
+
is_slow = execution_time is not None and execution_time >= self.slow_query_threshold
|
464
|
+
is_error = not success
|
465
|
+
|
466
|
+
# Format SQL based on mode and query characteristics
|
467
|
+
display_sql = self._format_sql_for_log(query, is_error=is_error, is_slow=is_slow, override_mode=effective_mode)
|
468
|
+
|
469
|
+
# Skip if no SQL to display
|
470
|
+
if not display_sql:
|
471
|
+
return
|
472
|
+
|
473
|
+
# Build log message
|
474
|
+
status_icon = "✓" if success else "✗"
|
475
|
+
message_parts = [status_icon]
|
476
|
+
|
477
|
+
# Add execution time if available
|
478
|
+
if execution_time is not None:
|
479
|
+
message_parts.append(f"{execution_time:.3f}s")
|
480
|
+
|
481
|
+
# Add affected rows if available
|
482
|
+
if affected_rows is not None:
|
483
|
+
message_parts.append(f"{affected_rows} rows")
|
484
|
+
|
485
|
+
# Add SQL with appropriate prefix
|
486
|
+
if is_error:
|
487
|
+
message_parts.append(f"[ERROR] {display_sql}")
|
488
|
+
elif is_slow:
|
489
|
+
message_parts.append(f"[SLOW] {display_sql}")
|
490
|
+
else:
|
491
|
+
message_parts.append(display_sql)
|
492
|
+
|
493
|
+
message = " | ".join(message_parts)
|
494
|
+
|
495
|
+
# Use findCaller to get the actual caller's file and line
|
496
|
+
import sys
|
497
|
+
|
498
|
+
frame = sys._getframe(1)
|
499
|
+
filename = frame.f_code.co_filename
|
500
|
+
lineno = frame.f_lineno
|
501
|
+
|
502
|
+
# Create a new record with the caller's info
|
503
|
+
record = self.logger.makeRecord(
|
504
|
+
self.logger.name,
|
505
|
+
logging.ERROR if is_error else logging.INFO,
|
506
|
+
filename,
|
507
|
+
lineno,
|
508
|
+
self._format_message(message),
|
509
|
+
(),
|
510
|
+
None,
|
511
|
+
)
|
512
|
+
|
513
|
+
self.logger.handle(record)
|
514
|
+
|
515
|
+
def log_performance(self, operation: str, duration: float, **kwargs):
|
516
|
+
"""Log performance metrics (always enabled, control via log level)"""
|
517
|
+
|
518
|
+
# Use findCaller to get the actual caller's file and line
|
519
|
+
import sys
|
520
|
+
|
521
|
+
frame = sys._getframe(1)
|
522
|
+
filename = frame.f_code.co_filename
|
523
|
+
lineno = frame.f_lineno
|
524
|
+
|
525
|
+
kwargs["duration"] = f"{duration:.3f}s"
|
526
|
+
# Create a new record with the caller's info
|
527
|
+
record = self.logger.makeRecord(
|
528
|
+
self.logger.name,
|
529
|
+
logging.INFO,
|
530
|
+
filename,
|
531
|
+
lineno,
|
532
|
+
self._format_message(f"Performance: {operation}", **kwargs),
|
533
|
+
(),
|
534
|
+
None,
|
535
|
+
)
|
536
|
+
self.logger.handle(record)
|
537
|
+
|
538
|
+
def log_error(self, error: Exception, context: Optional[str] = None, include_traceback: bool = False):
|
539
|
+
"""
|
540
|
+
Log errors with context and optional traceback.
|
541
|
+
|
542
|
+
Args:
|
543
|
+
error: The exception object
|
544
|
+
context: Optional context description (e.g., "Query execution", "Table creation")
|
545
|
+
include_traceback: If True, include full traceback in log (useful for debugging)
|
546
|
+
"""
|
547
|
+
error_type = type(error).__name__
|
548
|
+
error_message = str(error)
|
549
|
+
|
550
|
+
kwargs = {"error_type": error_type, "error_message": error_message}
|
551
|
+
if context:
|
552
|
+
kwargs["context"] = context
|
553
|
+
|
554
|
+
# Use findCaller to get the actual caller's file and line
|
555
|
+
import sys
|
556
|
+
|
557
|
+
frame = sys._getframe(1)
|
558
|
+
filename = frame.f_code.co_filename
|
559
|
+
lineno = frame.f_lineno
|
560
|
+
|
561
|
+
# Prepare error message
|
562
|
+
error_msg = self._format_message(f"Error occurred: {error_type}", **kwargs)
|
563
|
+
|
564
|
+
# Add traceback if requested (useful for debugging)
|
565
|
+
exc_info = None
|
566
|
+
if include_traceback:
|
567
|
+
exc_info = sys.exc_info()
|
568
|
+
|
569
|
+
# Create a new record with the caller's info
|
570
|
+
record = self.logger.makeRecord(
|
571
|
+
self.logger.name,
|
572
|
+
logging.ERROR,
|
573
|
+
filename,
|
574
|
+
lineno,
|
575
|
+
error_msg,
|
576
|
+
(),
|
577
|
+
exc_info, # Include exception info if requested
|
578
|
+
)
|
579
|
+
self.logger.handle(record)
|
580
|
+
|
581
|
+
def log_transaction(self, action: str, success: bool = True, **kwargs):
|
582
|
+
"""Log transaction events"""
|
583
|
+
status = "✓ Transaction" if success else "✗ Transaction failed"
|
584
|
+
|
585
|
+
# Use findCaller to get the actual caller's file and line
|
586
|
+
import sys
|
587
|
+
|
588
|
+
frame = sys._getframe(1)
|
589
|
+
filename = frame.f_code.co_filename
|
590
|
+
lineno = frame.f_lineno
|
591
|
+
|
592
|
+
# Create a new record with the caller's info
|
593
|
+
record = self.logger.makeRecord(
|
594
|
+
self.logger.name,
|
595
|
+
logging.INFO,
|
596
|
+
filename,
|
597
|
+
lineno,
|
598
|
+
self._format_message(f"{status}: {action}", **kwargs),
|
599
|
+
(),
|
600
|
+
None,
|
601
|
+
)
|
602
|
+
self.logger.handle(record)
|
603
|
+
|
604
|
+
def log_account_operation(
|
605
|
+
self,
|
606
|
+
operation: str,
|
607
|
+
account_name: Optional[str] = None,
|
608
|
+
user_name: Optional[str] = None,
|
609
|
+
success: bool = True,
|
610
|
+
):
|
611
|
+
"""Log account management operations"""
|
612
|
+
status = "✓ Account operation" if success else "✗ Account operation failed"
|
613
|
+
kwargs = {"operation": operation}
|
614
|
+
if account_name:
|
615
|
+
kwargs["account"] = account_name
|
616
|
+
if user_name:
|
617
|
+
kwargs["user"] = user_name
|
618
|
+
|
619
|
+
# Use findCaller to get the actual caller's file and line
|
620
|
+
import sys
|
621
|
+
|
622
|
+
frame = sys._getframe(1)
|
623
|
+
filename = frame.f_code.co_filename
|
624
|
+
lineno = frame.f_lineno
|
625
|
+
|
626
|
+
# Create a new record with the caller's info
|
627
|
+
record = self.logger.makeRecord(
|
628
|
+
self.logger.name,
|
629
|
+
logging.INFO,
|
630
|
+
filename,
|
631
|
+
lineno,
|
632
|
+
self._format_message(f"{status}: {operation}", **kwargs),
|
633
|
+
(),
|
634
|
+
None,
|
635
|
+
)
|
636
|
+
self.logger.handle(record)
|
637
|
+
|
638
|
+
def log_snapshot_operation(
|
639
|
+
self,
|
640
|
+
operation: str,
|
641
|
+
snapshot_name: Optional[str] = None,
|
642
|
+
level: Optional[str] = None,
|
643
|
+
success: bool = True,
|
644
|
+
):
|
645
|
+
"""Log snapshot operations"""
|
646
|
+
status = "✓ Snapshot operation" if success else "✗ Snapshot operation failed"
|
647
|
+
kwargs = {"operation": operation}
|
648
|
+
if snapshot_name:
|
649
|
+
kwargs["snapshot"] = snapshot_name
|
650
|
+
if level:
|
651
|
+
kwargs["level"] = level
|
652
|
+
|
653
|
+
# Use findCaller to get the actual caller's file and line
|
654
|
+
import sys
|
655
|
+
|
656
|
+
frame = sys._getframe(1)
|
657
|
+
filename = frame.f_code.co_filename
|
658
|
+
lineno = frame.f_lineno
|
659
|
+
|
660
|
+
# Create a new record with the caller's info
|
661
|
+
record = self.logger.makeRecord(
|
662
|
+
self.logger.name,
|
663
|
+
logging.INFO,
|
664
|
+
filename,
|
665
|
+
lineno,
|
666
|
+
self._format_message(f"{status}: {operation}", **kwargs),
|
667
|
+
(),
|
668
|
+
None,
|
669
|
+
)
|
670
|
+
self.logger.handle(record)
|
671
|
+
|
672
|
+
def log_pubsub_operation(
|
673
|
+
self,
|
674
|
+
operation: str,
|
675
|
+
publication_name: Optional[str] = None,
|
676
|
+
subscription_name: Optional[str] = None,
|
677
|
+
success: bool = True,
|
678
|
+
):
|
679
|
+
"""Log PubSub operations"""
|
680
|
+
status = "✓ PubSub operation" if success else "✗ PubSub operation failed"
|
681
|
+
kwargs = {"operation": operation}
|
682
|
+
if publication_name:
|
683
|
+
kwargs["publication"] = publication_name
|
684
|
+
if subscription_name:
|
685
|
+
kwargs["subscription"] = subscription_name
|
686
|
+
|
687
|
+
# Use findCaller to get the actual caller's file and line
|
688
|
+
import sys
|
689
|
+
|
690
|
+
frame = sys._getframe(1)
|
691
|
+
filename = frame.f_code.co_filename
|
692
|
+
lineno = frame.f_lineno
|
693
|
+
|
694
|
+
# Create a new record with the caller's info
|
695
|
+
record = self.logger.makeRecord(
|
696
|
+
self.logger.name,
|
697
|
+
logging.INFO,
|
698
|
+
filename,
|
699
|
+
lineno,
|
700
|
+
self._format_message(f"{status}: {operation}", **kwargs),
|
701
|
+
(),
|
702
|
+
None,
|
703
|
+
)
|
704
|
+
self.logger.handle(record)
|
705
|
+
|
706
|
+
def set_level(self, level: int):
|
707
|
+
"""Set logging level"""
|
708
|
+
self.logger.setLevel(level)
|
709
|
+
for handler in self.logger.handlers:
|
710
|
+
handler.setLevel(level)
|
711
|
+
|
712
|
+
def add_handler(self, handler: logging.Handler):
|
713
|
+
"""Add custom handler to logger"""
|
714
|
+
self.logger.addHandler(handler)
|
715
|
+
|
716
|
+
def remove_handler(self, handler: logging.Handler):
|
717
|
+
"""Remove handler from logger"""
|
718
|
+
self.logger.removeHandler(handler)
|
719
|
+
|
720
|
+
def is_custom(self) -> bool:
|
721
|
+
"""Check if using custom logger"""
|
722
|
+
return self._is_custom
|
723
|
+
|
724
|
+
|
725
|
+
def create_default_logger(
|
726
|
+
level: int = logging.INFO,
|
727
|
+
format_string: Optional[str] = None,
|
728
|
+
sql_log_mode: str = "auto",
|
729
|
+
slow_query_threshold: float = 1.0,
|
730
|
+
max_sql_display_length: int = 500,
|
731
|
+
) -> MatrixOneLogger:
|
732
|
+
"""
|
733
|
+
Create a default MatrixOne logger
|
734
|
+
|
735
|
+
Args::
|
736
|
+
|
737
|
+
level: Logging level
|
738
|
+
format_string: Custom format string
|
739
|
+
sql_log_mode: SQL logging mode ('off', 'auto', 'simple', 'full')
|
740
|
+
slow_query_threshold: Threshold in seconds for slow query warnings
|
741
|
+
max_sql_display_length: Maximum SQL length in auto mode before summarizing
|
742
|
+
|
743
|
+
Returns::
|
744
|
+
|
745
|
+
MatrixOneLogger instance
|
746
|
+
"""
|
747
|
+
return MatrixOneLogger(
|
748
|
+
logger=None,
|
749
|
+
level=level,
|
750
|
+
format_string=format_string,
|
751
|
+
sql_log_mode=sql_log_mode,
|
752
|
+
slow_query_threshold=slow_query_threshold,
|
753
|
+
max_sql_display_length=max_sql_display_length,
|
754
|
+
)
|
755
|
+
|
756
|
+
|
757
|
+
def create_custom_logger(
|
758
|
+
logger: logging.Logger,
|
759
|
+
sql_log_mode: str = "auto",
|
760
|
+
slow_query_threshold: float = 1.0,
|
761
|
+
max_sql_display_length: int = 500,
|
762
|
+
) -> MatrixOneLogger:
|
763
|
+
"""
|
764
|
+
Create MatrixOne logger from custom logger
|
765
|
+
|
766
|
+
Args::
|
767
|
+
|
768
|
+
logger: Custom logger instance
|
769
|
+
sql_log_mode: SQL logging mode ('off', 'auto', 'simple', 'full')
|
770
|
+
slow_query_threshold: Threshold in seconds for slow query warnings
|
771
|
+
max_sql_display_length: Maximum SQL length in auto mode before summarizing
|
772
|
+
|
773
|
+
Returns::
|
774
|
+
|
775
|
+
MatrixOneLogger instance
|
776
|
+
"""
|
777
|
+
return MatrixOneLogger(
|
778
|
+
logger=logger,
|
779
|
+
sql_log_mode=sql_log_mode,
|
780
|
+
slow_query_threshold=slow_query_threshold,
|
781
|
+
max_sql_display_length=max_sql_display_length,
|
782
|
+
)
|