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.
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/PKG-INFO +1 -1
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/pyproject.toml +1 -1
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/__init__.py +5 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/application/config_service.py +4 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/config.py +2 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/interceptors/runtime_interceptor.py +24 -20
- elven_logs_interceptor_python-0.1.5/src/logs_interceptor/infrastructure/internal_capture_guard.py +23 -0
- elven_logs_interceptor_python-0.1.5/src/logs_interceptor/infrastructure/log_noise_filter.py +56 -0
- {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
- {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
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/integrations/logging_handler.py +15 -1
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/integrations/loguru.py +14 -22
- elven_logs_interceptor_python-0.1.5/src/logs_interceptor/integrations/structlog.py +38 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/preload.py +13 -2
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/presentation/factory.py +5 -1
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/utils.py +20 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/tests/integration/test_api.py +20 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/tests/unit/test_config_service.py +14 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/tests/unit/test_env_config.py +12 -0
- elven_logs_interceptor_python-0.1.5/tests/unit/test_integration_filters.py +86 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/tests/unit/test_loguru_sink.py +23 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/tests/unit/test_loki_json_transport.py +21 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/tests/unit/test_protobuf_transport_safety.py +48 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/tests/unit/test_runtime_interceptor.py +14 -0
- elven_logs_interceptor_python-0.1.4/src/logs_interceptor/integrations/structlog.py +0 -21
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/.github/workflows/ci.yml +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/.github/workflows/publish.yml +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/.gitignore +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/ARCHITECTURE.md +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/Makefile +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/README.md +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/examples/basic_app.py +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/examples/fastapi_integration.py +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/examples/full_config_reference.py +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/examples/high_volume.py +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/examples/tracking_usage.py +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/scripts/publish.sh +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/application/__init__.py +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/application/log_service.py +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/domain/__init__.py +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/domain/entities.py +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/domain/interfaces.py +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/domain/value_objects.py +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/__init__.py +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/buffer/__init__.py +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/buffer/memory_buffer.py +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/circuit_breaker/__init__.py +0 -0
- {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
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/compression/__init__.py +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/compression/base.py +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/compression/brotli_compressor.py +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/compression/factory.py +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/compression/gzip_compressor.py +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/compression/noop_compressor.py +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/context/__init__.py +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/context/context_provider.py +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/dlq/__init__.py +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/dlq/file_dlq.py +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/dlq/memory_dlq.py +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/filter/__init__.py +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/filter/log_filter.py +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/interceptors/__init__.py +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/memory/__init__.py +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/memory/memory_tracker.py +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/metrics/__init__.py +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/metrics/metrics_collector.py +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/transport/__init__.py +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/transport/resilient_transport.py +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/transport/transport_factory.py +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/workers/__init__.py +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/infrastructure/workers/worker_pool.py +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/integrations/__init__.py +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/integrations/celery.py +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/integrations/django.py +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/integrations/fastapi.py +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/integrations/flask.py +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/presentation/__init__.py +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/src/logs_interceptor/types.py +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/test-apps/elven-live-demo/.env.example +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/test-apps/elven-live-demo/README.md +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/test-apps/elven-live-demo/app.py +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/test-apps/elven-live-demo/run.sh +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/test-apps/elven-observability-smoke/.env.example +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/test-apps/elven-observability-smoke/README.md +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/test-apps/elven-observability-smoke/app.py +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/test-apps/elven-observability-smoke/run.sh +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/tests/unit/test_circuit_breaker_extra.py +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/tests/unit/test_core_components.py +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/tests/unit/test_log_filter_extra.py +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/tests/unit/test_log_service_unit.py +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/tests/unit/test_memory_buffer_extra.py +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/tests/unit/test_resilient_transport.py +0 -0
- {elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/tests/unit/test_utils_extra.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "elven-logs-interceptor-python"
|
|
7
|
-
version = "0.1.
|
|
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
|
-
|
|
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
|
|
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__(
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
|
elven_logs_interceptor_python-0.1.5/src/logs_interceptor/infrastructure/internal_capture_guard.py
ADDED
|
@@ -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":
|
|
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
|
-
|
|
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":
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
{elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/.github/workflows/ci.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/examples/basic_app.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/examples/high_volume.py
RENAMED
|
File without changes
|
|
File without changes
|
{elven_logs_interceptor_python-0.1.4 → elven_logs_interceptor_python-0.1.5}/scripts/publish.sh
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|