-
Notifications
You must be signed in to change notification settings - Fork 35
FEAT: mssql-python driver changes to support bulk copy logging. #430
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
6838a3a
5312733
3198670
a3a05cb
b805294
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -104,6 +104,10 @@ def __init__(self): | |||||||||||||||||
| self._handler_lock = threading.RLock() # Reentrant lock for handler operations | ||||||||||||||||||
| self._cleanup_registered = False # Track if atexit cleanup is registered | ||||||||||||||||||
|
|
||||||||||||||||||
| # Cached level for fast checks (avoid repeated isEnabledFor calls) | ||||||||||||||||||
| self._cached_level = logging.CRITICAL | ||||||||||||||||||
| self._is_debug_enabled = False | ||||||||||||||||||
|
|
||||||||||||||||||
| # Don't setup handlers yet - do it lazily when setLevel is called | ||||||||||||||||||
| # This prevents creating log files when user changes output mode before enabling logging | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
@@ -145,15 +149,20 @@ def _setup_handlers(self): | |||||||||||||||||
| # Custom formatter to extract source from message and format as CSV | ||||||||||||||||||
| class CSVFormatter(logging.Formatter): | ||||||||||||||||||
| def format(self, record): | ||||||||||||||||||
| # Extract source from message (e.g., [Python] or [DDBC]) | ||||||||||||||||||
| msg = record.getMessage() | ||||||||||||||||||
| if msg.startswith("[") and "]" in msg: | ||||||||||||||||||
| end_bracket = msg.index("]") | ||||||||||||||||||
| source = msg[1:end_bracket] | ||||||||||||||||||
| message = msg[end_bracket + 2 :].strip() # Skip '] ' | ||||||||||||||||||
| # Check if this is from py-core (via py_core_log method) | ||||||||||||||||||
| if hasattr(record, "funcName") and record.funcName == "py-core": | ||||||||||||||||||
| source = "py-core" | ||||||||||||||||||
| message = record.getMessage() | ||||||||||||||||||
| else: | ||||||||||||||||||
| source = "Unknown" | ||||||||||||||||||
| message = msg | ||||||||||||||||||
| # Extract source from message (e.g., [Python] or [DDBC]) | ||||||||||||||||||
| msg = record.getMessage() | ||||||||||||||||||
| if msg.startswith("[") and "]" in msg: | ||||||||||||||||||
| end_bracket = msg.index("]") | ||||||||||||||||||
| source = msg[1:end_bracket] | ||||||||||||||||||
| message = msg[end_bracket + 2 :].strip() # Skip '] ' | ||||||||||||||||||
| else: | ||||||||||||||||||
| source = "Unknown" | ||||||||||||||||||
| message = msg | ||||||||||||||||||
|
|
||||||||||||||||||
| # Format timestamp with milliseconds using period separator | ||||||||||||||||||
| timestamp = self.formatTime(record, "%Y-%m-%d %H:%M:%S") | ||||||||||||||||||
|
|
@@ -326,6 +335,42 @@ def _write_log_header(self): | |||||||||||||||||
| pass # Even stderr notification failed | ||||||||||||||||||
| # Don't crash - logging continues without header | ||||||||||||||||||
|
|
||||||||||||||||||
| def py_core_log(self, level: int, msg: str, filename: str = "cursor.rs", lineno: int = 0): | ||||||||||||||||||
| """ | ||||||||||||||||||
| Logging method for py-core (Rust/TDS) code with custom source location. | ||||||||||||||||||
|
|
||||||||||||||||||
| Args: | ||||||||||||||||||
| level: Log level (DEBUG, INFO, WARNING, ERROR) | ||||||||||||||||||
| msg: Message string (already formatted) | ||||||||||||||||||
| filename: Source filename (e.g., 'cursor.rs') | ||||||||||||||||||
| lineno: Line number in source file | ||||||||||||||||||
| """ | ||||||||||||||||||
| try: | ||||||||||||||||||
| if not self._logger.isEnabledFor(level): | ||||||||||||||||||
| return | ||||||||||||||||||
|
|
||||||||||||||||||
| # Create a custom LogRecord with Rust source location | ||||||||||||||||||
| import logging as log_module | ||||||||||||||||||
|
|
||||||||||||||||||
| record = log_module.LogRecord( | ||||||||||||||||||
| name=self._logger.name, | ||||||||||||||||||
| level=level, | ||||||||||||||||||
| pathname=filename, | ||||||||||||||||||
| lineno=lineno, | ||||||||||||||||||
| msg=msg, | ||||||||||||||||||
| args=(), | ||||||||||||||||||
| exc_info=None, | ||||||||||||||||||
| func="py-core", | ||||||||||||||||||
| sinfo=None, | ||||||||||||||||||
| ) | ||||||||||||||||||
| self._logger.handle(record) | ||||||||||||||||||
| except Exception: | ||||||||||||||||||
| # Fallback - use regular logging | ||||||||||||||||||
| try: | ||||||||||||||||||
| self._logger.log(level, msg) | ||||||||||||||||||
| except: | ||||||||||||||||||
| pass | ||||||||||||||||||
|
|
||||||||||||||||||
| def _log(self, level: int, msg: str, add_prefix: bool = True, *args, **kwargs): | ||||||||||||||||||
| """ | ||||||||||||||||||
| Internal logging method with exception safety. | ||||||||||||||||||
|
|
@@ -352,8 +397,9 @@ def _log(self, level: int, msg: str, add_prefix: bool = True, *args, **kwargs): | |||||||||||||||||
| All other failures are silently ignored to prevent app crashes. | ||||||||||||||||||
| """ | ||||||||||||||||||
| try: | ||||||||||||||||||
| # Fast level check (zero overhead if disabled) | ||||||||||||||||||
| if not self._logger.isEnabledFor(level): | ||||||||||||||||||
| # Fast level check using cached level (zero overhead if disabled) | ||||||||||||||||||
| # This avoids the overhead of isEnabledFor() method call | ||||||||||||||||||
| if level < self._cached_level: | ||||||||||||||||||
| return | ||||||||||||||||||
|
|
||||||||||||||||||
| # Add prefix if requested (only after level check) | ||||||||||||||||||
|
|
@@ -364,8 +410,9 @@ def _log(self, level: int, msg: str, add_prefix: bool = True, *args, **kwargs): | |||||||||||||||||
| if args: | ||||||||||||||||||
| msg = msg % args | ||||||||||||||||||
|
|
||||||||||||||||||
| # Log the message (no args since already formatted) | ||||||||||||||||||
| self._logger.log(level, msg, **kwargs) | ||||||||||||||||||
| # Log the message with proper stack level to capture caller's location | ||||||||||||||||||
| # stacklevel=3 skips: _log -> debug/info/warning/error -> actual caller | ||||||||||||||||||
| self._logger.log(level, msg, stacklevel=3, **kwargs) | ||||||||||||||||||
| except Exception: | ||||||||||||||||||
| # Last resort: Try stderr fallback for any logging failure | ||||||||||||||||||
| # This helps diagnose critical issues (disk full, permission denied, etc.) | ||||||||||||||||||
|
|
@@ -441,6 +488,10 @@ def _setLevel( | |||||||||||||||||
| # Set level (atomic operation, no lock needed) | ||||||||||||||||||
| self._logger.setLevel(level) | ||||||||||||||||||
|
|
||||||||||||||||||
| # Cache level for fast checks (avoid repeated isEnabledFor calls) | ||||||||||||||||||
|
||||||||||||||||||
| # Cache level for fast checks (avoid repeated isEnabledFor calls) | |
| # Cache level for fast checks (avoid repeated isEnabledFor calls). | |
| # NOTE: These two assignments are not synchronized together. This can cause | |
| # a brief window where another thread may observe an inconsistent state | |
| # (e.g., _cached_level == DEBUG but _is_debug_enabled is still False). | |
| # This is considered acceptable because the underlying logger remains the | |
| # single source of truth for effective log level checks, and any | |
| # inconsistency only affects logging decisions transiently. |
Copilot
AI
Feb 16, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new is_debug_enabled property and py_core_log method lack test coverage. Given that tests/test_007_logging.py has comprehensive logging tests, these new features should have corresponding tests to verify: 1) is_debug_enabled correctly reflects the logging level state, 2) is_debug_enabled updates when setLevel is called, 3) py_core_log correctly formats records with custom source locations, 4) py_core_log falls back gracefully on errors.
| """Fast check if debug logging is enabled (cached for performance)""" | |
| return self._is_debug_enabled | |
| """Fast check if debug logging is enabled (cached for performance) | |
| This uses getattr with a default to avoid AttributeError if the | |
| internal cache has not yet been initialized. | |
| """ | |
| return getattr(self, "_is_debug_enabled", False) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The py_core_log method uses self._logger.isEnabledFor(level) instead of the cached level check. For consistency with the performance optimization introduced in _log method and to avoid redundant isEnabledFor calls, this should use the cached level check: "if level < self._cached_level: return". This would maintain the same performance benefit that was the goal of introducing the cached level.