elven-logs-interceptor-python 0.1.3__tar.gz → 0.1.5__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 (94) hide show
  1. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/PKG-INFO +1 -1
  2. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/pyproject.toml +1 -1
  3. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/__init__.py +5 -0
  4. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/application/config_service.py +4 -0
  5. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/application/log_service.py +3 -3
  6. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/config.py +2 -0
  7. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/interceptors/runtime_interceptor.py +33 -7
  8. elven_logs_interceptor_python-0.1.5/src/logs_interceptor/infrastructure/internal_capture_guard.py +23 -0
  9. elven_logs_interceptor_python-0.1.5/src/logs_interceptor/infrastructure/log_noise_filter.py +56 -0
  10. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/transport/loki_json_transport.py +8 -2
  11. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/transport/loki_protobuf_transport.py +8 -2
  12. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/integrations/logging_handler.py +15 -1
  13. elven_logs_interceptor_python-0.1.5/src/logs_interceptor/integrations/loguru.py +85 -0
  14. elven_logs_interceptor_python-0.1.5/src/logs_interceptor/integrations/structlog.py +38 -0
  15. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/preload.py +13 -2
  16. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/presentation/factory.py +8 -4
  17. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/utils.py +20 -0
  18. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/tests/integration/test_api.py +20 -0
  19. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/tests/unit/test_config_service.py +14 -0
  20. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/tests/unit/test_env_config.py +12 -0
  21. elven_logs_interceptor_python-0.1.5/tests/unit/test_integration_filters.py +86 -0
  22. elven_logs_interceptor_python-0.1.5/tests/unit/test_loguru_sink.py +137 -0
  23. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/tests/unit/test_loki_json_transport.py +21 -0
  24. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/tests/unit/test_protobuf_transport_safety.py +48 -0
  25. elven_logs_interceptor_python-0.1.5/tests/unit/test_runtime_interceptor.py +67 -0
  26. elven_logs_interceptor_python-0.1.3/src/logs_interceptor/integrations/loguru.py +0 -36
  27. elven_logs_interceptor_python-0.1.3/src/logs_interceptor/integrations/structlog.py +0 -21
  28. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/.github/workflows/ci.yml +0 -0
  29. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/.github/workflows/publish.yml +0 -0
  30. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/.gitignore +0 -0
  31. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/ARCHITECTURE.md +0 -0
  32. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/Makefile +0 -0
  33. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/README.md +0 -0
  34. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/examples/basic_app.py +0 -0
  35. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/examples/fastapi_integration.py +0 -0
  36. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/examples/full_config_reference.py +0 -0
  37. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/examples/high_volume.py +0 -0
  38. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/examples/tracking_usage.py +0 -0
  39. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/scripts/publish.sh +0 -0
  40. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/application/__init__.py +0 -0
  41. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/domain/__init__.py +0 -0
  42. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/domain/entities.py +0 -0
  43. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/domain/interfaces.py +0 -0
  44. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/domain/value_objects.py +0 -0
  45. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/__init__.py +0 -0
  46. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/buffer/__init__.py +0 -0
  47. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/buffer/memory_buffer.py +0 -0
  48. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/circuit_breaker/__init__.py +0 -0
  49. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/circuit_breaker/circuit_breaker.py +0 -0
  50. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/compression/__init__.py +0 -0
  51. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/compression/base.py +0 -0
  52. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/compression/brotli_compressor.py +0 -0
  53. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/compression/factory.py +0 -0
  54. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/compression/gzip_compressor.py +0 -0
  55. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/compression/noop_compressor.py +0 -0
  56. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/context/__init__.py +0 -0
  57. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/context/context_provider.py +0 -0
  58. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/dlq/__init__.py +0 -0
  59. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/dlq/file_dlq.py +0 -0
  60. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/dlq/memory_dlq.py +0 -0
  61. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/filter/__init__.py +0 -0
  62. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/filter/log_filter.py +0 -0
  63. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/interceptors/__init__.py +0 -0
  64. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/memory/__init__.py +0 -0
  65. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/memory/memory_tracker.py +0 -0
  66. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/metrics/__init__.py +0 -0
  67. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/metrics/metrics_collector.py +0 -0
  68. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/transport/__init__.py +0 -0
  69. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/transport/resilient_transport.py +0 -0
  70. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/transport/transport_factory.py +0 -0
  71. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/workers/__init__.py +0 -0
  72. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/workers/worker_pool.py +0 -0
  73. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/integrations/__init__.py +0 -0
  74. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/integrations/celery.py +0 -0
  75. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/integrations/django.py +0 -0
  76. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/integrations/fastapi.py +0 -0
  77. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/integrations/flask.py +0 -0
  78. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/presentation/__init__.py +0 -0
  79. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/types.py +0 -0
  80. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/test-apps/elven-live-demo/.env.example +0 -0
  81. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/test-apps/elven-live-demo/README.md +0 -0
  82. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/test-apps/elven-live-demo/app.py +0 -0
  83. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/test-apps/elven-live-demo/run.sh +0 -0
  84. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/test-apps/elven-observability-smoke/.env.example +0 -0
  85. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/test-apps/elven-observability-smoke/README.md +0 -0
  86. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/test-apps/elven-observability-smoke/app.py +0 -0
  87. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/test-apps/elven-observability-smoke/run.sh +0 -0
  88. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/tests/unit/test_circuit_breaker_extra.py +0 -0
  89. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/tests/unit/test_core_components.py +0 -0
  90. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/tests/unit/test_log_filter_extra.py +0 -0
  91. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/tests/unit/test_log_service_unit.py +0 -0
  92. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/tests/unit/test_memory_buffer_extra.py +0 -0
  93. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/tests/unit/test_resilient_transport.py +0 -0
  94. {elven_logs_interceptor_python-0.1.3 → elven_logs_interceptor_python-0.1.5}/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.5
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.5"
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"
@@ -133,6 +133,11 @@ def _coerce_config(user_config: LogsInterceptorConfig | dict[str, Any] | None) -
133
133
  max_message_length=_pick(filter_raw, "max_message_length", "maxMessageLength"),
