cloud-dog-logging 0.4.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.
Files changed (44) hide show
  1. cloud_dog_logging/GAPS.md +38 -0
  2. cloud_dog_logging/__init__.py +406 -0
  3. cloud_dog_logging/app_logger.py +143 -0
  4. cloud_dog_logging/audit_logger.py +333 -0
  5. cloud_dog_logging/audit_schema.py +237 -0
  6. cloud_dog_logging/batching.py +86 -0
  7. cloud_dog_logging/compat.py +94 -0
  8. cloud_dog_logging/config.py +248 -0
  9. cloud_dog_logging/correlation.py +100 -0
  10. cloud_dog_logging/errors.py +35 -0
  11. cloud_dog_logging/event_catalogue.py +76 -0
  12. cloud_dog_logging/exceptions.py +43 -0
  13. cloud_dog_logging/field_providers.py +227 -0
  14. cloud_dog_logging/formatters/__init__.py +64 -0
  15. cloud_dog_logging/formatters/json_formatter.py +326 -0
  16. cloud_dog_logging/formatters/text_formatter.py +88 -0
  17. cloud_dog_logging/handler_types.py +103 -0
  18. cloud_dog_logging/handlers/__init__.py +28 -0
  19. cloud_dog_logging/handlers/dual_handler.py +82 -0
  20. cloud_dog_logging/handlers/rotating_file.py +210 -0
  21. cloud_dog_logging/handlers/stdout_handler.py +49 -0
  22. cloud_dog_logging/health/__init__.py +26 -0
  23. cloud_dog_logging/health/reporter.py +121 -0
  24. cloud_dog_logging/integrity.py +223 -0
  25. cloud_dog_logging/middleware/__init__.py +27 -0
  26. cloud_dog_logging/middleware/audit.py +261 -0
  27. cloud_dog_logging/middleware/fastapi.py +163 -0
  28. cloud_dog_logging/presets.py +80 -0
  29. cloud_dog_logging/redaction.py +207 -0
  30. cloud_dog_logging/sampling.py +82 -0
  31. cloud_dog_logging/signing.py +78 -0
  32. cloud_dog_logging/sinks/__init__.py +41 -0
  33. cloud_dog_logging/sinks/base.py +52 -0
  34. cloud_dog_logging/sinks/db_sink.py +67 -0
  35. cloud_dog_logging/sinks/fan_out.py +65 -0
  36. cloud_dog_logging/sinks/file_sink.py +94 -0
  37. cloud_dog_logging/sinks/stdout_sink.py +56 -0
  38. cloud_dog_logging/tool_events.py +67 -0
  39. cloud_dog_logging-0.4.0.dist-info/METADATA +23 -0
  40. cloud_dog_logging-0.4.0.dist-info/RECORD +44 -0
  41. cloud_dog_logging-0.4.0.dist-info/WHEEL +4 -0
  42. cloud_dog_logging-0.4.0.dist-info/licenses/LICENCE +190 -0
  43. cloud_dog_logging-0.4.0.dist-info/licenses/LICENSE +176 -0
  44. cloud_dog_logging-0.4.0.dist-info/licenses/NOTICE +7 -0
