elven-logs-interceptor-python 0.1.3__tar.gz → 0.1.4__tar.gz

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 (90) hide show
  1. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/PKG-INFO +1 -1
  2. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/pyproject.toml +1 -1
  3. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/application/log_service.py +3 -3
  4. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/interceptors/runtime_interceptor.py +22 -0
  5. elven_logs_interceptor_python-0.1.4/src/logs_interceptor/integrations/loguru.py +93 -0
  6. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/presentation/factory.py +3 -3
  7. elven_logs_interceptor_python-0.1.4/tests/unit/test_loguru_sink.py +114 -0
  8. elven_logs_interceptor_python-0.1.4/tests/unit/test_runtime_interceptor.py +53 -0
  9. elven_logs_interceptor_python-0.1.3/src/logs_interceptor/integrations/loguru.py +0 -36
  10. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/.github/workflows/ci.yml +0 -0
  11. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/.github/workflows/publish.yml +0 -0
  12. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/.gitignore +0 -0
  13. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/ARCHITECTURE.md +0 -0
  14. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/Makefile +0 -0
  15. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/README.md +0 -0
  16. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/examples/basic_app.py +0 -0
  17. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/examples/fastapi_integration.py +0 -0
  18. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/examples/full_config_reference.py +0 -0
  19. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/examples/high_volume.py +0 -0
  20. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/examples/tracking_usage.py +0 -0
  21. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/scripts/publish.sh +0 -0
  22. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/__init__.py +0 -0
  23. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/application/__init__.py +0 -0
  24. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/application/config_service.py +0 -0
  25. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/config.py +0 -0
  26. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/domain/__init__.py +0 -0
  27. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/domain/entities.py +0 -0
  28. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/domain/interfaces.py +0 -0
  29. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/domain/value_objects.py +0 -0
  30. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/__init__.py +0 -0
  31. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/buffer/__init__.py +0 -0
  32. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/buffer/memory_buffer.py +0 -0
  33. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/circuit_breaker/__init__.py +0 -0
  34. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/circuit_breaker/circuit_breaker.py +0 -0
  35. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/compression/__init__.py +0 -0
  36. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/compression/base.py +0 -0
  37. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/compression/brotli_compressor.py +0 -0
  38. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/compression/factory.py +0 -0
  39. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/compression/gzip_compressor.py +0 -0
  40. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/compression/noop_compressor.py +0 -0
  41. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/context/__init__.py +0 -0
  42. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/context/context_provider.py +0 -0
  43. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/dlq/__init__.py +0 -0
  44. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/dlq/file_dlq.py +0 -0
  45. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/dlq/memory_dlq.py +0 -0
  46. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/filter/__init__.py +0 -0
  47. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/filter/log_filter.py +0 -0
  48. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/interceptors/__init__.py +0 -0
  49. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/memory/__init__.py +0 -0
  50. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/memory/memory_tracker.py +0 -0
  51. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/metrics/__init__.py +0 -0
  52. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/metrics/metrics_collector.py +0 -0
  53. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/transport/__init__.py +0 -0
  54. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/transport/loki_json_transport.py +0 -0
  55. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/transport/loki_protobuf_transport.py +0 -0
  56. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/transport/resilient_transport.py +0 -0
  57. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/transport/transport_factory.py +0 -0
  58. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/workers/__init__.py +0 -0
  59. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/workers/worker_pool.py +0 -0
  60. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/integrations/__init__.py +0 -0
  61. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/integrations/celery.py +0 -0
  62. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/integrations/django.py +0 -0
  63. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/integrations/fastapi.py +0 -0
  64. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/integrations/flask.py +0 -0
  65. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/integrations/logging_handler.py +0 -0
  66. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/integrations/structlog.py +0 -0
  67. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/preload.py +0 -0
  68. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/presentation/__init__.py +0 -0
  69. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/types.py +0 -0
  70. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/utils.py +0 -0
  71. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/test-apps/elven-live-demo/.env.example +0 -0
  72. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/test-apps/elven-live-demo/README.md +0 -0
  73. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/test-apps/elven-live-demo/app.py +0 -0
  74. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/test-apps/elven-live-demo/run.sh +0 -0
  75. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/test-apps/elven-observability-smoke/.env.example +0 -0
  76. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/test-apps/elven-observability-smoke/README.md +0 -0
  77. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/test-apps/elven-observability-smoke/app.py +0 -0
  78. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/test-apps/elven-observability-smoke/run.sh +0 -0
  79. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/tests/integration/test_api.py +0 -0
  80. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/tests/unit/test_circuit_breaker_extra.py +0 -0
  81. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/tests/unit/test_config_service.py +0 -0
  82. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/tests/unit/test_core_components.py +0 -0
  83. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/tests/unit/test_env_config.py +0 -0
  84. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/tests/unit/test_log_filter_extra.py +0 -0
  85. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/tests/unit/test_log_service_unit.py +0 -0
  86. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/tests/unit/test_loki_json_transport.py +0 -0
  87. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/tests/unit/test_memory_buffer_extra.py +0 -0
  88. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/tests/unit/test_protobuf_transport_safety.py +0 -0
  89. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/tests/unit/test_resilient_transport.py +0 -0
  90. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.4}/tests/unit/test_utils_extra.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: elven-logs-interceptor-python