134
134
  sanitize=_pick(filter_raw, "sanitize", "sanitize"),
135
135
  sensitive_patterns=_pick(filter_raw, "sensitive_patterns", "sensitivePatterns"),
136
+ exclude_logger_prefixes=_pick(
137
+ filter_raw,
138
+ "exclude_logger_prefixes",
139
+ "excludeLoggerPrefixes",
140
+ ),
136
141
  )
137
142
  if filter_raw
138
143
  else None
@@ -17,6 +17,7 @@ from ..config import (
17
17
  ResolvedTransportConfig,
18
18
  TransportConfig,
19
19
  )
20
+ from ..infrastructure.log_noise_filter import normalize_excluded_logger_prefixes
20
21
  from ..types import LogLevel
21
22
 
22
23
  DEFAULT_LEVELS: list[LogLevel] = ["debug", "info", "warn", "error", "fatal"]
@@ -194,6 +195,9 @@ class ConfigService:
194
195
  else source.max_message_length,
195
196
  sanitize=True if source.sanitize is None else source.sanitize,
196
197
  sensitive_patterns=source.sensitive_patterns or DEFAULT_SENSITIVE_PATTERNS,
198
+ exclude_logger_prefixes=list(
199
+ normalize_excluded_logger_prefixes(source.exclude_logger_prefixes)
200
+ ),
197
201
  )
198
202
 
199
203
  @staticmethod
@@ -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)
@@ -45,6 +45,7 @@ class FilterConfig:
45
45
  max_message_length: int | None = None
46
46
  sanitize: bool | None = None
47
47
  sensitive_patterns: list[str] | None = None
