Coverage for functions \ flipdare \ backend \ app_stats.py: 70%

89 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 

13from flipdare.backend.app_logger import AppLogger 

14from flipdare.app_log import LOG 

15from flipdare.app_types import CronResult 

16from flipdare.constants import IS_TRACE 

17from flipdare.result.output_result import OutputResult 

18from flipdare.core.singleton import Singleton 

19from flipdare.firestore.backend.app_stat_db import AppStatDb 

20from flipdare.generated.model.backend.app_stat_metric_model import AppStatMetricModel 

21from flipdare.generated.model.backend.metric.count_metric import CountMetric 

22from flipdare.generated.model.backend.metric.outcome_metric import OutcomeMetric 

23from flipdare.generated.shared.app_error_code import AppErrorCode 

24from flipdare.generated.shared.backend.app_job_type import AppJobType 

25from flipdare.generated.shared.firestore_collections import FirestoreCollections 

26 

27__all__ = ["AppStats"] 

28 

29 

30class AppStats(Singleton): 

31 """ 

32 Performance statistics admin. 

33 """ 

34 

35 def __init__( 

36 self, 

37 stat_db: AppStatDb | None = None, 

38 app_logger: AppLogger | None = None, 

39 ) -> None: 

40 super().__init__() 

41 self._stat_db = stat_db 

42 self._app_logger = app_logger 

43 

44 @property 

45 def stat_db(self) -> AppStatDb: 

46 from flipdare.services import get_db_manager 

47 

48 if self._stat_db is None: 

49 self._stat_db = get_db_manager().stat_db 

50 return self._stat_db 

51 

52 @property 

53 def app_logger(self) -> AppLogger: 

54 from flipdare.services import get_app_logger 

55 

56 if self._app_logger is None: 

57 self._app_logger = get_app_logger() 

58 return self._app_logger 

59 

60 def add( 

61 self, 

62 job_type: AppJobType, 

63 result: CronResult, 

64 duration: int | None = None, 

65 ) -> None: 

66 match result: 

67 case CountMetric(): 

68 self.add_count_metric(job_type=job_type, metric=result) 

69 case OutcomeMetric(): 

70 self.add_outcome_metric(job_type=job_type, metric=result) 

71 case OutputResult(): 

72 self.add_result(job_type=job_type, output_result=result, duration=duration) 

73 

74 def add_result( 

75 self, 

76 job_type: AppJobType, 

77 output_result: OutputResult, 

78 duration: int | None = None, 

79 ) -> None: 

80 succeeded = output_result.is_ok 

81 duration = duration or output_result.duration 

82 

83 if IS_TRACE: 

84 msg = ( 

85 f"Logging output result for {job_type.label}: " 

86 f"success={succeeded}, " 

87 f"message={output_result.message}, " 

88 f"duration={duration}s" 

89 ) 

90 LOG().trace(msg) 

91 

92 self.add_outcome_metric( 

93 job_type, 

94 OutcomeMetric( 

95 succeeded=succeeded, 

96 duration=duration, 

97 ), 

98 ) 

99 

100 def add_outcome( 

101 self, 

102 job_type: AppJobType, 

103 succeeded: bool, 

104 duration: int, 

105 ) -> None: 

106 if IS_TRACE: 

107 msg = ( 

108 f"Logging outcome metric for {job_type.label}: " 

109 f"succeeded={succeeded}, duration={duration}s" 

110 ) 

111 LOG().trace(msg) 

112 

113 self.add_outcome_metric( 

114 job_type, 

115 OutcomeMetric( 

116 succeeded=succeeded, 

117 duration=duration, 

118 ), 

119 ) 

120 

121 def add_outcome_metric( 

122 self, 

123 job_type: AppJobType, 

124 metric: OutcomeMetric, 

125 ) -> None: 

126 if IS_TRACE: 

127 msg = f"Logging outcome metric for {job_type.label}: {metric}" 

128 LOG().trace(msg) 

129 

130 model: AppStatMetricModel | None = None 

131 try: 

132 model = AppStatMetricModel( 

133 id=None, 

134 job_type=job_type, 

135 metric=metric, 

136 ) 

137 self._add_stat(job_type, model) 

138 except Exception as ex: 

139 # this is most likely a pydantic validation error.. 

140 cause = f"Failed to create outcome metric for {job_type.label}: {ex}" 

141 LOG().error(cause) 

142 self.app_logger.unexpected_code_path( 

143 job_type=job_type, 

144 collection=FirestoreCollections.APP_STAT_METRIC, 

145 message=cause, 

146 ex_error=ex, 

147 ) 

148 return 

149 

150 def add_count( 

151 self, 

152 job_type: AppJobType, 

153 success_ct: int, 

154 failed_ct: int, 

155 skipped_ct: int, 

156 duration: int, 

157 ) -> None: 

158 if IS_TRACE: 

159 msg = ( 

160 f"Logging count metric for {job_type.label}: " 

161 f"success={success_ct}, failed={failed_ct}, skipped={skipped_ct}, duration={duration}s" 

162 ) 

163 LOG().trace(msg) 

164 

165 self.add_count_metric( 

166 job_type, 

167 CountMetric( 

168 success_ct=success_ct, 

169 failed_ct=failed_ct, 

170 skipped_ct=skipped_ct, 

171 duration=duration, 

172 ), 

173 ) 

174 

175 def add_count_metric( 

176 self, 

177 job_type: AppJobType, 

178 metric: CountMetric, 

179 ) -> None: 

180 if IS_TRACE: 

181 msg = f"Logging count metric for {job_type.label}: {metric}" 

182 LOG().trace(msg) 

183 

184 model: AppStatMetricModel | None = None 

185 try: 

186 model = AppStatMetricModel( 

187 id=None, 

188 job_type=job_type, 

189 metric=metric, 

190 ) 

191 self._add_stat(job_type, model) 

192 except Exception as ex: 

193 # this is most likely a pydantic validation error.. 

194 cause = f"Failed to create count metric for {job_type.label}: {ex}" 

195 LOG().error(cause) 

196 self.app_logger.unexpected_code_path( 

197 job_type=job_type, 

198 collection=FirestoreCollections.APP_STAT_METRIC, 

199 message=cause, 

200 ex_error=ex, 

201 ) 

202 return 

203 

204 def _add_stat( 

205 self, 

206 job_type: AppJobType, 

207 metric: AppStatMetricModel, 

208 ) -> None: 

209 try: 

210 self.stat_db.add(metric=metric) 

211 except Exception as ex: 

212 cause = f"Failed to log count metric for {job_type.label}: {ex}" 

213 LOG().error(cause) 

214 self.app_logger.db_error( 

215 error_code=AppErrorCode.DATABASE_EX, 

216 job_type=job_type, 

217 collection=FirestoreCollections.APP_STAT_METRIC, 

218 message=cause, 

219 ex_error=ex, 

220 data=metric.to_dict(), 

221 )