elven-logs-interceptor-python 0.1.4__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 (93) hide show
  1. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/PKG-INFO +1 -1
  2. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/pyproject.toml +1 -1
  3. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/__init__.py +5 -0
  4. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/application/config_service.py +4 -0
  5. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/config.py +2 -0
  6. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/interceptors/runtime_interceptor.py +24 -20
  7. elven_logs_interceptor_python-0.1.5/src/logs_interceptor/infrastructure/internal_capture_guard.py +23 -0
  8. elven_logs_interceptor_python-0.1.5/src/logs_interceptor/infrastructure/log_noise_filter.py +56 -0
  9. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/transport/loki_json_transport.py +8 -2
  10. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/transport/loki_protobuf_transport.py +8 -2
  11. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/integrations/logging_handler.py +15 -1
  12. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/integrations/loguru.py +14 -22
  13. elven_logs_interceptor_python-0.1.5/src/logs_interceptor/integrations/structlog.py +38 -0
  14. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/preload.py +13 -2
  15. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/presentation/factory.py +5 -1
  16. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/utils.py +20 -0
  17. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/tests/integration/test_api.py +20 -0
  18. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/tests/unit/test_config_service.py +14 -0
  19. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/tests/unit/test_env_config.py +12 -0
  20. elven_logs_interceptor_python-0.1.5/tests/unit/test_integration_filters.py +86 -0
  21. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/tests/unit/test_loguru_sink.py +23 -0
  22. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/tests/unit/test_loki_json_transport.py +21 -0
  23. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/tests/unit/test_protobuf_transport_safety.py +48 -0
  24. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/tests/unit/test_runtime_interceptor.py +14 -0
  25. elven_logs_interceptor_python-0.1.4/src/logs_interceptor/integrations/structlog.py +0 -21
  26. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/.github/workflows/ci.yml +0 -0
  27. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/.github/workflows/publish.yml +0 -0
  28. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/.gitignore +0 -0
  29. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/ARCHITECTURE.md +0 -0
  30. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/Makefile +0 -0
  31. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/README.md +0 -0
  32. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/examples/basic_app.py +0 -0
  33. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/examples/fastapi_integration.py +0 -0
  34. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/examples/full_config_reference.py +0 -0
  35. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/examples/high_volume.py +0 -0
  36. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/examples/tracking_usage.py +0 -0
  37. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/scripts/publish.sh +0 -0
  38. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/application/__init__.py +0 -0
  39. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/application/log_service.py +0 -0
  40. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/domain/__init__.py +0 -0
  41. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/domain/entities.py +0 -0
  42. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/domain/interfaces.py +0 -0
  43. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/domain/value_objects.py +0 -0
  44. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/__init__.py +0 -0
  45. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/buffer/__init__.py +0 -0
  46. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/buffer/memory_buffer.py +0 -0
  47. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/circuit_breaker/__init__.py +0 -0
  48. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/circuit_breaker/circuit_breaker.py +0 -0
  49. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/compression/__init__.py +0 -0
  50. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/compression/base.py +0 -0
  51. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/compression/brotli_compressor.py +0 -0
  52. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/compression/factory.py +0 -0
  53. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/compression/gzip_compressor.py +0 -0
  54. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/compression/noop_compressor.py +0 -0
  55. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/context/__init__.py +0 -0
  56. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/context/context_provider.py +0 -0
  57. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/dlq/__init__.py +0 -0
  58. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/dlq/file_dlq.py +0 -0
  59. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/dlq/memory_dlq.py +0 -0
  60. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/filter/__init__.py +0 -0
  61. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/filter/log_filter.py +0 -0
  62. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/interceptors/__init__.py +0 -0
  63. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/memory/__init__.py +0 -0
  64. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/memory/memory_tracker.py +0 -0
  65. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/metrics/__init__.py +0 -0
  66. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/metrics/metrics_collector.py +0 -0
  67. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/transport/__init__.py +0 -0
  68. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/transport/resilient_transport.py +0 -0
  69. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/transport/transport_factory.py +0 -0
  70. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/workers/__init__.py +0 -0
  71. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/workers/worker_pool.py +0 -0
  72. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/integrations/__init__.py +0 -0
  73. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/integrations/celery.py +0 -0
  74. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/integrations/django.py +0 -0
  75. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/integrations/fastapi.py +0 -0
  76. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/integrations/flask.py +0 -0
  77. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/presentation/__init__.py +0 -0
  78. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/types.py +0 -0
  79. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/test-apps/elven-live-demo/.env.example +0 -0
  80. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/test-apps/elven-live-demo/README.md +0 -0
  81. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/test-apps/elven-live-demo/app.py +0 -0
  82. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/test-apps/elven-live-demo/run.sh +0 -0
  83. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/test-apps/elven-observability-smoke/.env.example +0 -0
  84. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/test-apps/elven-observability-smoke/README.md +0 -0
  85. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/test-apps/elven-observability-smoke/app.py +0 -0
  86. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/test-apps/elven-observability-smoke/run.sh +0 -0
  87. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/tests/unit/test_circuit_breaker_extra.py +0 -0
  88. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/tests/unit/test_core_components.py +0 -0
  89. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/tests/unit/test_log_filter_extra.py +0 -0
  90. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/tests/unit/test_log_service_unit.py +0 -0
  91. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/tests/unit/test_memory_buffer_extra.py +0 -0
  92. {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/tests/unit/test_resilient_transport.py +0 -0
  93. {elven_logs_interceptor_python-0.1.4 → 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.4
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.4"
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
@@ -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,28 +10,25 @@ 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
-
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
- )
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
25
15
 
26
16
 
27
17
  class _BridgeLoggingHandler(logging.Handler):
28
- def __init__(self, logger: ILogger) -> None:
18
+ def __init__(self, logger: ILogger, exclude_prefixes: list[str] | None = None) -> None:
29
19
  super().__init__()
30
20
  self._logger = logger
21
+ self._exclude_prefixes = normalize_excluded_logger_prefixes(exclude_prefixes)
31
22
 
32
23
  def emit(self, record: logging.LogRecord) -> None:
33
24
  try:
34
- if record.name.startswith(_IGNORED_LOGGER_PREFIXES):
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
+ ):
35
32
  return