3
- Version: 0.1.3
3
+ Version: 0.1.4
4
4
  Summary: Production-grade logs interceptor for Python with Loki transport, resilience, batching, and framework integrations.
5
5
  Author: Elven Observability
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "elven-logs-interceptor-python"
7
- version = "0.1.3"
7
+ version = "0.1.4"
8
8
  description = "Production-grade logs interceptor for Python with Loki transport, resilience, batching, and framework integrations."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -22,11 +22,11 @@ try:
22
22
  except Exception: # pragma: no cover
23
23
  pass
24
24
 
25
+ otel_trace: Any | None
25
26
  try:
26
- from opentelemetry import trace as _otel_trace # type: ignore[import-not-found]
27
+ from opentelemetry import trace as otel_trace
27
28
  except Exception: # pragma: no cover - optional dependency
28
- _otel_trace = None
29
- otel_trace: Any = _otel_trace
29
+ otel_trace = None
30
30
 
31
31
 
32
32
  @dataclass(slots=True)
@@ -11,6 +11,18 @@ from ...domain.interfaces import ILogger
11
11
  from ...types import LogLevel
12
12
  from ...utils import safe_stringify
13
13
 
14
+ _IGNORED_LOGGER_PREFIXES = (
15
+ "httpcore",
16
+ "httpx",
17
+ "urllib3",
18
+ "hpack",
19
+ "h2",
20
+ "h11",
21
+ "opentelemetry",
22
+ "logs_interceptor",
23
+ "elven_unified_observability",
24
+ )
25
+
14
26
 
15
27
  class _BridgeLoggingHandler(logging.Handler):
16
28
  def __init__(self, logger: ILogger) -> None:
@@ -19,6 +31,9 @@ class _BridgeLoggingHandler(logging.Handler):
19
31
 
20
32
  def emit(self, record: logging.LogRecord) -> None:
21
33
  try:
34
+ if record.name.startswith(_IGNORED_LOGGER_PREFIXES):
35
+ return
36
+
22
37
  level = self._map_level(record.levelno)
23
38
  msg = record.getMessage()
