Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
57cfc13
REFACTOR: Enhance & Fix Logs
bewithgaurav Jul 14, 2025
f95309e
tests and minor fix
bewithgaurav Jul 14, 2025
98e2234
conflicts
bewithgaurav Jul 16, 2025
9534896
restoring branch
bewithgaurav Jul 16, 2025
0ed415f
restoring ddbc bindings for branch:
bewithgaurav Jul 16, 2025
7ba4115
restoring logging_config
bewithgaurav Jul 16, 2025
0562f8a
restoring logging_other files
bewithgaurav Jul 16, 2025
1b67d51
eliminated auth changes
bewithgaurav Jul 16, 2025
558cdd6
logging tests only
bewithgaurav Jul 16, 2025
3266fe6
refactor
bewithgaurav Jul 16, 2025
d116a73
restored resource cleanup changes
bewithgaurav Jul 16, 2025
8baaff3
fixed tests
bewithgaurav Jul 16, 2025
fb1289f
escape connection string properly
bewithgaurav Jul 16, 2025
388a3c3
restore connection tests
bewithgaurav Jul 16, 2025
56634ce
minor changes
bewithgaurav Jul 16, 2025
6e00afb
stopped logging during destruction since that cause GIL issues
bewithgaurav Jul 16, 2025
9642fd5
logger default status fixed
bewithgaurav Jul 16, 2025
1e74bf0
Merge branch 'main' into bewithgaurav/enhance_logging
bewithgaurav Jul 17, 2025
6acd665
refactored
bewithgaurav Jul 17, 2025
03e3b5f
Merge branch 'bewithgaurav/enhance_logging' of https://github.com/mic…
bewithgaurav Jul 17, 2025
97b9131
cleanup
bewithgaurav Jul 17, 2025
d85fecc
main cleanup
bewithgaurav Jul 17, 2025
5706644
fixes
bewithgaurav Jul 17, 2025
d58c71c
merge conflicts
bewithgaurav Jul 17, 2025
596db8b
Merge branch 'main' into bewithgaurav/enhance_logging
jahnvi480 Jul 17, 2025
094f514
Merge branch 'main' into bewithgaurav/enhance_logging
bewithgaurav Jul 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 17 additions & 11 deletions mssql_python/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,21 @@
- Cursors are also cleaned up automatically when no longer referenced, to prevent memory leaks.
"""
import weakref
import logging
from mssql_python.cursor import Cursor
from mssql_python.logging_config import get_logger, ENABLE_LOGGING
from mssql_python.logging_config import get_logger, LoggingManager, setup_logging
from mssql_python.constants import ConstantsDDBC as ddbc_sql_const
from mssql_python.helpers import add_driver_to_connection_str, check_error
from mssql_python.helpers import add_driver_to_connection_str, check_error, sanitize_connection_string
from mssql_python import ddbc_bindings
from mssql_python.pooling import PoolingManager
from mssql_python.exceptions import DatabaseError, InterfaceError

# Ensure we're getting the most up-to-date logger
logger = get_logger()
# If no logger is available yet, set up a default one
if logger is None:
setup_logging(log_level=logging.INFO)
logger = get_logger()


class Connection:
Expand Down Expand Up @@ -113,8 +119,8 @@ def _construct_connection_string(self, connection_str: str = "", **kwargs) -> st
continue
conn_str += f"{key}={value};"

if ENABLE_LOGGING:
logger.info("Final connection string: %s", conn_str)
if logger:
logger.info("Final connection string: %s", sanitize_connection_string(conn_str))

return conn_str

Expand All @@ -137,7 +143,7 @@ def autocommit(self, value: bool) -> None:
None
"""
self.setautocommit(value)
if ENABLE_LOGGING:
if logger:
logger.info("Autocommit mode set to %s.", value)

def setautocommit(self, value: bool = True) -> None:
Expand Down Expand Up @@ -193,7 +199,7 @@ def commit(self) -> None:
"""
# Commit the current transaction
self._conn.commit()
if ENABLE_LOGGING:
if logger:
logger.info("Transaction committed successfully.")

def rollback(self) -> None:
Expand All @@ -209,7 +215,7 @@ def rollback(self) -> None:
"""
# Roll back the current transaction
self._conn.rollback()
if ENABLE_LOGGING:
if logger:
logger.info("Transaction rolled back successfully.")

def close(self) -> None:
Expand Down Expand Up @@ -242,11 +248,11 @@ def close(self) -> None:
except Exception as e:
# Collect errors but continue closing other cursors
close_errors.append(f"Error closing cursor: {e}")
if ENABLE_LOGGING:
if logger:
logger.warning(f"Error closing cursor: {e}")

# If there were errors closing cursors, log them but continue
if close_errors and ENABLE_LOGGING:
if close_errors and logger:
logger.warning(f"Encountered {len(close_errors)} errors while closing cursors")