@@ -0,0 +1,38 @@
1
+ # cloud_dog_logging PS-40 v2 Gaps
2
+
3
+ Source lane: W28A-619
4
+ Status: Documentation-only gap note. No source changes made in this lane.
5
+
6
+ The full package gap matrix is recorded at:
7
+
8
+ ```text
9
+ cloud-dog-ai-platform-standards/working/evidence/W28A-619-log-standards-review/02-cloud-dog-log-gap-matrix.md
10
+ ```
11
+
12
+ ## Summary
13
+
14
+ - Total reviewed rows: 47
15
+ - Block rows: 5
16
+ - Major rows: 34
17
+ - Minor rows: 4
18
+ - Present/no-gap rows: 4
19
+
20
+ ## Blockers Before PS-40 v2 Runtime Adoption
21
+
22
+ | Gap | Evidence | Required fix lane |
23
+ |---|---|---|
24
+ | No canonical `span_id` context model/formatter/schema. | `audit_schema.py:138-160`, `json_formatter.py:251-260`, `correlation.py:32-58` | W28A-XYZ |
25
+ | No per-surface API/WebUI/MCP/A2A handler/file configuration. | `config.py:56-93`, `__init__.py:198-223`, `__init__.py:359-382` | W28A-XYZ |
26
+ | No `<service>.<surface>.log` path derivation from `log.dir`. | `config.py:61-63`, `__init__.py:200-223`, `__init__.py:364-375` | W28A-XYZ |
27
+ | No outbound `X-Correlation-Id` / W3C `traceparent` injection helper. | `correlation.py:32-58` plus package source inspection | W28A-XYZ |
28
+ | No job envelope helper for lifecycle correlation inheritance. | `correlation.py:32-58` plus package source inspection | W28A-XYZ |
29
+
30
+ ## Follow-on W28A-XYZ Must Cover
31
+
32
+ 1. Emit the complete PS-40 v2 canonical field set in both application and audit entries.
33
+ 2. Add per-surface file topology and `log.dir`-based path derivation.
34
+ 3. Add W3C trace context support including `trace_id`, `span_id`, inbound parsing, outbound forwarding, and job inheritance.
35
+ 4. Add explicit redaction presets for JWT, Vault token, OAuth token, API key, password, session cookie, and PII classes.
36
+ 5. Add automatic audit sub-channel routing for SECURITY/AUDIT/auth/rbac/config/security/denied/error events.
37
+ 6. Add forbidden-pattern lint helper and PS-40 v2 conformance tests.
38
+ 7. Update README, REQUIREMENTS, TESTS, and usage docs, then build/test/publish with evidence.
@@ -0,0 +1,406 @@
1
+ # Copyright 2026 Cloud-Dog, Viewdeck Engineering Limited
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ # cloud_dog_logging — Public API
16
+ #
17
+ # Licence: Apache 2.0 — Cloud-Dog AI Platform
18
+ # Owner: Cloud-Dog AI
19
+ # Description: Drop-in Python logging library implementing PS-40. Provides
20
+ # two mandatory log streams (audit + application), structured JSON output,
21
+ # correlation ID propagation, secret redaction, configurable rotation, and
22
+ # the public extension surface (LogRecord factory hook, JSON-field hook,
23
+ # declarative HandlerType enum) that lets services delete bespoke
24
+ # ``logger.py`` wrappers without losing functionality.
25
+ # Related requirements: FR1.1, FR1.9, FR1.18, FR1.19, FR1.20, FR1.21, FR1.22, FR1.23, FR1.24
26
+ # Related architecture: SA1, CC1.1, CC1.6, CC1.9
27
+
28
+ """cloud_dog_logging — PS-40 Logging & Observability for Cloud-Dog services."""
29
+
30
+ from __future__ import annotations
31
+
32
+ import atexit
33
+ import logging
34
+ from typing import Any, Callable
35
+
36
+ from cloud_dog_logging.app_logger import AppLogger
37
+ from cloud_dog_logging.audit_logger import AuditLogger
38
+ from cloud_dog_logging.audit_schema import Actor, AuditEvent, Target
39
+ from cloud_dog_logging.batching import BatchingSink
40
+ from cloud_dog_logging.compat import setup_logger
41
+ from cloud_dog_logging.config import LogConfig
42
+ from cloud_dog_logging.correlation import (
43
+ clear_correlation_id,
44
+ get_correlation_id,
45
+ get_environment,
46
+ get_service_name,
47
+ get_service_instance,
48
+ set_correlation_id,
49
+ set_environment,
50
+ set_service_name,
51
+ set_service_instance,
52
+ )
53
+ from cloud_dog_logging.exceptions import format_exception
54
+ from cloud_dog_logging.field_providers import (
55
+ FieldProvider,
56
+ clear_log_field_providers,
57
+ get_registered_field_providers,
58
+ register_log_field_provider,
59
+ unregister_log_field_provider,
60
+ )
61
+ from cloud_dog_logging.handler_types import HandlerType
62
+ from cloud_dog_logging.middleware.audit import AuditMiddleware
63
+ from cloud_dog_logging.formatters import (
64
+ JSONFieldProvider,
65
+ JSONFormatter,
66
+ TextFormatter,
67
+ add_json_field,
68
+ clear_json_fields,
69
+ get_registered_json_fields,
70
+ remove_json_field,
71
+ )
72
+ from cloud_dog_logging.handlers.dual_handler import DualHandler
73
+ from cloud_dog_logging.handlers.rotating_file import ConfigurableRotatingHandler
74
+ from cloud_dog_logging.handlers.stdout_handler import StdoutHandler
75
+ from cloud_dog_logging.health.reporter import LogHealthReporter
76
+ from cloud_dog_logging.integrity import AuditIntegrityVerifier
77
+ from cloud_dog_logging.presets import BUILTIN_PRESETS, RedactionPreset, load_presets
78
+ from cloud_dog_logging.redaction import RedactionEngine
79
+ from cloud_dog_logging.sampling import SamplingFilter
80
+ from cloud_dog_logging.signing import HMACSigner
81
+ from cloud_dog_logging.sinks import AuditSink, DatabaseSink, FanOutSink, FileSink, StdoutSink
82
+ from cloud_dog_logging.tool_events import log_tool_event
83
+
84
+ __all__ = [
85
+ "AuditMiddleware",
86
+ "setup_logging",
87
+ "get_logger",
88
+ "get_audit_logger",
89
+ "setup_logger",
90
+ "log_tool_event",
91
+ "AppLogger",
92
+ "AuditLogger",
93
+ "Actor",
94
+ "AuditEvent",
95
+ "Target",
96
+ "LogConfig",
97
+ "RedactionEngine",
98
+ "RedactionPreset",
99
+ "BUILTIN_PRESETS",
100
+ "load_presets",
101
+ "SamplingFilter",
102
+ "BatchingSink",
103
+ "HMACSigner",
104
+ "AuditSink",
105
+ "FileSink",
106
+ "StdoutSink",
107
+ "DatabaseSink",
108
+ "FanOutSink",
109
+ "format_exception",
110
+ "JSONFormatter",
111
+ "TextFormatter",
112
+ "ConfigurableRotatingHandler",
113
+ "StdoutHandler",
114
+ "DualHandler",
115
+ "LogHealthReporter",
116
+ "AuditIntegrityVerifier",
117
+ "get_integrity_verifier",
118
+ "get_correlation_id",
119
+ "set_correlation_id",
120
+ "clear_correlation_id",
121
+ "get_service_name",
122
+ "get_service_instance",
123
+ "get_environment",
124
+ "set_service_name",
125
+ "set_service_instance",
126
+ "set_environment",
127
+ # Public extension surface (0.4.0):
128
+ "FieldProvider",
129
+ "register_log_field_provider",
130
+ "unregister_log_field_provider",
131
+ "clear_log_field_providers",
132
+ "get_registered_field_providers",
133
+ "JSONFieldProvider",
134
+ "add_json_field",
135
+ "remove_json_field",
136
+ "clear_json_fields",
137
+ "get_registered_json_fields",
138
+ "HandlerType",
139
+ ]
140
+
141
+ __version__ = "0.4.0"
142
+
143
+ _audit_logger: AuditLogger | None = None
144
+ _redaction_engine: RedactionEngine | None = None
145
+ _log_config: LogConfig | None = None
146
+ _sampling_filter: SamplingFilter | None = None
147
+ _integrity_verifier: AuditIntegrityVerifier | None = None
148
+ _is_configured: bool = False
149
+
150
+
151
+ def setup_logging(config: dict[str, Any] | Any | None = None) -> None:
152
+ """One-time logging setup from config dict or platform GlobalConfig."""
153
+ global _audit_logger, _redaction_engine, _log_config, _sampling_filter, _integrity_verifier, _is_configured
154
+
155
+ if _audit_logger is not None:
156
+ try:
157
+ _audit_logger.close()
158
+ except Exception:
159
+ pass
160
+ if _integrity_verifier is not None:
161
+ try:
162
+ _integrity_verifier.stop()
163
+ except Exception:
164
+ pass
165
+ _integrity_verifier = None
166
+
167
+ if config is None:
168
+ _log_config = LogConfig()
169
+ elif isinstance(config, dict):
170
+ _log_config = LogConfig.from_dict(config)
171
+ else:
172
+ _log_config = LogConfig.from_platform_config(config)
173
+
174
+ set_service_name(_log_config.service_name)
175
+ set_service_instance(_log_config.service_instance)
176
+ set_environment(_log_config.environment)
177
+
178
+ presets = _resolve_redaction_presets(config, _log_config)
179
+ _redaction_engine = RedactionEngine(
180
+ additional_patterns=_log_config.redaction_patterns if _log_config.redaction_patterns else None,
181
+ pii_enabled=_log_config.pii_redaction,
182
+ presets=presets,
183
+ )
184
+
185
+ if _log_config.log_format.lower() == "json":
186
+ formatter: logging.Formatter = JSONFormatter(
187
+ service_name=_log_config.service_name,
188
+ extra_fields=list(_log_config.extra_fields) if _log_config.extra_fields else None,
189
+ )
190
+ else:
191
+ formatter = TextFormatter(service_name=_log_config.service_name)
192
+
193
+ root_level = getattr(logging, _log_config.log_level.upper(), logging.INFO)
194
+ app_root = logging.getLogger()
195
+ app_root.setLevel(root_level)
196
+ app_root.handlers.clear()
197
+
198
+ handler_selection = _resolve_handler_selection(_log_config)
199
+
200
+ if "file" in handler_selection:
201
+ file_handler = ConfigurableRotatingHandler(
202
+ filename=_log_config.app_log_file, # type: ignore[arg-type]
203
+ max_bytes=_log_config.rotation_max_bytes,
204
+ backup_count=_log_config.rotation_backup_count,
205
+ rotation_mode=_log_config.rotation_mode,
206
+ when=_log_config.rotation_when,
207
+ interval=_log_config.rotation_interval,
208
+ compress=_log_config.rotation_compress,
209
+ stream_name="application",
210
+ )
211
+ file_handler.setFormatter(formatter)
212
+ if "console" in handler_selection:
213
+ stdout_handler = StdoutHandler(stream_name="stdout")
214
+ stdout_handler.setFormatter(formatter)
215
+ dual = DualHandler(file_handler=file_handler, stream_handler=stdout_handler)
216
+ dual.setFormatter(formatter)
217
+ app_root.addHandler(dual)
218
+ else:
219
+ app_root.addHandler(file_handler)
220
+ elif "console" in handler_selection:
221
+ stdout_handler = StdoutHandler(stream_name="stdout")
222
+ stdout_handler.setFormatter(formatter)
223
+ app_root.addHandler(stdout_handler)
224
+
225
+ _sampling_filter = None
226
+ if _log_config.sampling_rates:
227
+ _sampling_filter = SamplingFilter(_log_config.sampling_rates)
228
+ for handler in app_root.handlers:
229
+ handler.addFilter(_sampling_filter)
230
+
231
+ audit_sink = _build_audit_sink(_log_config, on_audit_rotate=_on_audit_rotation)
232
+ signer = _build_signer(_log_config)
233
+ audit_py_logger = logging.getLogger("cloud_dog_logging.audit")
234
+ audit_py_logger.setLevel(logging.INFO)
235
+ audit_py_logger.propagate = False
236
+ if not audit_py_logger.handlers:
237
+ audit_py_logger.addHandler(logging.NullHandler())
238
+
239
+ _audit_logger = AuditLogger(
240
+ logger=audit_py_logger,
241
+ redaction_engine=_redaction_engine,
242
+ service_name=_log_config.service_name,
243
+ sink=audit_sink,
244
+ signer=signer,
245
+ )
246
+
247
+ if _log_config.integrity_enabled:
248
+ audit_path = _log_config.audit_log_file or "logs/audit.log.jsonl"
249
+ _integrity_verifier = AuditIntegrityVerifier(
250
+ audit_log_path=audit_path,
251
+ integrity_log_path=_log_config.integrity_log_file,
252
+ interval_seconds=_log_config.integrity_interval_seconds,
253
+ hash_algorithm=_log_config.integrity_hash_algorithm,
254
+ service_name=_log_config.service_name,
255
+ service_instance=_log_config.service_instance,
256
+ environment=_log_config.environment,
257
+ )
258
+ _integrity_verifier.start()
259
+
260
+ for logger_name, level_str in _log_config.level_overrides.items():
261
+ override_level = getattr(logging, level_str.upper(), None)
262
+ if override_level is not None:
263
+ logging.getLogger(logger_name).setLevel(override_level)
264
+
265
+ _is_configured = True
266
+
267
+
268
+ def _resolve_handler_selection(log_config: LogConfig) -> set[str]:
269
+ """Resolve the set of active handler kinds from declarative + legacy knobs.
270
+
271
+ Behaviour matrix:
272
+
273
+ - If ``log_config.handlers`` is ``None`` (the legacy default) the return
274
+ preserves bit-for-bit the prior behaviour: ``{"file"}`` if
275
+ ``app_log_file`` is set, ``{"console"}`` if only ``console_output`` is
276
+ true, ``{"file", "console"}`` for the dual case, ``set()`` otherwise.
277
+ - If ``log_config.handlers`` is supplied the listed :class:`HandlerType`
278
+ members drive the selection. ``DUAL`` expands to ``{"file", "console"}``
279
+ and ``ROTATING`` is treated as an alias of ``FILE`` (the platform's
280
+ only file handler is already rotating). The legacy knobs still narrow
281
+ the selection so callers cannot ask for ``FILE`` without
282
+ ``app_log_file`` set or ``CONSOLE`` with ``console_output=False``.
283
+ """
284
+ legacy: set[str] = set()
285
+ if log_config.app_log_file:
286
+ legacy.add("file")
287
+ if log_config.console_output:
288
+ legacy.add("console")
289
+
290
+ if not log_config.handlers:
291
+ return legacy
292
+
293
+ declarative: set[str] = set()
294
+ for raw in log_config.handlers:
295
+ try:
296
+ kind = HandlerType.coerce(raw)
297
+ except ValueError:
298
+ continue
299
+ if kind in (HandlerType.FILE, HandlerType.ROTATING):
300
+ declarative.add("file")
301
+ elif kind is HandlerType.CONSOLE:
302
+ declarative.add("console")
303
+ elif kind is HandlerType.DUAL:
304
+ declarative.add("file")
305
+ declarative.add("console")
306
+
307
+ # Honour legacy guard: cannot emit to file without app_log_file configured.
308
+ if "file" in declarative and not log_config.app_log_file:
309
+ declarative.discard("file")
310
+ if "console" in declarative and not log_config.console_output:
311
+ # The declarative request wins for console — when the caller asked
312
+ # for CONSOLE explicitly, force-enable console output.
313
+ log_config.console_output = True
314
+
315
+ return declarative
316
+
317
+
318
+ def get_logger(name: str, pii_redaction: bool = True) -> AppLogger:
319
+ """Get a configured application logger for the given module name."""
320
+ py_logger = logging.getLogger(name)
321
+
322
+ redaction = _redaction_engine
323
+ if redaction is None:
324
+ redaction = RedactionEngine(pii_enabled=pii_redaction)
325
+
326
+ return AppLogger(logger=py_logger, redaction_engine=redaction)
327
+
328
+
329
+ def get_audit_logger() -> AuditLogger:
330
+ """Get the singleton audit logger for security events.
331
+
332
+ If ``setup_logging()`` has not been called yet, this performs a default
333
+ initialisation so that a ``FileSink`` writing to ``logs/audit.log.jsonl``
334
+ is available instead of the legacy NullHandler fallback.
335
+ """
336
+ global _audit_logger
337
+ if _audit_logger is None:
338
+ if not _is_configured:
339
+ setup_logging(None)
340
+ else:
341
+ py_logger = logging.getLogger("cloud_dog_logging.audit")
342
+ if not py_logger.handlers:
343
+ py_logger.addHandler(logging.NullHandler())
344
+ _audit_logger = AuditLogger(logger=py_logger)
345
+ return _audit_logger
346
+
347
+
348
+ def get_integrity_verifier() -> AuditIntegrityVerifier | None:
349
+ """Get the audit integrity verifier when enabled."""
350
+ return _integrity_verifier
351
+
352
+
353
+ def _on_audit_rotation(_meta: dict[str, object]) -> None:
354
+ verifier = get_integrity_verifier()
355
+ if verifier is not None:
356
+ verifier.compute_now(trigger="rotation")
357
+
358
+
359
+ def _build_audit_sink(
360
+ log_config: LogConfig,
361
+ on_audit_rotate: Callable[[dict[str, object]], None] | None = None,
362
+ ) -> AuditSink:
363
+ sinks: list[AuditSink] = []
364
+ audit_path = log_config.audit_log_file or "logs/audit.log.jsonl"
365
+ sinks.append(
366
+ FileSink(
367
+ audit_path,
368
+ max_bytes=log_config.rotation_max_bytes,
369
+ backup_count=log_config.rotation_backup_count,
370
+ rotation_mode=log_config.rotation_mode,
371
+ when=log_config.rotation_when,
372
+ interval=log_config.rotation_interval,
373
+ compress=log_config.rotation_compress,
374
+ on_rotate=on_audit_rotate,
375
+ )
376
+ )
377
+ if log_config.console_output:
378
+ sinks.append(StdoutSink())
379
+
380
+ if len(sinks) == 1:
381
+ return sinks[0]
382
+ return FanOutSink(sinks)
383
+
384
+
385
+ def _build_signer(log_config: LogConfig) -> HMACSigner | None:
386
+ if not log_config.audit_signing_enabled:
387
+ return None
388
+ return HMACSigner(log_config.audit_signing_key or "")
389
+
390
+
391
+ def _resolve_redaction_presets(config: dict[str, Any] | Any | None, log_config: LogConfig) -> list[RedactionPreset]:
392
+ if isinstance(config, dict):
393
+ return load_presets(config)
394
+ return load_presets({"log": {"redaction": {"presets": log_config.redaction_presets}}})
395
+
396
+
397
+ def _shutdown_integrity_verifier() -> None:
398
+ verifier = get_integrity_verifier()
399
+ if verifier is not None:
400
+ try:
401
+ verifier.stop()
402
+ except Exception:
403
+ pass
404
+
405
+
406
+ atexit.register(_shutdown_integrity_verifier)
@@ -0,0 +1,143 @@
1
+ # Copyright 2026 Cloud-Dog, Viewdeck Engineering Limited
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ # cloud_dog_logging — Application logger (structured JSON, levels)
16
+ #
17
+ # Licence: Proprietary — Cloud-Dog AI Platform
18
+ # Owner: Cloud-Dog AI
19
+ # Description: Structured application logger with automatic correlation ID
20
+ # injection, secret redaction on extra fields, and configurable formatting.
21
+ # Related requirements: FR1.4, FR1.6, FR1.9, FR1.13
22
+ # Related architecture: CC1.1
23
+
24
+ """Structured application logger for cloud_dog_logging."""
25
+
26
+ from __future__ import annotations
27
+
28
+ import logging
29
+ import sys
30
+ from typing import Any
31
+
32
+ from cloud_dog_logging.exceptions import format_exception
33
+ from cloud_dog_logging.redaction import RedactionEngine
34
+
35
+
36
+ class AppLogger:
37
+ """Structured application logger with redaction support.
38
+
39
+ Wraps a standard Python logger and applies secret redaction to all
40
+ extra fields before logging. Automatically includes correlation ID
41
+ via the configured formatter.
42
+
43
+ Args:
44
+ logger: The underlying Python logger.
45
+ redaction_engine: Redaction engine for extra fields. If None,
46
+ a default engine is created.
47
+
48
+ Related tests: UT1.7_AppLogger
49
+ """
50
+
51
+ def __init__(
52
+ self,
53
+ logger: logging.Logger,
54
+ redaction_engine: RedactionEngine | None = None,
55
+ ) -> None:
56
+ self._logger = logger
57
+ self._redaction = redaction_engine or RedactionEngine()
58
+
59
+ def _redact_extra(self, extra: dict[str, Any] | None) -> dict[str, Any]:
60
+ """Redact sensitive values from extra fields.
61
+
62
+ Args:
63
+ extra: The extra fields dictionary.
64
+
65
+ Returns:
66
+ A redacted copy of the extra fields.
67
+ """
68
+ if not extra:
69
+ return {}
70
+ return self._redaction.redact(extra)
71
+
72
+ def debug(self, msg: str, **extra: Any) -> None:
73
+ """Log a DEBUG-level message.
74
+
75
+ Args:
76
+ msg: The log message.
77
+ **extra: Additional context fields (will be redacted).
78
+ """
79
+ self._logger.debug(msg, extra=self._redact_extra(extra))
80
+
81
+ def info(self, msg: str, **extra: Any) -> None:
82
+ """Log an INFO-level message.
83
+
84
+ Args:
85
+ msg: The log message.
86
+ **extra: Additional context fields (will be redacted).
87
+ """
88
+ self._logger.info(msg, extra=self._redact_extra(extra))
89
+
90
+ def warning(self, msg: str, **extra: Any) -> None:
91
+ """Log a WARNING-level message.
92
+
93
+ Args:
94
+ msg: The log message.
95
+ **extra: Additional context fields (will be redacted).
96
+ """
97
+ self._logger.warning(msg, extra=self._redact_extra(extra))
98
+
99
+ def error(self, msg: str, **extra: Any) -> None:
100
+ """Log an ERROR-level message.
101
+
102
+ Args:
103
+ msg: The log message.
104
+ **extra: Additional context fields (will be redacted).
105
+ """
106
+ self._logger.error(msg, extra=self._redact_extra(extra))
107
+
108
+ def critical(self, msg: str, **extra: Any) -> None:
109
+ """Log a CRITICAL-level message.
110
+
111
+ Args:
112
+ msg: The log message.
113
+ **extra: Additional context fields (will be redacted).
114
+ """
115
+ self._logger.critical(msg, extra=self._redact_extra(extra))
116
+
117
+ def exception(self, msg: str, **extra: Any) -> None:
118
+ """Log an ERROR-level message with exception information.
119
+
120
+ Args:
121
+ msg: The log message.
122
+ **extra: Additional context fields (will be redacted).
123
+ """
124
+ _, exc_value, _ = sys.exc_info()
125
+ payload = dict(extra)
126
+ if isinstance(exc_value, BaseException):
127
+ payload["exception"] = format_exception(exc_value)
128
+ self._logger.error(msg, exc_info=True, extra=self._redact_extra(payload))
129
+
130
+ @property
131
+ def level(self) -> int:
132
+ """Return the effective log level."""
133
+ return self._logger.getEffectiveLevel()
134
+
135
+ @property
136
+ def name(self) -> str:
137
+ """Return the logger name."""
138
+ return self._logger.name
139
+
140
+ @property
141
+ def underlying_logger(self) -> logging.Logger:
142
+ """Return the underlying stdlib logger for advanced use."""
143
+ return self._logger