Coverage for functions \ flipdare \ error \ log_context.py: 92%
90 statements
« prev ^ index » next coverage.py v7.13.0, created at 2026-05-08 12:22 +1000
« prev ^ index » next coverage.py v7.13.0, created at 2026-05-08 12:22 +1000
1#!/usr/bin/env python
2from __future__ import annotations
4from datetime import datetime, UTC
5from typing import TYPE_CHECKING, Any
6from dataclasses import dataclass, field
7from flipdare.app_types import DatabaseDict
8from flipdare.constants import EMAIL_LOG_SUBJECT_MAX_LENGTH, NO_DOC_ID
9from flipdare.generated.shared.app_log_category import AppLogCategory
10from flipdare.result.app_result import AppResult
11from flipdare.error.app_error_protocol import AppErrorProtocol
12from flipdare.generated.model.backend.app_log_model import AppLogModel
13from flipdare.generated.schema.email.body.admin.log_email_schema import LogEmailSchema
14from flipdare.generated.shared.app_error_code import AppErrorCode
15from flipdare.generated.shared.backend.app_job_type import AppJobType
16from flipdare.generated.shared.backend.system_log_type import SystemLogType
17from flipdare.generated.shared.firestore_collections import FirestoreCollections
18from flipdare.generated.shared.search.search_collections import SearchCollections
20if TYPE_CHECKING:
21 from flipdare.error.message_format import BaseMsgFormat
24__all__ = ["LogContext"]
27@dataclass(frozen=True, kw_only=True)
28class LogContext:
29 # core
30 log_type: SystemLogType
31 called_by: str
32 message: str
33 doc_id: str = NO_DOC_ID
35 category: AppLogCategory
37 # optional, since we could have info.
38 error_code: AppErrorProtocol | None = None
40 source_override: str | None = (
41 None # this is used to construct the source (fallback is called_by)
42 )
43 # this is used to construct the source (fallback is called_by)
44 collection: FirestoreCollections | SearchCollections | None = None
45 job_type: AppJobType | None = None
47 # misc
48 duration: int | None = None
49 notify_admin: bool = False
50 result: AppResult[Any] | None = None
52 # data
53 stack_trace: str | None = None
54 formatter: BaseMsgFormat | None = None
56 # auto generated
57 _occurred_at: datetime = field(default_factory=lambda: datetime.now(UTC))
58 _extra: DatabaseDict | str | None = None
60 @property
61 def occurred_at(self) -> str:
62 return self._occurred_at.strftime("%Y-%m-%d %H:%M:%S UTC")
64 @property
65 def error_code_str(self) -> str:
66 return str(self.error_code) if self.error_code else AppErrorCode.SERVER.value
68 @property
69 def source(self) -> str:
70 return (
71 self.source_override
72 or self.job_type
73 or self.collection
74 or self.called_by
75 or self.category
76 )
78 @property
79 def subject(self) -> str:
80 msg = f"[{self.log_type.value}]: "
81 msg += f"{self.source} - "
82 msg += (
83 f"{self.message[:EMAIL_LOG_SUBJECT_MAX_LENGTH]}..."
84 if len(self.message) > EMAIL_LOG_SUBJECT_MAX_LENGTH
85 else self.message
86 )
87 return msg
89 @property
90 def extra(self) -> DatabaseDict:
91 actual_extra: dict[str, Any] = {}
92 extra = self._extra
93 if extra is not None:
94 actual_extra["extra"] = extra
96 if result := self.result:
97 actual_extra["result"] = result.to_dict()
99 return actual_extra
101 @property
102 def email(self) -> LogEmailSchema:
103 schema = LogEmailSchema(
104 occurred_at=self.occurred_at,
105 log_type=self.log_type,
106 error_code=self.error_code_str,
107 called_from=self.called_by,
108 source=self.source,
109 message=self.message,
110 detail=self.formatted,
111 )
113 if extra := self._extra:
114 if isinstance(extra, str):
115 schema["extra"] = {"info": extra}
116 else:
117 schema["extra"] = extra
119 if stack := self.stack_trace:
120 schema["stack_trace"] = stack
122 return schema
124 @property
125 def log_model(self) -> AppLogModel:
126 log_type = self.log_type
127 acknowledged = False
128 if log_type in (SystemLogType.WARNING, SystemLogType.INFO):
129 acknowledged = True
131 firestore_collection: FirestoreCollections | None = None
132 search_collection: SearchCollections | None = None
133 if isinstance(self.collection, FirestoreCollections):
134 firestore_collection = self.collection
135 elif isinstance(self.collection, SearchCollections):
136 search_collection = self.collection
138 return AppLogModel(
139 id=None,
140 category=self.category,
141 log_type=log_type,
142 source=self.source,
143 firestore_collection=firestore_collection,
144 search_collection=search_collection,
145 called_by=self.called_by,
146 admin_notified=self.notify_admin,
147 acknowledged=acknowledged,
148 message=self.message,
149 job_type=self.job_type or None,
150 error_code=self.error_code or None,
151 obj_id=self.doc_id,
152 extra=self.extra,
153 stack_trace=self.stack_trace,
154 )
156 @property
157 def formatted(self) -> str:
158 from flipdare.error.message_format import ErrorMsgFormat, InfoMsgFormat
160 if self.formatter is not None:
161 return str(self.formatter)
163 if self.log_type == SystemLogType.INFO:
164 return str(InfoMsgFormat(self))
166 return str(ErrorMsgFormat(self))