Coverage for src / local_deep_research / settings / logger.py: 94%

54 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-14 23:55 +0000

1""" 

2Centralized utility for logging settings and configuration. 

3Controls when and how settings are logged based on environment variables. 

4 

5Environment variable LDR_LOG_SETTINGS controls the verbosity: 

6- "none" or "false": No settings logging at all (default) 

7- "summary" or "info": Only log count and summary of settings 

8- "debug" or "full": Log complete settings (with sensitive keys redacted) 

9- "debug_unsafe"/"unsafe"/"raw": REMOVED for security — maps to "none" with a deprecation warning 

10""" 

11 

12import os 

13from typing import Any, Dict, Optional 

14from loguru import logger 

15 

16 

17# Check environment variable once at module load 

18SETTINGS_LOG_LEVEL = os.getenv("LDR_LOG_SETTINGS", "none").lower() 

19 

20# Map various values to standardized levels 

21if SETTINGS_LOG_LEVEL in ("false", "0", "no", "none", "off"): 

22 SETTINGS_LOG_LEVEL = "none" 

23elif SETTINGS_LOG_LEVEL in ("true", "1", "yes", "info", "summary"): 

24 SETTINGS_LOG_LEVEL = "summary" 

25elif SETTINGS_LOG_LEVEL in ("debug", "full", "all"): 

26 SETTINGS_LOG_LEVEL = "debug" 

27elif SETTINGS_LOG_LEVEL in ("debug_unsafe", "unsafe", "raw"): 

28 import warnings 

29 

30 warnings.warn( 

31 f"LDR_LOG_SETTINGS={os.getenv('LDR_LOG_SETTINGS')!r} is deprecated and has been " 

32 "removed for security. Use 'debug' for full settings with sensitive keys redacted. " 

33 "Defaulting to 'none'.", 

34 DeprecationWarning, 

35 stacklevel=2, 

36 ) 

37 # logger.warning() won't work here — loguru is disabled at module load time 

38 # (see __init__.py). Write directly to stderr so users actually see this. 

39 import sys 

40 

41 print( 

42 f"WARNING: LDR_LOG_SETTINGS={os.getenv('LDR_LOG_SETTINGS')!r} is deprecated and " 

43 "has been removed for security. Use 'debug' for full settings with sensitive keys " 

44 "redacted. Defaulting to 'none'.", 

45 file=sys.stderr, 

46 ) 

47 SETTINGS_LOG_LEVEL = "none" 

48else: 

49 # Invalid value, default to none 

50 SETTINGS_LOG_LEVEL = "none" 

51 

52 

53def log_settings( 

54 settings: Any, 

55 message: str = "Settings loaded", 

56 force_level: Optional[str] = None, 

57) -> None: 

58 """ 

59 Centralized settings logging with conditional output based on LDR_LOG_SETTINGS env var. 

60 

61 Args: 

62 settings: Settings object or dict to log 

63 message: Log message prefix 

64 force_level: Override the environment variable setting (for critical messages) 

65 

66 Behavior based on LDR_LOG_SETTINGS: 

67 - "none": No output 

68 - "summary": Log count and basic info at INFO level 

69 - "debug": Log full settings at DEBUG level (sensitive keys redacted) 

70 - "debug_unsafe"/"unsafe"/"raw": REMOVED — maps to "none" with a deprecation warning 

71 """ 

72 log_level = force_level or SETTINGS_LOG_LEVEL 

73 

74 if log_level == "none": 

75 return 

76 

77 if log_level == "summary": 

78 # Log only summary at INFO level 

79 summary = create_settings_summary(settings) 

80 logger.info(f"{message}: {summary}") 

81 

82 elif log_level == "debug": 82 ↛ exitline 82 didn't return from function 'log_settings' because the condition on line 82 was always true

83 # Log full settings at DEBUG level with redaction 

84 if isinstance(settings, dict): 84 ↛ 88line 84 didn't jump to line 88 because the condition on line 84 was always true

85 safe_settings = redact_sensitive_keys(settings) 

86 logger.debug(f"{message} (redacted): {safe_settings}") 

87 else: 

88 logger.debug(f"{message}: {settings}") 

89 

90 

91def redact_sensitive_keys(settings: Dict[str, Any]) -> Dict[str, Any]: 

92 """ 

93 Redact sensitive keys from settings dictionary. 

94 

95 Args: 

96 settings: Settings dictionary 

97 

98 Returns: 

99 Settings dictionary with sensitive values redacted 

100 """ 

101 sensitive_patterns = [ 

102 "api_key", 

103 "apikey", 

104 "password", 

105 "secret", 

106 "token", 

107 "credential", 

108 "auth", 

109 "private", 

110 ] 

111 

112 redacted = {} 

113 for key, value in settings.items(): 

114 # Check if key contains sensitive patterns 

115 key_lower = key.lower() 

116 is_sensitive = any( 

117 pattern in key_lower for pattern in sensitive_patterns 

118 ) 

119 

120 if is_sensitive: 

121 # Redact the value 

122 if isinstance(value, dict) and "value" in value: 

123 redacted[key] = {**value, "value": "***REDACTED***"} 

124 elif isinstance(value, str): 124 ↛ 127line 124 didn't jump to line 127 because the condition on line 124 was always true

125 redacted[key] = "***REDACTED***" 

126 else: 

127 redacted[key] = "***REDACTED***" 

128 elif isinstance(value, dict): 

129 # Recursively redact nested dicts 

130 redacted[key] = redact_sensitive_keys(value) 

131 else: 

132 redacted[key] = value 

133 

134 return redacted 

135 

136 

137def create_settings_summary(settings: Any) -> str: 

138 """ 

139 Create a summary of settings for logging. 

140 

141 Args: 

142 settings: Settings object or dict 

143 

144 Returns: 

145 Summary string 

146 """ 

147 if isinstance(settings, dict): 

148 # Count different types of settings 

149 search_engines = sum(1 for k in settings.keys() if "search.engine" in k) 

150 llm_settings = sum(1 for k in settings.keys() if "llm." in k) 

151 total = len(settings) 

152 

153 return f"{total} total settings (search engines: {search_engines}, LLM: {llm_settings})" 

154 return f"Settings object of type {type(settings).__name__}" 

155 

156 

157def get_settings_log_level() -> str: 

158 """ 

159 Get the current settings logging level. 

160 

161 Returns: 

162 Current log level: "none", "summary", or "debug" 

163 """ 

164 return SETTINGS_LOG_LEVEL