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
« 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.
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"""
12import os
13from typing import Any, Dict, Optional
14from loguru import logger
17# Check environment variable once at module load
18SETTINGS_LOG_LEVEL = os.getenv("LDR_LOG_SETTINGS", "none").lower()
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
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
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"
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.
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)
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
74 if log_level == "none":
75 return
77 if log_level == "summary":
78 # Log only summary at INFO level
79 summary = create_settings_summary(settings)
80 logger.info(f"{message}: {summary}")
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}")
91def redact_sensitive_keys(settings: Dict[str, Any]) -> Dict[str, Any]:
92 """
93 Redact sensitive keys from settings dictionary.
95 Args:
96 settings: Settings dictionary
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 ]
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 )
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
134 return redacted
137def create_settings_summary(settings: Any) -> str:
138 """
139 Create a summary of settings for logging.
141 Args:
142 settings: Settings object or dict
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)
153 return f"{total} total settings (search engines: {search_engines}, LLM: {llm_settings})"
154 return f"Settings object of type {type(settings).__name__}"
157def get_settings_log_level() -> str:
158 """
159 Get the current settings logging level.
161 Returns:
162 Current log level: "none", "summary", or "debug"
163 """
164 return SETTINGS_LOG_LEVEL