Coverage for functions \ flipdare \ app_log.py: 100%

0 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2026-05-08 12:22 +1000

1#!/usr/bin/env python 

2# Copyright (c) 2026 Flipdare Pty Ltd. All rights reserved. 

3# 

4# This file is part of Flipdare's proprietary software and contains 

5# confidential and copyrighted material. Unauthorised copying, 

6# modification, distribution, or use of this file is strictly 

7# prohibited without prior written permission from Flipdare Pty Ltd. 

8# 

9# This software includes third-party components licensed under MIT, 

10# BSD, and Apache 2.0 licences. See THIRD_PARTY_NOTICES for details. 

11# 

12 

13# pragma: no cover 

14from __future__ import annotations 

15import logging 

16from typing import Self, cast, Any 

17 

18from flipdare.generated.shared.backend.system_log_type import SystemLogType 

19from flipdare.util.app_log_formatter import PERFORMANCE_LEVEL, TRACE_LEVEL 

20 

21__all__ = ["SystemLogType", "LOG", "APP_LOG_NAME"] 

22 

23_NOISY_LOGGERS: list[str] = [ 

24 "faker", 

25 "requests", 

26 "urllib3", 

27 "matplotlib", 

28 "httpcore", 

29 "httpx", 

30 "asyncio", 

31] 

32 

33APP_LOG_NAME = "flipdare" 

34 

35 

36# RELEASE: temporarily set to permanent debug while finishing. 

37_DEFAULT_PROD_LEVEL = 5 # <------ THIS NEEDS TO BE CHANGED TO 20 WHEN RELEASING 

38_DEFAULT_TEST_LEVEL = 10 

39 

40 

41def LOG() -> AppLog: # pragma: no cover 

42 return AppLog.instance() 

43 

44 

45# NOTE: we avoid singleton pattern here to prevent circular imports 

46class AppLog: # pragma: no cover 

47 _instance: AppLog | None = None 

48 

49 __slots__ = [ 

50 "_initialized", 

51 "_is_prod", 

52 "_is_trace", 

53 "_logger", 

54 "_stacklevel", 

55 ] 

56 

57 _initialized: bool 

58 _stacklevel: int 

59 _logger: logging.Logger | None 

60 _is_prod: bool 

61 _is_trace: bool 

62 

63 def __new__(cls, stacklevel: int = 2, is_trace: bool = False) -> Self: 

64 if cls._instance is not None: 

65 return cast("Self", cls._instance) 

66 

67 instance = super().__new__(cls) 

68 # set immediately to prevent possible recursion issues. 

69 cls._instance = instance 

70 

71 instance._initialized = True 

72 instance._stacklevel = stacklevel 

73 instance._logger = None 

74 instance._is_prod = True 

75 instance._is_trace = is_trace 

76 

77 instance._setup_logging() 

78 return cls._instance 

79 

80 @classmethod 

81 def instance(cls) -> AppLog: 

82 # Now this just calls the constructor, which handles the logic 

83 return cls() 

84 

85 @property 

86 def is_trace(self) -> bool: 

87 return self.log.isEnabledFor(TRACE_LEVEL) 

88 

89 @property 

90 def is_debug(self) -> bool: 

91 return self.log.isEnabledFor(10) # DEBUG level 

92 

93 @property 

94 def is_info(self) -> bool: 

95 return self.log.isEnabledFor(20) # INFO level 

96 

97 @property 

98 def stacklevel(self) -> int: 

99 return self._stacklevel 

100 

101 @property 

102 def log(self) -> logging.Logger: 

103 if self._logger is None: 

104 self._setup_logging() 

105 

106 return self._logger # type: ignore 

107 

108 def system(self, log_type: SystemLogType, msg: str, include_stack: bool = False) -> None: 

109 # not used in testing, therefore no stacklevel is required.. 

110 stack_level = self.stacklevel 

111 

112 if log_type == SystemLogType.FATAL: 

113 self.log.critical(msg, stacklevel=stack_level, stack_info=include_stack) 

114 elif log_type == SystemLogType.ERROR: 

115 self.error(msg, include_stack=include_stack) 

116 elif log_type == SystemLogType.WARNING: 

117 self.warning(msg, include_stack=include_stack) 

118 else: 

119 self.info(msg, include_stack=include_stack) 

120 

121 def trace(self, msg: str, include_stack: bool = False) -> None: 

122 # not used in testing, therefore no stacklevel is required.. 

123 if not self.is_trace: 

124 return 

125 

126 self.log._log( 

127 TRACE_LEVEL, 

128 msg, 

129 args=(), 

130 stack_info=include_stack, 

131 stacklevel=self.stacklevel, 

132 ) 

