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

1""" 

2Logging system built on Python's logging module 

3 

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""" 

9 

10import logging 

11import os 

12import re 

13from pathlib import Path 

14 

15 

16class SensitiveDataFilter(logging.Filter): 

17 """ 

18 Filter that masks sensitive information. 

19 

20 Automatically detects and obfuscates sensitive values in log messages. 

21 

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 

26 

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 """ 

37 

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 ] 

46 

47 def filter(self, record: logging.LogRecord) -> bool: 

48 """ 

49 Mask sensitive data in the log record message. 

50 

51 Args: 

52 record: Log record to inspect. 

53 

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) 

60 

61 record.msg = message 

62 record.args = () # Clear args so getMessage() returns msg unchanged 

63 

64 return True 

65 

66 

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. 

74 

75 Supports simultaneous console and file output while masking sensitive data. 

76 

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. 

83 

84 Returns: 

85 Configured Logger object with console and file handlers. 

86 

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) 

92 

93 Example: 

94 >>> logger = setup_logger("my_app") 

95 >>> logger.info("Application started") 

96 >>> logger.debug("Detailed debug info") 

97 >>> logger.error("Error occurred") 

98 

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") 

105 

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) 

119 

120 logger = logging.getLogger(name) 

121 logger.setLevel(level) 

122 logger.handlers.clear() # Remove existing handlers to avoid duplicates 

123 

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) 

128 

129 formatter = logging.Formatter( 

130 fmt="[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s", 

131 datefmt="%Y-%m-%d %H:%M:%S", 

132 ) 

133 

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) 

139 

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) 

146 

147 return logger