Coverage for src / moai_adk / utils / logger.py: 21.05%
38 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-20 20:52 +0900
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-20 20:52 +0900
1"""
2Logging system built on Python's logging module
4SPEC requirements:
5- Store logs at .moai/logs/moai.log
6- Mask sensitive data: API Key, Email, Password
7- Log levels: development (DEBUG), test (INFO), production (WARNING)
8"""
10import logging
11import os
12import re
13from pathlib import Path
16class SensitiveDataFilter(logging.Filter):
17 """
18 Filter that masks sensitive information.
20 Automatically detects and obfuscates sensitive values in log messages.
22 Supported patterns:
23 - API Key: strings that start with sk-
24 - Email: standard email address format
25 - Password: values following password/passwd/pwd keywords
27 Example:
28 >>> filter_instance = SensitiveDataFilter()
29 >>> record = logging.LogRecord(
30 ... name="app", level=logging.INFO, pathname="", lineno=0,
31 ... msg="API Key: sk-secret123", args=(), exc_info=None
32 ... )
33 >>> filter_instance.filter(record)
34 >>> print(record.msg)
35 API Key: ***REDACTED***
36 """
38 PATTERNS = [
39 (r"sk-[a-zA-Z0-9]+", "***REDACTED***"), # API Key
40 (
41 r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b",
42 "***REDACTED***",
43 ), # Email
44 (r"(?i)(password|passwd|pwd)[\s:=]+\S+", r"\1: ***REDACTED***"), # Password
45 ]
47 def filter(self, record: logging.LogRecord) -> bool:
48 """
49 Mask sensitive data in the log record message.
51 Args:
52 record: Log record to inspect.
54 Returns:
55 True to keep the record.
56 """
57 message = record.getMessage()
58 for pattern, replacement in self.PATTERNS:
59 message = re.sub(pattern, replacement, message)
61 record.msg = message
62 record.args = () # Clear args so getMessage() returns msg unchanged
64 return True
67def setup_logger(
68 name: str,
69 log_dir: str | None = None,
70 level: int | None = None,
71) -> logging.Logger:
72 """
73 Configure and return a logger instance.
75 Supports simultaneous console and file output while masking sensitive data.
77 Args:
78 name: Logger name (module or application).
79 log_dir: Directory where logs are written.
80 Default: .moai/logs (created automatically).
81 level: Logging level (logging.DEBUG, INFO, WARNING, etc.).
82 Default: derived from the MOAI_ENV environment variable.
84 Returns:
85 Configured Logger object with console and file handlers.
87 Log level per environment (MOAI_ENV):
88 - development: DEBUG (emit all logs)
89 - test: INFO (informational and above)
90 - production: WARNING (warnings and above)
91 - default: INFO (when the environment variable is unset)
93 Example:
94 >>> logger = setup_logger("my_app")
95 >>> logger.info("Application started")
96 >>> logger.debug("Detailed debug info")
97 >>> logger.error("Error occurred")
99 # Production environment (only WARNING and above)
100 >>> import os
101 >>> os.environ["MOAI_ENV"] = "production"
102 >>> prod_logger = setup_logger("prod_app")
103 >>> prod_logger.warning("This will be logged")
104 >>> prod_logger.info("This will NOT be logged")
106 Notes:
107 - Log files are written using UTF-8 encoding.
108 - Sensitive data (API Key, Email, Password) is automatically masked.
109 - Existing handlers are removed to prevent duplicates.
110 """
111 if level is None:
112 env = os.getenv("MOAI_ENV", "").lower()
113 level_map = {
114 "development": logging.DEBUG,
115 "test": logging.INFO,
116 "production": logging.WARNING,
117 }
118 level = level_map.get(env, logging.INFO)
120 logger = logging.getLogger(name)
121 logger.setLevel(level)
122 logger.handlers.clear() # Remove existing handlers to avoid duplicates
124 if log_dir is None:
125 log_dir = ".moai/logs"
126 log_path = Path(log_dir)
127 log_path.mkdir(parents=True, exist_ok=True)
129 formatter = logging.Formatter(
130 fmt="[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s",
131 datefmt="%Y-%m-%d %H:%M:%S",
132 )
134 console_handler = logging.StreamHandler()
135 console_handler.setLevel(level)
136 console_handler.setFormatter(formatter)
137 console_handler.addFilter(SensitiveDataFilter())
138 logger.addHandler(console_handler)
140 log_file = log_path / "moai.log"
141 file_handler = logging.FileHandler(log_file, encoding="utf-8")
142 file_handler.setLevel(level)
143 file_handler.setFormatter(formatter)
144 file_handler.addFilter(SensitiveDataFilter())
145 logger.addHandler(file_handler)
147 return logger