36
33
 
37
34
  level = self._map_level(record.levelno)
@@ -63,16 +60,22 @@ class _BridgeLoggingHandler(logging.Handler):
63
60
 
64
61
 
65
62
  class RuntimeInterceptor:
66
- 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:
67
69
  self._logger = logger
68
70
  self._preserve_original = preserve_original
69
71
  self._enabled = False
72
+ self._exclude_prefixes = list(normalize_excluded_logger_prefixes(exclude_prefixes))
70
73
 
71
74
  self._original_print = builtins.print
72
75
  self._original_excepthook = sys.excepthook
73
76
  self._root_logger = logging.getLogger()
74
77
  self._original_root_level = self._root_logger.level
75
- self._bridge_handler = _BridgeLoggingHandler(logger)
78
+ self._bridge_handler = _BridgeLoggingHandler(logger, self._exclude_prefixes)
76
79
  self._original_handlers: list[logging.Handler] = []
77
80
 
78
81
  def enable(self) -> None:
@@ -113,10 +116,11 @@ class RuntimeInterceptor:
113
116
  def _patch_print(self) -> None:
114
117
  def intercepted_print(*args: Any, **kwargs: Any) -> None:
115
118
  try:
116
- message = " ".join(
117
- [arg if isinstance(arg, str) else safe_stringify(arg) for arg in args]
118
- )
119
- 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"})
120
124
  except Exception:
121
125
  pass
122
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] = {
@@ -4,19 +4,22 @@ from collections.abc import Mapping
4
4
  from typing import Any
5
5
 
6
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
+ )
7
12
 
8
13
 
9
14
  class LoguruSink:
10
15
  def __init__(self, logger: ILogger, exclude_prefixes: list[str] | None = None) -> None:
11
16
  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
+ self._exclude_prefixes = normalize_excluded_logger_prefixes(exclude_prefixes)
17
18
 
18
19
  def __call__(self, message: Any) -> None:
19
20
  try:
21
+ if is_internal_log_capture_suppressed():
22
+ return
20
23
  record = getattr(message, "record", None)
21
24
  if not isinstance(record, Mapping):
22
25
  self.logger.info(str(message), {"source": "loguru"})
@@ -73,21 +76,10 @@ class LoguruSink:
73
76
  return level
74
77
 
75
78
  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