133 # self.log.debug("TRACE: %s", msg, stacklevel=self.stacklevel, stack_info=include_stack) 

134 

135 def debug(self, msg: str, include_stack: bool = False, stacklevel: int | None = None) -> None: 

136 stack_level = stacklevel if stacklevel is not None else self.stacklevel 

137 self.log.debug(msg, stacklevel=stack_level, stack_info=include_stack) 

138 

139 def info(self, msg: str, include_stack: bool = False, stacklevel: int | None = None) -> None: 

140 stack_level = stacklevel if stacklevel is not None else self.stacklevel 

141 self.log.info(msg, stacklevel=stack_level, stack_info=include_stack) 

142 

143 def warning( 

144 self, msg: str, include_stack: bool = False, stacklevel: int | None = None 

145 ) -> None: 

146 stack_level = stacklevel if stacklevel is not None else self.stacklevel 

147 self.log.warning(msg, stacklevel=stack_level, stack_info=include_stack) 

148 

149 def error(self, msg: str, include_stack: bool = False, stacklevel: int | None = None) -> None: 

150 stack_level = stacklevel if stacklevel is not None else self.stacklevel 

151 self.log.error(msg, stacklevel=stack_level, stack_info=include_stack) 

152 

153 def performance(self, msg: str, include_stack: bool = False) -> None: 

154 # not used in testing, therefore no stacklevel is required.. 

155 self.log._log( 

156 PERFORMANCE_LEVEL, 

157 msg, 

158 args=(), 

159 stacklevel=self.stacklevel, 

160 stack_info=include_stack, 

161 ) 

162 

163 def _setup_logging(self) -> None: # pragma: no cover 

164 if self._logger is not None: 

165 return 

166 

167 # setup the default stack printer.. 

168 import stackprinter 

169 

170 stackprinter.set_excepthook( 

171 reverse=True, # Most recent call at the bottom (matches modern IDEs) 

172 source_lines=3, # 3 lines provides context without bloat 

173 show_vals="line", # Keeps variable values on the same line as code 

174 truncate_vals=100, # Prevents giant strings/dicts from pushing code off-screen 

175 line_wrap=120, # Standard width for most modern terminal windows 

176 suppressed_paths=[r"lib/python.*/site-packages"], # Hides library internals 

177 style="color", # Use 'color' or 'monochrome' depending on your terminal 

178 ) 

179 

180 from flipdare.app_env import get_env_type 

181 from flipdare.util.app_log_formatter import AppLogFormatter 

182 

183 # the env may not be initialized at this point, 

184 # so we get it directly from os.environ .. 

185 env_type = get_env_type() 

186 self._is_prod = env_type.is_prod 

187 

188 import logging as loaded_logging 

189 

190 lvl = _DEFAULT_PROD_LEVEL if self._is_prod else _DEFAULT_TEST_LEVEL 

191 

192 # rather than clearing root logger just updated levels 

193 for logger_name in _NOISY_LOGGERS: 

194 loaded_logging.getLogger(logger_name).setLevel(loaded_logging.WARNING) 

195 

196 app_logger = loaded_logging.getLogger(APP_LOG_NAME) 

197 app_logger.setLevel(lvl) 

198 msg = f"AppLogger initialized with level {lvl} and is_prod={self._is_prod}" 

199 print(msg) # noqa: T201 

200 

201 loaded_logging.addLevelName(TRACE_LEVEL, "TRACE") 

202 loaded_logging.addLevelName(PERFORMANCE_LEVEL, "PERFORMANCE") 

203 loaded_logging.Logger.trace = self._trace_wrapper # type: ignore 

204 loaded_logging.Logger.performance = self._performance_wrapper # type: ignore 

205 

206 # Add our custom handler to root logger 

207 handler = loaded_logging.StreamHandler() 

208 handler.setLevel(lvl) 

209 handler.setFormatter(AppLogFormatter()) 

210 app_logger.addHandler(handler) 

211 

212 # CRITICAL: Prevent duplicate logging to root logger 

213 app_logger.propagate = False 

214 self._logger = app_logger 

215 

216 def _trace_wrapper(self, message: str, *args: Any, **kws: Any) -> None: 

217 if self.log.isEnabledFor(TRACE_LEVEL): 

218 # Yes, Logger._log takes its '*args' as 'args' 

219 self.trace(message, *args, **kws) 

220 

221 def _performance_wrapper(self, message: str, *args: Any, **kws: Any) -> None: 

222 if self.log.isEnabledFor(PERFORMANCE_LEVEL): 

223 # Yes, Logger._log takes its '*args' as 'args' 

224 self.performance(message, *args, **kws)