48
+ exclude_logger_prefixes: list[str] | None = None
48
49
 
49
50
 
50
51
  @dataclass(slots=True)
@@ -149,6 +150,7 @@ class ResolvedFilterConfig:
149
150
  max_message_length: int
150
151
  sanitize: bool
151
152
  sensitive_patterns: list[str]
153
+ exclude_logger_prefixes: list[str] = field(default_factory=list)
152
154
 
153
155
 
154
156
  @dataclass(slots=True)
@@ -10,15 +10,27 @@ from typing import Any
10
10
  from ...domain.interfaces import ILogger
11
11
  from ...types import LogLevel
12
12
  from ...utils import safe_stringify
13
+ from ..internal_capture_guard import is_internal_log_capture_suppressed
14
+ from ..log_noise_filter import normalize_excluded_logger_prefixes, should_drop_log_record
13
15
 
14
16
 
15
17
  class _BridgeLoggingHandler(logging.Handler):
16
- def __init__(self, logger: ILogger) -> None:
18
+ def __init__(self, logger: ILogger, exclude_prefixes: list[str] | None = None) -> None:
17
19
  super().__init__()
18
20
  self._logger = logger
21
+ self._exclude_prefixes = normalize_excluded_logger_prefixes(exclude_prefixes)
19
22
 
20
23
  def emit(self, record: logging.LogRecord) -> None:
21
24
  try:
25
+ if is_internal_log_capture_suppressed():
26
+ return
27
+ if should_drop_log_record(
28
+ logger_name=record.name,
29
+ module_name=record.module,
30
+ exclude_prefixes=self._exclude_prefixes,
31
+ ):
32
+ return
33
+
22
34
  level = self._map_level(record.levelno)
23
35
  msg = record.getMessage()