79
  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
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:
@@ -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"]
@@ -0,0 +1,86 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+
5
+ from logs_interceptor.infrastructure.internal_capture_guard import suppress_internal_log_capture
6
+ from logs_interceptor.integrations.logging_handler import LoggingHandler
7
+ from logs_interceptor.integrations.structlog import StructlogProcessor
8
+
9
+
10
+ class _Logger:
11
+ def __init__(self) -> None:
12
+ self.calls: list[tuple[str, str, dict[str, object] | None]] = []
13
+
14
+ def log(self, level, message, context=None):
15
+ self.calls.append((level, message, context))
16
+
17
+
18
+ def test_logging_handler_ignores_records_during_internal_suppression() -> None:
19
+ logger = _Logger()
20
+ handler = LoggingHandler(logger)
21
+ record = logging.LogRecord(
22
+ name="service.api",
23
+ level=logging.INFO,
24
+ pathname=__file__,
25
+ lineno=10,
26
+ msg="hello",
27
+ args=(),
28
+ exc_info=None,
29
+ )
30
+
31
+ with suppress_internal_log_capture():
32
+ handler.emit(record)
33
+
34
+ assert logger.calls == []
35
+
36
+
37
+ def test_structlog_processor_ignores_records_during_internal_suppression() -> None:
38
+ logger = _Logger()
39
+ processor = StructlogProcessor(logger)
40
+
41
+ with suppress_internal_log_capture():
42
+ result = processor(None, "info", {"event": "hello", "request_id": "req-1"})
43
+
44
+ assert result == {"event": "hello", "request_id": "req-1"}
45
+ assert logger.calls == []
46
+
47
+
48
+ def test_logging_handler_ignores_configured_noisy_prefixes() -> None:
49
+ logger = _Logger()
50
+ handler = LoggingHandler(logger, exclude_prefixes=["botocore"])
51
+
52
+ noisy_record = logging.LogRecord(
53
+ name="botocore.endpoint",
54
+ level=logging.INFO,
55
+ pathname=__file__,
56
+ lineno=10,
57
+ msg="noise",
58
+ args=(),
59
+ exc_info=None,
60
+ )
61
+ app_record = logging.LogRecord(
62
+ name="service.api",
63
+ level=logging.INFO,
64
+ pathname=__file__,
65
+ lineno=20,
66
+ msg="hello",
67
+ args=(),
68
+ exc_info=None,
69
+ )
70
+
71
+ handler.emit(noisy_record)
72
+ handler.emit(app_record)
73
+
74
+ assert logger.calls == [
75
+ (
76
+ "info",
77
+ "hello",
78
+ {
79
+ "source": "python-logging",
80
+ "logger_name": "service.api",
81
+ "module": "test_integration_filters",
82
+ "function": None,
83
+ "line": 20,
84
+ },
85
+ )
86
+ ]
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from logs_interceptor.infrastructure.internal_capture_guard import suppress_internal_log_capture
3
4
  from logs_interceptor.integrations.loguru import LoguruSink
4
5
 
5
6
 
@@ -112,3 +113,25 @@ def test_loguru_sink_supports_loguru_record_level_objects() -> None:
112
113
  },
113
114
  )
114
115
  ]
116
+
117
+
118
+ def test_loguru_sink_ignores_records_during_internal_suppression() -> None:
119
+ logger = _Logger()
120
+ sink = LoguruSink(logger)
121
+
122
+ with suppress_internal_log_capture():
123
+ sink(
124
+ _Message(
125
+ {
126
+ "name": "service.api",
127
+ "module": "app",
128
+ "function": "handler",
129
+ "line": 42,
130
+ "message": "hello",
131
+ "level": {"name": "INFO"},
132
+ "extra": {"request_id": "req-1"},
133
+ }
134
+ )
135
+ )
136
+
137
+ assert logger.calls == []
@@ -7,6 +7,9 @@ import pytest
7
7
 
8
8
  from logs_interceptor.config import ResolvedTransportConfig
9
9
  from logs_interceptor.domain.entities import LogEntryEntity
10
+ from logs_interceptor.infrastructure.internal_capture_guard import (
11
+ is_internal_log_capture_suppressed,
12
+ )
10
13
  from logs_interceptor.infrastructure.transport.loki_json_transport import LokiJsonTransport, RetryableTransportError
11
14
 
12
15
 
@@ -70,6 +73,24 @@ def test_loki_json_transport_http_error(monkeypatch: pytest.MonkeyPatch) -> None
70
73
  assert transport.get_health()["healthy"] is False
71
74
 
72
75
 
76
+ def test_loki_json_transport_suppresses_internal_capture_during_request(
77
+ monkeypatch: pytest.MonkeyPatch,
78
+ ) -> None:
79
+ transport = LokiJsonTransport(_config())
80
+ observed: list[bool] = []
81
+
82
+ def fake_request(headers, body):
83
+ del headers, body
84
+ observed.append(is_internal_log_capture_suppressed())
85
+ return _Response(status_code=204)
86
+
87
+ monkeypatch.setattr(transport, "_request", fake_request)
88
+
89
+ transport.send(_entries())
90
+
91
+ assert observed == [True]
92
+
93
+
73
94
  def test_timestamp_to_ns_uses_timezone_correctly() -> None:
74
95
  iso = "2026-02-20T19:39:14.123456+00:00"
75
96
  expected = int(datetime(2026, 2, 20, 19, 39, 14, 123456, tzinfo=timezone.utc).timestamp() * 1_000_000_000)
@@ -11,6 +11,10 @@ from logs_interceptor.config import (
11
11
  ResolvedPerformanceConfig,
12
12
  ResolvedTransportConfig,
13
13
  )
14
+ from logs_interceptor.domain.entities import LogEntryEntity
15
+ from logs_interceptor.infrastructure.internal_capture_guard import (
16
+ is_internal_log_capture_suppressed,
17
+ )
14
18
  from logs_interceptor.infrastructure.transport.loki_protobuf_transport import LokiProtobufTransport
15
19
  from logs_interceptor.infrastructure.transport.resilient_transport import ResilientTransport
16
20
  from logs_interceptor.infrastructure.transport.transport_factory import TransportFactory
@@ -91,3 +95,47 @@ def test_transport_factory_falls_back_to_json_for_snappy_without_opt_in(monkeypa
91
95
  transport = TransportFactory.create(config)
92
96
  assert isinstance(transport, ResilientTransport)
93
97
  assert transport._transport.__class__.__name__ == "LokiJsonTransport" # noqa: SLF001
98
+
99
+
100
+ def test_protobuf_transport_suppresses_internal_capture_during_request(
101
+ monkeypatch: pytest.MonkeyPatch,
102
+ ) -> None:
103
+ class _Snappy:
104
+ @staticmethod
105
+ def compress(raw: bytes) -> bytes:
106
+ return raw[::-1]
107
+
108
+ class _Response:
109
+ status_code = 204
110
+ text = ""
111
+
112
+ monkeypatch.setenv("LOGS_ENABLE_EXPERIMENTAL_PROTOBUF", "true")
113
+ monkeypatch.setattr(
114
+ "logs_interceptor.infrastructure.transport.loki_protobuf_transport.snappy",
115
+ _Snappy(),
116
+ )
117
+
118
+ transport = LokiProtobufTransport(_config().transport)
119
+ observed: list[bool] = []
120
+
121
+ def fake_request(headers, body):
122
+ del headers, body
123
+ observed.append(is_internal_log_capture_suppressed())
124
+ return _Response()
125
+
126
+ monkeypatch.setattr(transport, "_request", fake_request)
127
+
128
+ transport.send(
129
+ [
130
+ LogEntryEntity(
131
+ id="1",
132
+ timestamp="2026-01-01T00:00:00.000000+00:00",
133
+ level="info",
134
+ message="hello",
135
+ labels={"service": "billing"},
136
+ context={"a": 1},
137
+ )
138
+ ]
139
+ )
140
+
141
+ assert observed == [True]
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import logging
4
4
 
5
5
  from logs_interceptor.infrastructure.interceptors.runtime_interceptor import RuntimeInterceptor
6
+ from logs_interceptor.infrastructure.internal_capture_guard import suppress_internal_log_capture
6
7
 
7
8
 
8
9
  class _Logger:
@@ -51,3 +52,16 @@ def test_runtime_interceptor_ignores_known_noisy_logger_prefixes() -> None:
51
52
  assert logger.records == []
52
53
 
53
54
  interceptor.restore()
55
+
56
+
57
+ def test_runtime_interceptor_ignores_records_during_internal_suppression() -> None:
58
+ logger = _Logger()
59
+ interceptor = RuntimeInterceptor(logger)
60
+ interceptor.enable()
61
+
62
+ with suppress_internal_log_capture():
63
+ logging.getLogger("service.api").warning("internal transport noise")
64
+
65
+ assert logger.records == []
66
+
67
+ interceptor.restore()
@@ -1,21 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import Any
4
-
5
- from ..domain.interfaces import ILogger
6
-
7
-
8
- class StructlogProcessor:
9
- def __init__(self, logger: ILogger) -> None:
10
- self.logger = logger
11
-
12
- def __call__(self, _logger: Any, method_name: str, event_dict: dict[str, Any]) -> dict[str, Any]:
13
- message = str(event_dict.get("event", method_name))
14
- level = method_name.lower()
15
- if level not in {"debug", "info", "warn", "error", "fatal"}:
16
- level = "info"
17
-
18
- context = dict(event_dict)
19
- context.pop("event", None)
20
- self.logger.log(level, message, {"source": "structlog", **context}) # type: ignore[arg-type]
21
- return event_dict