# Clear the cursor set explicitly to release any internal references
Expand All @@ -258,13 +264,13 @@ def close(self) -> None:
self._conn.close()
self._conn = None
except Exception as e:
if ENABLE_LOGGING:
if logger:
logger.error(f"Error closing database connection: {e}")
# Re-raise the connection close error as it's more critical
raise
finally:
# Always mark as closed, even if there were errors
self._closed = True

if ENABLE_LOGGING:
if logger:
logger.info("Connection closed successfully.")
10 changes: 5 additions & 5 deletions mssql_python/cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from typing import List, Union
from mssql_python.constants import ConstantsDDBC as ddbc_sql_const
from mssql_python.helpers import check_error
from mssql_python.logging_config import get_logger, ENABLE_LOGGING
from mssql_python.logging_config import get_logger, LoggingManager
from mssql_python import ddbc_bindings
from .row import Row

Expand Down Expand Up @@ -431,7 +431,7 @@ def _reset_cursor(self) -> None:
if self.hstmt:
self.hstmt.free()
self.hstmt = None
if ENABLE_LOGGING:
if logger:
logger.debug("SQLFreeHandle succeeded")
# Reinitialize the statement handle
self._initialize_cursor()
Expand All @@ -449,7 +449,7 @@ def close(self) -> None:
if self.hstmt:
self.hstmt.free()
self.hstmt = None
if ENABLE_LOGGING:
if logger:
logger.debug("SQLFreeHandle succeeded")
self.closed = True