24
39
  context = {
@@ -56,12 +71,17 @@ class RuntimeInterceptor:
56
71
  self._original_print = builtins.print
57
72
  self._original_excepthook = sys.excepthook
58
73
  self._root_logger = logging.getLogger()
74
+ self._original_root_level = self._root_logger.level
59
75
  self._bridge_handler = _BridgeLoggingHandler(logger)
60
76
  self._original_handlers: list[logging.Handler] = []
61
77
 
62
78
  def enable(self) -> None:
63
79
  if self._enabled:
64
80
  return
81
+
82
+ self._original_print = builtins.print
83
+ self._original_excepthook = sys.excepthook
84
+ self._original_root_level = self._root_logger.level
65
85
  self._enabled = True
66
86
 
67
87
  self._patch_print()
@@ -80,6 +100,8 @@ class RuntimeInterceptor:
80
100
  except ValueError:
81
101
  pass
82
102
 
103
+ self._root_logger.setLevel(self._original_root_level)
104
+
83
105
  if not self._preserve_original:
84
106
  self._root_logger.handlers = self._original_handlers
85
107
 
@@ -0,0 +1,93 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from typing import Any
5
+
6
+ from ..domain.interfaces import ILogger
7
+
8
+
9
+ class LoguruSink:
10
+ def __init__(self, logger: ILogger, exclude_prefixes: list[str] | None = None) -> None:
11
+ self.logger = logger
12
+ self._exclude_prefixes = tuple(
13
+ prefix.lower().strip()
14
+ for prefix in (exclude_prefixes or [])
15
+ if prefix and prefix.strip()
16
+ )
17
+
18
+ def __call__(self, message: Any) -> None:
19
+ try:
20
+ record = getattr(message, "record", None)
21
+ if not isinstance(record, Mapping):
22
+ self.logger.info(str(message), {"source": "loguru"})
23
+ return
24
+
25
+ if self._should_ignore(record):
26
+ return
27
+
28
+ level = self._resolve_level(record)
29
+ extra = record.get("extra")
30
+ normalized_extra = dict(extra) if isinstance(extra, Mapping) else extra
31
+
32
+ self.logger.log(
33
+ level, # type: ignore[arg-type]
34
+ str(record.get("message", "")),
35
+ {
36
+ "source": "loguru",
37
+ "logger_name": record.get("name"),
38
+ "module": record.get("module"),
39
+ "function": record.get("function"),
40
+ "line": record.get("line"),
41
+ "extra": normalized_extra,
42
+ },
43
+ )
44
+ except Exception:
45
+ return
46
+
47
+ @staticmethod
48
+ def _resolve_level(record: Mapping[str, Any]) -> str:
49
+ raw_level = record.get("level")
50
+ if isinstance(raw_level, Mapping):
51
+ level = str(raw_level.get("name", "INFO")).lower()
52
+ level_number = raw_level.get("no")
53
+ else:
54
+ level = str(getattr(raw_level, "name", "INFO")).lower()
55
+ level_number = getattr(raw_level, "no", None)
56
+
57
+ if level == "warning":
58
+ return "warn"
59
+ if level == "critical":
60
+ return "fatal"
61
+ if level not in {"debug", "info", "warn", "error", "fatal"}:
62
+ if not level and isinstance(level_number, (int, float)):
63
+ if level_number >= 50:
64
+ return "fatal"
65
+ if level_number >= 40:
66
+ return "error"
67
+ if level_number >= 30:
68
+ return "warn"
69
+ if level_number >= 20:
70
+ return "info"
71
+ return "debug"
72
+ return "info"
73
+ return level
74
+
75
+ def _should_ignore(self, record: Mapping[str, Any]) -> bool:
76
+ if not self._exclude_prefixes:
77
+ return False
78
+
79
+ name = str(record.get("name") or "").lower()
80
+ module = str(record.get("module") or "").lower()
81
+ extra = record.get("extra")
82
+ extra_logger_name = ""
83
+ if isinstance(extra, Mapping):
84
+ extra_logger_name = str(extra.get("logger_name") or "").lower()
85
+
86
+ for prefix in self._exclude_prefixes:
87
+ if (
88
+ name.startswith(prefix)
89
+ or module.startswith(prefix)
90
+ or extra_logger_name.startswith(prefix)
91
+ ):
92
+ return True
93
+ return False
@@ -15,11 +15,11 @@ from ..infrastructure.filter import LogFilter
15
15
  from ..infrastructure.interceptors import RuntimeInterceptor
16
16
  from ..infrastructure.transport import TransportFactory
17
17
 
18
+ otel_trace: Any | None
18
19
  try:
19
- from opentelemetry import trace as _otel_trace # type: ignore[import-not-found]
20
+ from opentelemetry import trace as otel_trace
20
21
  except Exception: # pragma: no cover - optional dependency
21
- _otel_trace = None
22
- otel_trace: Any = _otel_trace
22
+ otel_trace = None
23
23
 
24
24
 
25
25
  @dataclass(slots=True)
@@ -0,0 +1,114 @@
1
+ from __future__ import annotations
2
+
3
+ from logs_interceptor.integrations.loguru import LoguruSink
4
+
5
+
6
+ class _Logger:
7
+ def __init__(self) -> None:
8
+ self.calls: list[tuple[str, str, dict[str, object] | None]] = []
9
+
10
+ def info(self, message, context=None):
11
+ self.calls.append(("info", message, context))
12
+
13
+ def log(self, level, message, context=None):
14
+ self.calls.append((level, message, context))
15
+
16
+
17
+ class _Message:
18
+ def __init__(self, record):
19
+ self.record = record
20
+
21
+
22
+ class _Level:
23
+ def __init__(self, name: str, no: int) -> None:
24
+ self.name = name
25
+ self.no = no
26
+
27
+
28
+ def test_loguru_sink_records_logger_name_and_metadata() -> None:
29
+ logger = _Logger()
30
+ sink = LoguruSink(logger)
31
+
32
+ sink(
33
+ _Message(
34
+ {
35
+ "name": "service.api",
36
+ "module": "app",
37
+ "function": "handler",
38
+ "line": 42,
39
+ "message": "hello",
40
+ "level": {"name": "INFO"},
41
+ "extra": {"request_id": "req-1"},
42
+ }
43
+ )
44
+ )
45
+
46
+ assert logger.calls == [
47
+ (
48
+ "info",
49
+ "hello",
50
+ {
51
+ "source": "loguru",
52
+ "logger_name": "service.api",
53
+ "module": "app",
54
+ "function": "handler",
55
+ "line": 42,
56
+ "extra": {"request_id": "req-1"},
57
+ },
58
+ )
59
+ ]
60
+
61
+
62
+ def test_loguru_sink_excludes_configured_prefixes() -> None:
63
+ logger = _Logger()
64
+ sink = LoguruSink(logger, exclude_prefixes=["httpx", "httpcore"])
65
+
66
+ sink(
67
+ _Message(
68
+ {
69
+ "name": "httpx",
70
+ "module": "client",
71
+ "function": "send",
72
+ "line": 1,
73
+ "message": "request",
74
+ "level": {"name": "DEBUG"},
75
+ "extra": {},
76
+ }
77
+ )
78
+ )
79
+
80
+ assert logger.calls == []
81
+
82
+
83
+ def test_loguru_sink_supports_loguru_record_level_objects() -> None:
84
+ logger = _Logger()
85
+ sink = LoguruSink(logger)
86
+
87
+ sink(
88
+ _Message(
89
+ {
90
+ "name": "service.api",
91
+ "module": "app",
92
+ "function": "handler",
93
+ "line": 42,
94
+ "message": "hello",
95
+ "level": _Level("WARNING", 30),
96
+ "extra": {"request_id": "req-1"},
97
+ }
98
+ )
99
+ )
100
+
101
+ assert logger.calls == [
102
+ (
103
+ "warn",
104
+ "hello",
105
+ {
106
+ "source": "loguru",
107
+ "logger_name": "service.api",
108
+ "module": "app",
109
+ "function": "handler",
110
+ "line": 42,
111
+ "extra": {"request_id": "req-1"},
112
+ },
113
+ )
114
+ ]
@@ -0,0 +1,53 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+
5
+ from logs_interceptor.infrastructure.interceptors.runtime_interceptor import RuntimeInterceptor
6
+
7
+
8
+ class _Logger:
9
+ def __init__(self) -> None:
10
+ self.records: list[tuple[str, str, dict[str, object] | None]] = []
11
+
12
+ def log(self, level, message, context=None):
13
+ self.records.append((level, message, context))
14
+
15
+ def info(self, message, context=None):
16
+ self.records.append(("info", message, context))
17
+
18
+ def fatal(self, message, context=None):
19
+ self.records.append(("fatal", message, context))
20
+
21
+ def flush(self):
22
+ return None
23
+
24
+
25
+ def test_runtime_interceptor_restores_root_logger_level() -> None:
26
+ root = logging.getLogger()
27
+ original_level = root.level
28
+ root.setLevel(logging.WARNING)
29
+
30
+ interceptor = RuntimeInterceptor(_Logger())
31
+ interceptor.enable()
32
+
33
+ assert root.level == logging.DEBUG
34
+
35
+ interceptor.restore()
36
+
37
+ assert root.level == logging.WARNING
38
+ root.setLevel(original_level)
39
+
40
+
41
+ def test_runtime_interceptor_ignores_known_noisy_logger_prefixes() -> None:
42
+ logger = _Logger()
43
+ interceptor = RuntimeInterceptor(logger)
44
+ interceptor.enable()
45
+
46
+ noisy_logger = logging.getLogger("elven_unified_observability.runtime")
47
+ noisy_logger.warning("internal warning")
48
+ noisy_otel_logger = logging.getLogger("opentelemetry.sdk.trace")
49
+ noisy_otel_logger.info("trace noise")
50
+
51
+ assert logger.records == []
52
+
53
+ interceptor.restore()
@@ -1,36 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import Any
4
-
5
- from ..domain.interfaces import ILogger
6
-
7
-
8
- class LoguruSink:
9
- def __init__(self, logger: ILogger) -> None:
10
- self.logger = logger
11
-
12
- def __call__(self, message: Any) -> None:
13
- record = getattr(message, "record", None)
14
- if record is None:
15
- self.logger.info(str(message), {"source": "loguru"})
16
- return
17
-
18
- level = str(record.get("level", {}).get("name", "INFO")).lower()
19
- if level == "warning":
20
- level = "warn"
21
- if level == "critical":
22
- level = "fatal"
23
- if level not in {"debug", "info", "warn", "error", "fatal"}:
24
- level = "info"
25
-
26
- self.logger.log(
27
- level, # type: ignore[arg-type]
28
- str(record.get("message", "")),
29
- {
30
- "source": "loguru",
31
- "module": record.get("module"),
32
- "function": record.get("function"),
33
- "line": record.get("line"),
34
- "extra": record.get("extra"),
35
- },
36
- )