Coverage for src / sql_tool / core / logging.py: 100%

13 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-02-14 15:28 -0500

1"""Logging configuration using structlog. 

2 

3Logs go to stderr to keep stdout clean for data output (piping). 

4""" 

5 

6import logging 

7import sys 

8from typing import Any 

9 

10import structlog 

11 

12_LOG_LEVELS: dict[str, int] = { 

13 "debug": logging.DEBUG, 

14 "info": logging.INFO, 

15 "warning": logging.WARNING, 

16 "error": logging.ERROR, 

17 "critical": logging.CRITICAL, 

18} 

19 

20 

21def setup_logging(verbose: bool = False) -> None: 

22 """Configure structlog for SQL Tool. 

23 

24 Args: 

25 verbose: If True, set log level to DEBUG. Otherwise INFO. 

26 """ 

27 log_level = "debug" if verbose else "info" 

28 

29 structlog.configure( 

30 processors=[ 

31 structlog.contextvars.merge_contextvars, 

32 structlog.processors.add_log_level, 

33 structlog.processors.TimeStamper(fmt="iso"), 

34 structlog.dev.ConsoleRenderer(colors=sys.stderr.isatty()), 

35 ], 

36 wrapper_class=structlog.make_filtering_bound_logger(_LOG_LEVELS[log_level]), 

37 context_class=dict, 

38 logger_factory=structlog.PrintLoggerFactory(file=sys.stderr), 

39 cache_logger_on_first_use=True, 

40 ) 

41 

42 

43def get_logger(name: str | None = None) -> Any: 

44 """Get a structlog logger, optionally bound with a name. 

45 

46 IMPORTANT: Never call this at module level. Always call inside 

47 functions or __init__() after setup_logging() has been called. 

48 """ 

49 logger = structlog.get_logger() 

50 if name: 

51 logger = logger.bind(logger=name) 

52 return logger