24
36
  context = {
@@ -48,20 +60,31 @@ class _BridgeLoggingHandler(logging.Handler):
48
60
 
49
61
 
50
62
  class RuntimeInterceptor:
51
- def __init__(self, logger: ILogger, preserve_original: bool = True) -> None:
63
+ def __init__(
64
+ self,
65
+ logger: ILogger,
66
+ preserve_original: bool = True,
67
+ exclude_prefixes: list[str] | None = None,
68
+ ) -> None:
52
69
  self._logger = logger
53
70
  self._preserve_original = preserve_original
54
71
  self._enabled = False
72
+ self._exclude_prefixes = list(normalize_excluded_logger_prefixes(exclude_prefixes))
55
73
 
56
74
  self._original_print = builtins.print
57
75
  self._original_excepthook = sys.excepthook
58
76
  self._root_logger = logging.getLogger()
59
- self._bridge_handler = _BridgeLoggingHandler(logger)
77
+ self._original_root_level = self._root_logger.level
78
+ self._bridge_handler = _BridgeLoggingHandler(logger, self._exclude_prefixes)
60
79
  self._original_handlers: list[logging.Handler] = []
61
80
 
62
81
  def enable(self) -> None:
63
82
  if self._enabled:
64
83
  return
84
+
85
+ self._original_print = builtins.print
86
+ self._original_excepthook = sys.excepthook
87
+ self._original_root_level = self._root_logger.level
65
88
  self._enabled = True
66
89
 
67
90
  self._patch_print()
@@ -80,6 +103,8 @@ class RuntimeInterceptor:
80
103
  except ValueError:
81
104
  pass
82
105
 
106
+ self._root_logger.setLevel(self._original_root_level)
107
+
83
108
  if not self._preserve_original:
84
109
  self._root_logger.handlers = self._original_handlers
85
110
 
@@ -91,10 +116,11 @@ class RuntimeInterceptor:
91
116
  def _patch_print(self) -> None:
92
117
  def intercepted_print(*args: Any, **kwargs: Any) -> None:
93
118
  try:
94
- message = " ".join(
95
- [arg if isinstance(arg, str) else safe_stringify(arg) for arg in args]
96
- )
97
- self._logger.info(message, {"source": "print"})
119
+ if not is_internal_log_capture_suppressed():
120
+ message = " ".join(
121
+ [arg if isinstance(arg, str) else safe_stringify(arg) for arg in args]
122
+ )
123
+ self._logger.info(message, {"source": "print"})
98
124
  except Exception:
99
125
  pass
100
126
 
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterator
4
+ from contextlib import contextmanager
5
+ from contextvars import ContextVar
6
+
7
+ _SUPPRESSION_DEPTH: ContextVar[int] = ContextVar(
8
+ "logs_interceptor_internal_capture_suppression_depth",
9
+ default=0,
10
+ )
11
+
12
+
13
+ @contextmanager
14
+ def suppress_internal_log_capture() -> Iterator[None]:
15
+ token = _SUPPRESSION_DEPTH.set(_SUPPRESSION_DEPTH.get() + 1)
16
+ try:
17
+ yield
18
+ finally:
19
+ _SUPPRESSION_DEPTH.reset(token)
20
+
21
+
22
+ def is_internal_log_capture_suppressed() -> bool:
23
+ return _SUPPRESSION_DEPTH.get() > 0
@@ -0,0 +1,56 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable, Mapping
4
+ from typing import Any
5
+
6
+ DEFAULT_EXCLUDED_LOGGER_PREFIXES = (
7
+ "httpcore",
8
+ "httpx",
9
+ "urllib3",
10
+ "hpack",
11
+ "h2",
12
+ "h11",
13
+ "opentelemetry",
14
+ "logs_interceptor",
15
+ "elven_unified_observability",
16
+ )
17
+
18
+
19
+ def normalize_excluded_logger_prefixes(prefixes: Iterable[str] | None = None) -> tuple[str, ...]:
20
+ values = list(prefixes) if prefixes is not None else list(DEFAULT_EXCLUDED_LOGGER_PREFIXES)
21
+ normalized: list[str] = []
22
+ seen: set[str] = set()
23
+ for item in values:
24
+ value = str(item).strip().lower()
25
+ if not value or value in seen:
26
+ continue
27
+ seen.add(value)
28
+ normalized.append(value)
29
+ return tuple(normalized)
30
+
31
+
32
+ def should_drop_log_record(
33
+ *,
34
+ logger_name: str | None = None,
35
+ module_name: str | None = None,
36
+ extra: Mapping[str, Any] | None = None,
37
+ exclude_prefixes: Iterable[str] | None = None,
38
+ ) -> bool:
39
+ prefixes = normalize_excluded_logger_prefixes(exclude_prefixes)
40
+ if not prefixes:
41
+ return False
42
+
43
+ normalized_logger_name = str(logger_name or "").strip().lower()
44
+ normalized_module_name = str(module_name or "").strip().lower()
45
+ normalized_extra_logger_name = ""
46
+ if isinstance(extra, Mapping):
47
+ normalized_extra_logger_name = str(extra.get("logger_name") or "").strip().lower()
48
+
49
+ for prefix in prefixes:
50
+ if (
51
+ normalized_logger_name.startswith(prefix)
52
+ or normalized_module_name.startswith(prefix)
53
+ or normalized_extra_logger_name.startswith(prefix)
54
+ ):
55
+ return True
56
+ return False
@@ -12,7 +12,9 @@ import httpx
12
12
  from ...config import ResolvedTransportConfig
13
13
  from ...domain.entities import LogEntryEntity
14
14
  from ...types import TransportHealth, TransportMetrics
15
+ from ...utils import get_distribution_version
15
16
  from ..compression import CompressorConfig, CompressorFactory
17
+ from ..internal_capture_guard import suppress_internal_log_capture
16
18
 
17
19
  try:
18
20
  import orjson # type: ignore[import-not-found]
@@ -88,7 +90,10 @@ class LokiJsonTransport:
88
90
  headers: dict[str, str] = {
89
91
  "Content-Type": "application/json",
90
92
  "X-Scope-OrgID": self._config.tenant_id,
91
- "User-Agent": "elven-logs-interceptor-python/0.1.3",
93
+ "User-Agent": (
94
+ "elven-logs-interceptor-python/"
95
+ f"{get_distribution_version('elven-logs-interceptor-python')}"
96
+ ),
92
97
  **self._extra_headers,
93
98
  }
94
99
  if self._config.auth_token:
@@ -98,7 +103,8 @@ class LokiJsonTransport:
98
103
  if encoding:
99
104
  headers["Content-Encoding"] = encoding
100
105
 
101
- response = self._request(headers, body)
106
+ with suppress_internal_log_capture():
107
+ response = self._request(headers, body)
102
108
 
103
109
  if response.status_code >= 300:
104
110
  raise RetryableTransportError(
@@ -11,6 +11,8 @@ import httpx
11
11
  from ...config import ResolvedTransportConfig
12
12
  from ...domain.entities import LogEntryEntity
13
13
  from ...types import TransportHealth, TransportMetrics
14
+ from ...utils import get_distribution_version
15
+ from ..internal_capture_guard import suppress_internal_log_capture
14
16
 
15
17
  try:
16
18
  import snappy # type: ignore[import-not-found]
@@ -94,13 +96,17 @@ class LokiProtobufTransport:
94
96
  "Content-Type": "application/x-protobuf",
95
97
  "Content-Encoding": "snappy",
96
98
  "X-Scope-OrgID": self._config.tenant_id,
97
- "User-Agent": "elven-logs-interceptor-python/0.1.3",
99
+ "User-Agent": (
100
+ "elven-logs-interceptor-python/"
101
+ f"{get_distribution_version('elven-logs-interceptor-python')}"
102
+ ),
98
103
  **self._extra_headers,
99
104
  }
100
105
  if self._config.auth_token:
101
106
  headers["Authorization"] = f"Bearer {self._config.auth_token}"
102
107
 
103
- response = self._request(headers, compressed)
108
+ with suppress_internal_log_capture():
109
+ response = self._request(headers, compressed)
104
110
  if response.status_code >= 300:
105
111
  raise RetryableTransportError(
106
112
  message=f"Loki responded with {response.status_code}: {response.text}",
@@ -5,16 +5,30 @@ import traceback as traceback_module
5
5
  from typing import Any
6
6
 
7
7
  from ..domain.interfaces import ILogger
8
+ from ..infrastructure.internal_capture_guard import is_internal_log_capture_suppressed
9
+ from ..infrastructure.log_noise_filter import (
10
+ normalize_excluded_logger_prefixes,
11
+ should_drop_log_record,
12
+ )
8
13
  from ..types import LogLevel
9
14
 
10
15
 
11
16
  class LoggingHandler(logging.Handler):
12
- def __init__(self, logger: ILogger) -> None:
17
+ def __init__(self, logger: ILogger, exclude_prefixes: list[str] | None = None) -> None:
13
18
  super().__init__()
14
19
  self._logger = logger
20
+ self._exclude_prefixes = normalize_excluded_logger_prefixes(exclude_prefixes)
15
21
 
16
22
  def emit(self, record: logging.LogRecord) -> None:
17
23
  try:
24
+ if is_internal_log_capture_suppressed():
25
+ return
26
+ if should_drop_log_record(
27
+ logger_name=record.name,
28
+ module_name=record.module,
29
+ exclude_prefixes=self._exclude_prefixes,
30
+ ):
31
+ return
18
32
  level = self._map_level(record.levelno)
19
33
  message = record.getMessage()
20
34
  context: dict[str, Any] = {
@@ -0,0 +1,85 @@
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
+ from ..infrastructure.internal_capture_guard import is_internal_log_capture_suppressed
8
+ from ..infrastructure.log_noise_filter import (
9
+ normalize_excluded_logger_prefixes,
10
+ should_drop_log_record,
11
+ )
12
+
13
+
14
+ class LoguruSink:
15
+ def __init__(self, logger: ILogger, exclude_prefixes: list[str] | None = None) -> None:
16
+ self.logger = logger
17
+ self._exclude_prefixes = normalize_excluded_logger_prefixes(exclude_prefixes)
18
+
19
+ def __call__(self, message: Any) -> None:
20
+ try:
21
+ if is_internal_log_capture_suppressed():
22
+ return
23
+ record = getattr(message, "record", None)
24
+ if not isinstance(record, Mapping):
25
+ self.logger.info(str(message), {"source": "loguru"})
26
+ return
27
+
28
+ if self._should_ignore(record):
29
+ return
30
+
31
+ level = self._resolve_level(record)
32
+ extra = record.get("extra")
33
+ normalized_extra = dict(extra) if isinstance(extra, Mapping) else extra
34
+
35
+ self.logger.log(
36
+ level, # type: ignore[arg-type]
37
+ str(record.get("message", "")),
38
+ {
39
+ "source": "loguru",
40
+ "logger_name": record.get("name"),
41
+ "module": record.get("module"),
42
+ "function": record.get("function"),
43
+ "line": record.get("line"),
44
+ "extra": normalized_extra,
45
+ },
46
+ )
47
+ except Exception:
48
+ return
49
+
50
+ @staticmethod
51
+ def _resolve_level(record: Mapping[str, Any]) -> str:
52
+ raw_level = record.get("level")
53
+ if isinstance(raw_level, Mapping):
54
+ level = str(raw_level.get("name", "INFO")).lower()
55
+ level_number = raw_level.get("no")
56
+ else:
57
+ level = str(getattr(raw_level, "name", "INFO")).lower()
58
+ level_number = getattr(raw_level, "no", None)
59
+
60
+ if level == "warning":
61
+ return "warn"
62
+ if level == "critical":
63
+ return "fatal"
64
+ if level not in {"debug", "info", "warn", "error", "fatal"}:
65
+ if not level and isinstance(level_number, (int, float)):
66
+ if level_number >= 50:
67
+ return "fatal"
68
+ if level_number >= 40:
69
+ return "error"
70
+ if level_number >= 30:
71
+ return "warn"
72
+ if level_number >= 20:
73
+ return "info"
74
+ return "debug"
75
+ return "info"
76
+ return level
77
+
78
+ def _should_ignore(self, record: Mapping[str, Any]) -> bool:
79
+ extra = record.get("extra")
80
+ return should_drop_log_record(
81
+ logger_name=str(record.get("name") or ""),
82
+ module_name=str(record.get("module") or ""),
83
+ extra=extra if isinstance(extra, Mapping) else None,
84
+ exclude_prefixes=self._exclude_prefixes,
85
+ )
@@ -0,0 +1,38 @@
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
+ from ..infrastructure.internal_capture_guard import is_internal_log_capture_suppressed
8
+ from ..infrastructure.log_noise_filter import (
9
+ normalize_excluded_logger_prefixes,
10
+ should_drop_log_record,
11
+ )
12
+
13
+
14
+ class StructlogProcessor:
15
+ def __init__(self, logger: ILogger, exclude_prefixes: list[str] | None = None) -> None:
16
+ self.logger = logger
17
+ self._exclude_prefixes = normalize_excluded_logger_prefixes(exclude_prefixes)
18
+
19
+ def __call__(self, _logger: Any, method_name: str, event_dict: dict[str, Any]) -> dict[str, Any]:
20
+ if is_internal_log_capture_suppressed():
21
+ return event_dict
22
+ if should_drop_log_record(
23
+ logger_name=event_dict.get("logger_name"),
24
+ module_name=event_dict.get("module"),
25
+ extra=event_dict.get("extra") if isinstance(event_dict.get("extra"), Mapping) else None,
26
+ exclude_prefixes=self._exclude_prefixes,
27
+ ):
28
+ return event_dict
29
+
30
+ message = str(event_dict.get("event", method_name))
31
+ level = method_name.lower()
32
+ if level not in {"debug", "info", "warn", "error", "fatal"}:
33
+ level = "info"
34
+
35
+ context = dict(event_dict)
36
+ context.pop("event", None)
37
+ self.logger.log(level, message, {"source": "structlog", **context}) # type: ignore[arg-type]
38
+ return event_dict
@@ -8,14 +8,25 @@ import sys
8
8
  _LOGS_INTERCEPTOR_PRELOADED = False
9
9
 
10
10
 
11
+ def _write(stream: object, prefix: str, *args: object) -> None:
12
+ message = " ".join(str(arg) for arg in args)
13
+ text = f"{prefix} {message}\n"
14
+ writer = getattr(stream, "write", None)
15
+ flusher = getattr(stream, "flush", None)
16
+ if callable(writer):
17
+ writer(text)
18
+ if callable(flusher):
19
+ flusher()
20
+
21
+
11
22
  def _debug(*args: object) -> None:
12
23
  if os.getenv("LOGS_DEBUG") == "true" and os.getenv("LOGS_SILENT_ERRORS") != "true":
13
- print("[logs-interceptor:preload]", *args)
24
+ _write(sys.stdout, "[logs-interceptor:preload]", *args)
14
25
 
15
26
 
16
27
  def _error(*args: object) -> None:
17
28
  if os.getenv("LOGS_SILENT_ERRORS") != "true":
18
- print("[logs-interceptor:preload]", *args, file=sys.stderr)
29
+ _write(sys.stderr, "[logs-interceptor:preload]", *args)
19
30
 
20
31
 
21
32
  def _install() -> None:
@@ -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)
@@ -81,7 +81,11 @@ class LogsInterceptorFactory:
81
81
 
82
82
  runtime_interceptor = None
83
83
  if config.intercept_console:
84
- runtime_interceptor = RuntimeInterceptor(logger, config.preserve_original_console)
84
+ runtime_interceptor = RuntimeInterceptor(
85
+ logger,
86
+ config.preserve_original_console,
87
+ exclude_prefixes=config.filter.exclude_logger_prefixes,
88
+ )
85
89
  runtime_interceptor.enable()
86
90
 
87
91
  original_destroy = logger.destroy
@@ -10,6 +10,7 @@ import sys
10
10
  from collections.abc import Iterable
11
11
  from dataclasses import asdict, fields, is_dataclass
12
12
  from datetime import datetime
13
+ from functools import cache
13
14
  from typing import Any, cast
14
15
 
15
16
  from .config import (
@@ -80,6 +81,19 @@ def is_silent_errors_enabled() -> bool:
80
81
  return parse_bool(os.getenv("LOGS_SILENT_ERRORS"), False)
81
82
 
82
83
 
84
+ @cache
85
+ def get_distribution_version(distribution_name: str) -> str:
86
+ try:
87
+ from importlib.metadata import PackageNotFoundError, version
88
+ except ImportError: # pragma: no cover - Python 3.10+ should not hit this
89
+ return "unknown"
90
+
91
+ try:
92
+ return version(distribution_name)
93
+ except PackageNotFoundError:
94
+ return "unknown"
95
+
96
+
83
97
  def _internal_log(level: str, message: str, context: Any | None = None) -> None:
84
98
  if level == "debug" and not is_debug_enabled():
85
99
  return
@@ -396,6 +410,12 @@ def load_config_from_env() -> LogsInterceptorConfig:
396
410
  max_message_length=parse_int_range(
397
411
  env.get("LOGS_FILTER_MAX_MESSAGE_LENGTH"), 8192, 64, 1_000_000
398
412
  ),
413
+ exclude_logger_prefixes=[
414
+ item.strip()
415
+ for item in (env.get("LOGS_FILTER_EXCLUDE_LOGGER_PREFIXES") or "").split(",")
416
+ if item.strip()
417
+ ]
418
+ or None,
399
419
  ),
400
420
  circuit_breaker=CircuitBreakerConfig(
401
421
  enabled=parse_bool(env.get("LOGS_CIRCUIT_BREAKER_ENABLED"), True),
@@ -100,3 +100,23 @@ def test_flush_propagates_transport_errors() -> None:
100
100
 
101
101
  with pytest.raises(httpx.ConnectError):
102
102
  instance.flush()
103
+
104
+
105
+ def test_runtime_uses_configured_excluded_logger_prefixes() -> None:
106
+ instance = init(
107
+ {
108
+ **_valid_config(),
109
+ "interceptConsole": True,
110
+ "filter": {"excludeLoggerPrefixes": ["botocore"]},
111
+ }
112
+ )
113
+ assert instance is get_logger()
114
+
115
+ import logging
116
+
117
+ logging.getLogger("botocore.endpoint").warning("should be ignored")
118
+ logging.getLogger("service.app").warning("should be captured")
119
+
120
+ metrics = instance.get_metrics()
121
+ assert metrics["logs_processed"] >= 1
122
+ assert metrics["logs_processed"] < 3
@@ -49,6 +49,8 @@ def test_resolve_applies_defaults() -> None:
49
49
  assert resolved.transport.max_retries == 3
50
50
  assert resolved.buffer.max_size == 100
51
51
  assert resolved.filter.sampling_rate == 1.0
52
+ assert "httpx" in resolved.filter.exclude_logger_prefixes
53
+ assert "logs_interceptor" in resolved.filter.exclude_logger_prefixes
52
54
  assert resolved.circuit_breaker.failure_threshold == 50
53
55
  assert resolved.performance.max_concurrent_flushes == 3
54
56
 
@@ -112,3 +114,15 @@ def test_resolve_transport_compression_variants() -> None:
112
114
  )
113
115
  )
114
116
  assert snappy_cfg.transport.compression == "snappy"
117
+
118
+
119
+ def test_resolve_filter_normalizes_custom_excluded_prefixes() -> None:
120
+ resolved = ConfigService.resolve(
121
+ LogsInterceptorConfig(
122
+ transport=TransportConfig(url="https://loki.example.com/loki/api/v1/push", tenant_id="tenant"),
123
+ app_name="app",
124
+ filter=FilterConfig(exclude_logger_prefixes=["HTTPX", " httpx ", "custom_stack"]),
125
+ )
126
+ )
127
+
128
+ assert resolved.filter.exclude_logger_prefixes == ["httpx", "custom_stack"]
@@ -45,3 +45,15 @@ def test_load_labels_from_prefix(monkeypatch) -> None:
45
45
  config = load_config_from_env()
46
46
 
47
47
  assert config.labels == {"service": "busca-prd", "environment": "prd"}
48
+
49
+
50
+ def test_load_excluded_logger_prefixes_from_env(monkeypatch) -> None:
51
+ monkeypatch.setenv("LOGS_URL", "https://loki.example.com/loki/api/v1/push")
52
+ monkeypatch.setenv("LOGS_TENANT", "tenant-a")
53
+ monkeypatch.setenv("LOGS_APP_NAME", "app-a")
54
+ monkeypatch.setenv("LOGS_FILTER_EXCLUDE_LOGGER_PREFIXES", "httpx, httpcore ,custom_stack")
55
+
56
+ config = load_config_from_env()
57
+
58
+ assert config.filter is not None
59
+ assert config.filter.exclude_logger_prefixes == ["httpx", "httpcore", "custom_stack"]