Expand Down Expand Up @@ -584,7 +584,7 @@ def execute(
# Executing a new statement. Reset is_stmt_prepared to false
self.is_stmt_prepared = [False]

if ENABLE_LOGGING:
if logger:
logger.debug("Executing query: %s", operation)
for i, param in enumerate(parameters):
logger.debug(
Expand Down Expand Up @@ -637,7 +637,7 @@ def executemany(self, operation: str, seq_of_parameters: list) -> None:
total_rowcount = 0
for parameters in seq_of_parameters:
parameters = list(parameters)
if ENABLE_LOGGING:
if logger:
logger.info("Executing query with parameters: %s", parameters)
prepare_stmt = first_execution
first_execution = False
Expand Down
6 changes: 3 additions & 3 deletions mssql_python/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
This module contains custom exception classes for the mssql_python package.
These classes are used to raise exceptions when an error occurs while executing a query.
"""
from mssql_python.logging_config import get_logger, ENABLE_LOGGING
from mssql_python.logging_config import get_logger, LoggingManager

logger = get_logger()

Expand Down Expand Up @@ -621,7 +621,7 @@ def truncate_error_message(error_message: str) -> str:
string_third = string_second[string_second.index("]") + 1 :]
return string_first + string_third
except Exception as e:
if ENABLE_LOGGING:
if logger:
logger.error("Error while truncating error message: %s",e)
return error_message

Expand All @@ -641,7 +641,7 @@ def raise_exception(sqlstate: str, ddbc_error: str) -> None:
"""
exception_class = sqlstate_to_exception(sqlstate, ddbc_error)
if exception_class:
if ENABLE_LOGGING:
if logger:
logger.error(exception_class)
raise exception_class
raise DatabaseError(
Expand Down
20 changes: 18 additions & 2 deletions mssql_python/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from mssql_python import ddbc_bindings
from mssql_python.exceptions import raise_exception
from mssql_python.logging_config import get_logger, ENABLE_LOGGING
from mssql_python.logging_config import get_logger
import platform
from pathlib import Path
from mssql_python.ddbc_bindings import normalize_architecture
Expand Down Expand Up @@ -73,7 +73,7 @@ def check_error(handle_type, handle, ret):
"""
if ret < 0:
error_info = ddbc_bindings.DDBCSQLCheckError(handle_type, handle, ret)
if ENABLE_LOGGING:
if logger:
logger.error("Error: %s", error_info.ddbcErrorMsg)
raise_exception(error_info.sqlState, error_info.ddbcErrorMsg)

Expand Down Expand Up @@ -184,3 +184,19 @@ def get_driver_path(module_dir, architecture):
raise RuntimeError(f"ODBC driver not found at: {driver_path_str}")

return driver_path_str


def sanitize_connection_string(conn_str: str) -> str:
"""
Sanitize the connection string by removing sensitive information.

Args:
conn_str (str): The connection string to sanitize.

Returns:
str: The sanitized connection string.
"""
# Remove sensitive information from the connection string, Pwd section
# Replace Pwd=...; or Pwd=... (end of string) with Pwd=***;
import re
return re.sub(r"(Pwd\s*=\s*)[^;]*", r"\1***", conn_str, flags=re.IGNORECASE)
157 changes: 120 additions & 37 deletions mssql_python/logging_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,58 +8,141 @@
from logging.handlers import RotatingFileHandler
import os
import sys
import datetime

ENABLE_LOGGING = False

class LoggingManager:
"""
Singleton class to manage logging configuration for the mssql_python package.
This class provides a centralized way to manage logging configuration and replaces
the previous approach using global variables.
"""
_instance = None
_initialized = False
_logger = None
_log_file = None

def __new__(cls):
if cls._instance is None:
cls._instance = super(LoggingManager, cls).__new__(cls)
return cls._instance

def __init__(self):
if not self._initialized:
self._initialized = True
self._enabled = False

@classmethod
def is_logging_enabled(cls):
"""Class method to check if logging is enabled for backward compatibility"""
if cls._instance is None:
return False
return cls._instance._enabled

@property
def enabled(self):
"""Check if logging is enabled"""
return self._enabled

@property
def log_file(self):
"""Get the current log file path"""
return self._log_file

def setup(self, mode="file", log_level=logging.DEBUG):
"""
Set up logging configuration.

This method configures the logging settings for the application.
It sets the log level, format, and log file location.

Args:
mode (str): The logging mode ('file' or 'stdout').
log_level (int): The logging level (default: logging.DEBUG).
"""
# Enable logging
self._enabled = True

# Create a logger for mssql_python module
# Use a consistent logger name to ensure we're using the same logger throughout
self._logger = logging.getLogger("mssql_python")
self._logger.setLevel(log_level)

# Configure the root logger to ensure all messages are captured
root_logger = logging.getLogger()
root_logger.setLevel(log_level)

# Make sure the logger propagates to the root logger
self._logger.propagate = True

# Clear any existing handlers to avoid duplicates during re-initialization
if self._logger.handlers:
self._logger.handlers.clear()

# Construct the path to the log file
# Directory for log files - currentdir/logs
current_dir = os.path.dirname(os.path.abspath(__file__))
log_dir = os.path.join(current_dir, 'logs')
# exist_ok=True allows the directory to be created if it doesn't exist
os.makedirs(log_dir, exist_ok=True)

# Generate timestamp-based filename for better sorting and organization
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
self._log_file = os.path.join(log_dir, f'mssql_python_trace_{timestamp}_{os.getpid()}.log')

# Create a log handler to log to driver specific file
# By default we only want to log to a file, max size 500MB, and keep 5 backups
file_handler = RotatingFileHandler(self._log_file, maxBytes=512*1024*1024, backupCount=5)
file_handler.setLevel(log_level)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(filename)s - %(message)s')
file_handler.setFormatter(formatter)
self._logger.addHandler(file_handler)

if mode == 'stdout':
# If the mode is stdout, then we want to log to the console as well
stdout_handler = logging.StreamHandler(sys.stdout)
stdout_handler.setLevel(log_level)
stdout_handler.setFormatter(formatter)
self._logger.addHandler(stdout_handler)
elif mode != 'file':
raise ValueError(f'Invalid logging mode: {mode}')

return self._logger

def get_logger(self):
"""
Get the logger instance.

Returns:
logging.Logger: The logger instance, or None if logging is not enabled.
"""
if not self.enabled:
return None
return self._logger


# Create a singleton instance
_manager = LoggingManager()

def setup_logging(mode="file", log_level=logging.DEBUG):
"""
Set up logging configuration.

This method configures the logging settings for the application.
It sets the log level, format, and log file location.


This is a wrapper around the LoggingManager.setup method for backward compatibility.

Args:
mode (str): The logging mode ('file' or 'stdout').
log_level (int): The logging level (default: logging.DEBUG).
"""
global ENABLE_LOGGING
ENABLE_LOGGING = True

# Create a logger for mssql_python module
logger = logging.getLogger(__name__)
logger.setLevel(log_level)

# Construct the path to the log file
# TODO: Use a different dir to dump log file
current_dir = os.path.dirname(os.path.abspath(__file__))
log_file = os.path.join(current_dir, f'mssql_python_trace_{os.getpid()}.log')

# Create a log handler to log to driver specific file
# By default we only want to log to a file, max size 500MB, and keep 5 backups
# TODO: Rotate files based on time too? Ex: everyday
file_handler = RotatingFileHandler(log_file, maxBytes=512*1024*1024, backupCount=5)
file_handler.setLevel(log_level)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(filename)s - %(message)s')
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)

if mode == 'stdout':
# If the mode is stdout, then we want to log to the console as well
stdout_handler = logging.StreamHandler(sys.stdout)
stdout_handler.setLevel(log_level)
stdout_handler.setFormatter(formatter)
logger.addHandler(stdout_handler)
elif mode != 'file':
raise ValueError(f'Invalid logging mode: {mode}')
return _manager.setup(mode, log_level)

def get_logger():
"""
Get the logger instance.

This is a wrapper around the LoggingManager.get_logger method for backward compatibility.

Returns:
logging.Logger: The logger instance.
"""
if not ENABLE_LOGGING:
return None
return logging.getLogger(__name__)
return _manager.get_logger()
Loading
Loading