monitaudit 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
monitaudit/__init__.py ADDED
@@ -0,0 +1,23 @@
1
+ # src/monitaudit/__init__.py
2
+
3
+ from __future__ import annotations
4
+
5
+ from .logger import monitauditLogger
6
+ from .models.context import LogContext
7
+ from .models.monitaudit import Observability
8
+ from .sinks import (
9
+ ConsoleSink,
10
+ DeltaSink,
11
+ FileSink,
12
+ JsonSink,
13
+ )
14
+
15
+ __all__ = [
16
+ "ConsoleSink",
17
+ "DeltaSink",
18
+ "FileSink",
19
+ "JsonSink",
20
+ "LogContext",
21
+ "Observability",
22
+ "monitauditLogger",
23
+ ]
@@ -0,0 +1,15 @@
1
+ # src/monitaudit/constants.py
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ FUNCTION_START = "function_start"
7
+ FUNCTION_END = "function_end"
8
+ FUNCTION_ERROR = "function_error"
9
+
10
+
11
+ __all__ = [
12
+ "FUNCTION_START",
13
+ "FUNCTION_END",
14
+ "FUNCTION_ERROR",
15
+ ]
monitaudit/core.py ADDED
File without changes
monitaudit/logger.py ADDED
@@ -0,0 +1,449 @@
1
+ # src/monitaudit/logger.py
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ import sys
7
+ import traceback
8
+ from typing import Any, Literal, cast
9
+
10
+ from monitaudit.models.base import BaseLog
11
+ from monitaudit.models.context import LogContext
12
+ from monitaudit.models.log import (
13
+ CriticalLog,
14
+ DebugLog,
15
+ ErrorLog,
16
+ InfoLog,
17
+ WarningLog,
18
+ )
19
+ from monitaudit.sinks.base import Sink
20
+ from monitaudit.utilities.json_safe import json_safe_dict
21
+
22
+
23
+ SinkFailureMode = Literal[
24
+ "continue",
25
+ "raise",
26
+ ]
27
+
28
+ StandardLog = DebugLog | InfoLog | WarningLog
29
+ StandardLogType = (
30
+ type[DebugLog]
31
+ | type[InfoLog]
32
+ | type[WarningLog]
33
+ )
34
+
35
+ ExceptionLog = ErrorLog | CriticalLog
36
+ ExceptionLogType = (
37
+ type[ErrorLog]
38
+ | type[CriticalLog]
39
+ )
40
+
41
+
42
+ class monitauditLogger:
43
+ def __init__(
44
+ self,
45
+ context: LogContext,
46
+ *sinks: Sink,
47
+ sink_failure_mode: SinkFailureMode = "continue",
48
+ ) -> None:
49
+ self.context: LogContext = context
50
+ self.sinks: tuple[Sink, ...] = sinks
51
+ self.sink_failure_mode: SinkFailureMode = (
52
+ sink_failure_mode
53
+ )
54
+
55
+ def debug(
56
+ self,
57
+ message: str,
58
+ *,
59
+ call_id: str | None = None,
60
+ parent_call_id: str | None = None,
61
+ parameters: dict[str, Any] | None = None,
62
+ duration_ms: float | None = None,
63
+ module: str | None = None,
64
+ function: str | None = None,
65
+ **attributes: Any,
66
+ ) -> DebugLog:
67
+ return cast(
68
+ DebugLog,
69
+ self._log(
70
+ DebugLog,
71
+ message,
72
+ call_id=call_id,
73
+ parent_call_id=parent_call_id,
74
+ parameters=parameters,
75
+ duration_ms=duration_ms,
76
+ module=module,
77
+ function=function,
78
+ attributes=attributes,
79
+ ),
80
+ )
81
+
82
+ def info(
83
+ self,
84
+ message: str,
85
+ *,
86
+ call_id: str | None = None,
87
+ parent_call_id: str | None = None,
88
+ parameters: dict[str, Any] | None = None,
89
+ duration_ms: float | None = None,
90
+ module: str | None = None,
91
+ function: str | None = None,
92
+ **attributes: Any,
93
+ ) -> InfoLog:
94
+ return cast(
95
+ InfoLog,
96
+ self._log(
97
+ InfoLog,
98
+ message,
99
+ call_id=call_id,
100
+ parent_call_id=parent_call_id,
101
+ parameters=parameters,
102
+ duration_ms=duration_ms,
103
+ module=module,
104
+ function=function,
105
+ attributes=attributes,
106
+ ),
107
+ )
108
+
109
+ def warning(
110
+ self,
111
+ message: str,
112
+ *,
113
+ call_id: str | None = None,
114
+ parent_call_id: str | None = None,
115
+ parameters: dict[str, Any] | None = None,
116
+ duration_ms: float | None = None,
117
+ module: str | None = None,
118
+ function: str | None = None,
119
+ **attributes: Any,
120
+ ) -> WarningLog:
121
+ return cast(
122
+ WarningLog,
123
+ self._log(
124
+ WarningLog,
125
+ message,
126
+ call_id=call_id,
127
+ parent_call_id=parent_call_id,
128
+ parameters=parameters,
129
+ duration_ms=duration_ms,
130
+ module=module,
131
+ function=function,
132
+ attributes=attributes,
133
+ ),
134
+ )
135
+
136
+ def error(
137
+ self,
138
+ message: str,
139
+ *,
140
+ exception: BaseException | None = None,
141
+ call_id: str | None = None,
142
+ parent_call_id: str | None = None,
143
+ parameters: dict[str, Any] | None = None,
144
+ duration_ms: float | None = None,
145
+ module: str | None = None,
146
+ function: str | None = None,
147
+ **attributes: Any,
148
+ ) -> ErrorLog:
149
+ return cast(
150
+ ErrorLog,
151
+ self._log_exception(
152
+ ErrorLog,
153
+ message,
154
+ exception=exception,
155
+ call_id=call_id,
156
+ parent_call_id=parent_call_id,
157
+ parameters=parameters,
158
+ duration_ms=duration_ms,
159
+ module=module,
160
+ function=function,
161
+ attributes=attributes,
162
+ ),
163
+ )
164
+
165
+ def critical(
166
+ self,
167
+ message: str,
168
+ *,
169
+ exception: BaseException | None = None,
170
+ call_id: str | None = None,
171
+ parent_call_id: str | None = None,
172
+ parameters: dict[str, Any] | None = None,
173
+ duration_ms: float | None = None,
174
+ module: str | None = None,
175
+ function: str | None = None,
176
+ **attributes: Any,
177
+ ) -> CriticalLog:
178
+ return cast(
179
+ CriticalLog,
180
+ self._log_exception(
181
+ CriticalLog,
182
+ message,
183
+ exception=exception,
184
+ call_id=call_id,
185
+ parent_call_id=parent_call_id,
186
+ parameters=parameters,
187
+ duration_ms=duration_ms,
188
+ module=module,
189
+ function=function,
190
+ attributes=attributes,
191
+ ),
192
+ )
193
+
194
+ def _log(
195
+ self,
196
+ log_type: StandardLogType,
197
+ message: str,
198
+ *,
199
+ call_id: str | None,
200
+ parent_call_id: str | None,
201
+ parameters: dict[str, Any] | None,
202
+ duration_ms: float | None,
203
+ module: str | None,
204
+ function: str | None,
205
+ attributes: dict[str, Any],
206
+ ) -> StandardLog:
207
+ resolved_module, resolved_function = self._resolve_caller(
208
+ module=module,
209
+ function=function,
210
+ )
211
+
212
+ log = log_type(
213
+ message=message,
214
+ context=self.context,
215
+ call_id=call_id,
216
+ parent_call_id=parent_call_id,
217
+ parameters=self._prepare_parameters(
218
+ parameters
219
+ ),
220
+ duration_ms=duration_ms,
221
+ module=resolved_module,
222
+ function=resolved_function,
223
+ attributes=self._prepare_attributes(
224
+ attributes
225
+ ),
226
+ )
227
+
228
+ self._write(log)
229
+
230
+ return log
231
+
232
+ def _log_exception(
233
+ self,
234
+ log_type: ExceptionLogType,
235
+ message: str,
236
+ *,
237
+ exception: BaseException | None,
238
+ call_id: str | None,
239
+ parent_call_id: str | None,
240
+ parameters: dict[str, Any] | None,
241
+ duration_ms: float | None,
242
+ module: str | None,
243
+ function: str | None,
244
+ attributes: dict[str, Any],
245
+ ) -> ExceptionLog:
246
+ resolved_module, resolved_function = self._resolve_caller(
247
+ module=module,
248
+ function=function,
249
+ )
250
+
251
+ (
252
+ error_type,
253
+ error_message,
254
+ traceback_lines,
255
+ ) = self._serialize_exception(exception)
256
+
257
+ log = log_type(
258
+ message=message,
259
+ context=self.context,
260
+ call_id=call_id,
261
+ parent_call_id=parent_call_id,
262
+ parameters=self._prepare_parameters(
263
+ parameters
264
+ ),
265
+ duration_ms=duration_ms,
266
+ module=resolved_module,
267
+ function=resolved_function,
268
+ attributes=self._prepare_attributes(
269
+ attributes
270
+ ),
271
+ error_type=error_type,
272
+ error_message=error_message,
273
+ traceback=traceback_lines,
274
+ )
275
+
276
+ self._write(log)
277
+
278
+ return log
279
+
280
+ @staticmethod
281
+ def _prepare_parameters(
282
+ parameters: dict[str, Any] | None,
283
+ ) -> dict[str, Any]:
284
+ """
285
+ Convert parameters to JSON-safe values.
286
+
287
+ Parameter redaction and size limiting are performed by
288
+ Observability.trace(). This method provides the final safety
289
+ boundary before parameters are stored in the log model.
290
+ """
291
+ if not parameters:
292
+ return {}
293
+
294
+ return json_safe_dict(parameters)
295
+
296
+ @staticmethod
297
+ def _prepare_attributes(
298
+ attributes: dict[str, Any],
299
+ ) -> dict[str, Any]:
300
+ """
301
+ Convert arbitrary structured attributes to JSON-safe values.
302
+
303
+ This protects all sinks from unsupported objects such as Path,
304
+ UUID, datetime, Enum, set, Pydantic models, and custom classes.
305
+ """
306
+ if not attributes:
307
+ return {}
308
+
309
+ return json_safe_dict(attributes)
310
+
311
+ @staticmethod
312
+ def _serialize_exception(
313
+ exception: BaseException | None,
314
+ ) -> tuple[
315
+ str | None,
316
+ str | None,
317
+ list[str],
318
+ ]:
319
+ """
320
+ Convert an exception into serialisable error fields.
321
+
322
+ The raw exception object is never stored in a log model or
323
+ passed to a sink.
324
+ """
325
+ if exception is None:
326
+ return None, None, []
327
+
328
+ return (
329
+ type(exception).__name__,
330
+ str(exception),
331
+ traceback.format_exception(
332
+ type(exception),
333
+ exception,
334
+ exception.__traceback__,
335
+ ),
336
+ )
337
+
338
+ @staticmethod
339
+ def _report_sink_failure(
340
+ *,
341
+ sink: Sink,
342
+ exception: Exception,
343
+ ) -> None:
344
+ print(
345
+ (
346
+ "monitaudit sink failure: "
347
+ f"{type(sink).__name__}: "
348
+ f"{type(exception).__name__}: "
349
+ f"{exception}"
350
+ ),
351
+ file=sys.stderr,
352
+ )
353
+
354
+ def _write(
355
+ self,
356
+ log: BaseLog,
357
+ ) -> None:
358
+ failures: list[
359
+ tuple[Sink, Exception]
360
+ ] = []
361
+
362
+ for sink in self.sinks:
363
+ try:
364
+ sink.write(log)
365
+
366
+ except Exception as exception:
367
+ failures.append(
368
+ (
369
+ sink,
370
+ exception,
371
+ )
372
+ )
373
+
374
+ self._report_sink_failure(
375
+ sink=sink,
376
+ exception=exception,
377
+ )
378
+
379
+ if (
380
+ failures
381
+ and self.sink_failure_mode == "raise"
382
+ ):
383
+ sink_names = ", ".join(
384
+ type(sink).__name__
385
+ for sink, _ in failures
386
+ )
387
+
388
+ raise RuntimeError(
389
+ f"monitaudit sink failure(s): {sink_names}"
390
+ ) from failures[0][1]
391
+
392
+ def _resolve_caller(
393
+ self,
394
+ *,
395
+ module: str | None,
396
+ function: str | None,
397
+ ) -> tuple[
398
+ str | None,
399
+ str | None,
400
+ ]:
401
+ if (
402
+ module is not None
403
+ and function is not None
404
+ ):
405
+ return module, function
406
+
407
+ (
408
+ detected_module,
409
+ detected_function,
410
+ ) = self._caller()
411
+
412
+ return (
413
+ module or detected_module,
414
+ function or detected_function,
415
+ )
416
+
417
+ @staticmethod
418
+ def _caller() -> tuple[
419
+ str | None,
420
+ str | None,
421
+ ]:
422
+ frame = inspect.currentframe()
423
+
424
+ if frame is None:
425
+ return None, None
426
+
427
+ try:
428
+ caller = frame.f_back
429
+
430
+ while caller is not None:
431
+ module = caller.f_globals.get(
432
+ "__name__"
433
+ )
434
+ function = (
435
+ caller.f_code.co_name
436
+ )
437
+
438
+ if module not in {
439
+ __name__,
440
+ "monitaudit.models.monitaudit",
441
+ }:
442
+ return module, function
443
+
444
+ caller = caller.f_back
445
+
446
+ return None, None
447
+
448
+ finally:
449
+ del frame
@@ -0,0 +1,23 @@
1
+ # src/monitaudit/models/__init__.py
2
+
3
+ from __future__ import annotations
4
+
5
+ from .base import BaseLog
6
+ from .context import LogContext
7
+ from .log import (
8
+ CriticalLog,
9
+ DebugLog,
10
+ ErrorLog,
11
+ InfoLog,
12
+ WarningLog,
13
+ )
14
+
15
+ __all__ = [
16
+ "BaseLog",
17
+ "CriticalLog",
18
+ "DebugLog",
19
+ "ErrorLog",
20
+ "InfoLog",
21
+ "LogContext",
22
+ "WarningLog",
23
+ ]
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime, timezone
4
+ from typing import Any
5
+
6
+ from pydantic import BaseModel, ConfigDict, Field
7
+
8
+ from monitaudit.models.context import LogContext
9
+
10
+
11
+ class BaseLog(BaseModel):
12
+ model_config = ConfigDict(extra="forbid")
13
+
14
+ timestamp: datetime = Field(
15
+ default_factory=lambda: datetime.now(timezone.utc)
16
+ )
17
+
18
+ level: str
19
+ message: str
20
+ context: LogContext
21
+
22
+ call_id: str | None = None
23
+ parent_call_id: str | None = None
24
+
25
+ parameters: dict[str, Any] = Field(default_factory=dict)
26
+
27
+ duration_ms: float | None = Field(default=None, ge=0)
28
+
29
+ module: str | None = None
30
+ function: str | None = None
31
+
32
+ attributes: dict[str, Any] = Field(default_factory=dict)
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+ from uuid import uuid4
5
+
6
+ from pydantic import BaseModel, ConfigDict, Field
7
+
8
+
9
+ class LogContext(BaseModel):
10
+ model_config = ConfigDict(
11
+ extra="forbid",
12
+ frozen=True,
13
+ )
14
+
15
+ application: str = Field(min_length=1)
16
+ environment: str = Field(min_length=1)
17
+
18
+ correlation_id: str = Field(
19
+ default_factory=lambda: str(uuid4())
20
+ )
21
+
22
+ run_id: str | None = None
23
+
24
+ attributes: dict[str, Any] = Field(default_factory=dict)
@@ -0,0 +1,35 @@
1
+ # src/monitaudit/models/log.py
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Literal
6
+
7
+ from pydantic import Field
8
+
9
+ from monitaudit.models.base import BaseLog
10
+
11
+
12
+ class DebugLog(BaseLog):
13
+ level: Literal["DEBUG"] = "DEBUG"
14
+
15
+
16
+ class InfoLog(BaseLog):
17
+ level: Literal["INFO"] = "INFO"
18
+
19
+
20
+ class WarningLog(BaseLog):
21
+ level: Literal["WARNING"] = "WARNING"
22
+
23
+
24
+ class ErrorLog(BaseLog):
25
+ level: Literal["ERROR"] = "ERROR"
26
+ error_type: str | None = None
27
+ error_message: str | None = None
28
+ traceback: list[str] = Field( default_factory=list )
29
+
30
+
31
+ class CriticalLog(BaseLog):
32
+ level: Literal["CRITICAL"] = "CRITICAL"
33
+ error_type: str | None = None
34
+ error_message: str | None = None
35
+ traceback: list[str] = Field( default_factory=list )