elven-logs-interceptor-python 0.1.2__tar.gz → 0.1.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/PKG-INFO +1 -1
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/pyproject.toml +1 -1
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/application/log_service.py +2 -2
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/buffer/memory_buffer.py +5 -1
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/interceptors/runtime_interceptor.py +22 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/transport/loki_json_transport.py +1 -1
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/transport/loki_protobuf_transport.py +1 -1
- elven_logs_interceptor_python-0.1.4/src/logs_interceptor/integrations/loguru.py +93 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/presentation/factory.py +2 -2
- elven_logs_interceptor_python-0.1.4/test-apps/elven-live-demo/.env.example +49 -0
- elven_logs_interceptor_python-0.1.4/test-apps/elven-live-demo/README.md +17 -0
- elven_logs_interceptor_python-0.1.4/test-apps/elven-live-demo/app.py +97 -0
- elven_logs_interceptor_python-0.1.4/test-apps/elven-observability-smoke/run.sh +21 -0
- elven_logs_interceptor_python-0.1.4/tests/unit/test_loguru_sink.py +114 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/tests/unit/test_memory_buffer_extra.py +27 -0
- elven_logs_interceptor_python-0.1.4/tests/unit/test_runtime_interceptor.py +53 -0
- elven_logs_interceptor_python-0.1.2/src/logs_interceptor/integrations/loguru.py +0 -36
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/.github/workflows/ci.yml +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/.github/workflows/publish.yml +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/.gitignore +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/ARCHITECTURE.md +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/Makefile +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/README.md +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/examples/basic_app.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/examples/fastapi_integration.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/examples/full_config_reference.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/examples/high_volume.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/examples/tracking_usage.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/scripts/publish.sh +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/__init__.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/application/__init__.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/application/config_service.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/config.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/domain/__init__.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/domain/entities.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/domain/interfaces.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/domain/value_objects.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/__init__.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/buffer/__init__.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/circuit_breaker/__init__.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/circuit_breaker/circuit_breaker.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/compression/__init__.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/compression/base.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/compression/brotli_compressor.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/compression/factory.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/compression/gzip_compressor.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/compression/noop_compressor.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/context/__init__.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/context/context_provider.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/dlq/__init__.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/dlq/file_dlq.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/dlq/memory_dlq.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/filter/__init__.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/filter/log_filter.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/interceptors/__init__.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/memory/__init__.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/memory/memory_tracker.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/metrics/__init__.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/metrics/metrics_collector.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/transport/__init__.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/transport/resilient_transport.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/transport/transport_factory.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/workers/__init__.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/workers/worker_pool.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/integrations/__init__.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/integrations/celery.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/integrations/django.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/integrations/fastapi.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/integrations/flask.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/integrations/logging_handler.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/integrations/structlog.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/preload.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/presentation/__init__.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/types.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/utils.py +0 -0
- {elven_logs_interceptor_python-0.1.2/test-apps/elven-observability-smoke → elven_logs_interceptor_python-0.1.4/test-apps/elven-live-demo}/run.sh +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/test-apps/elven-observability-smoke/.env.example +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/test-apps/elven-observability-smoke/README.md +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/test-apps/elven-observability-smoke/app.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/tests/integration/test_api.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/tests/unit/test_circuit_breaker_extra.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/tests/unit/test_config_service.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/tests/unit/test_core_components.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/tests/unit/test_env_config.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/tests/unit/test_log_filter_extra.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/tests/unit/test_log_service_unit.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/tests/unit/test_loki_json_transport.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/tests/unit/test_protobuf_transport_safety.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/tests/unit/test_resilient_transport.py +0 -0
- {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/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.4"
|
|
8
8
|
description = "Production-grade logs interceptor for Python with Loki transport, resilience, batching, and framework integrations."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -22,11 +22,11 @@ try:
|
|
|
22
22
|
except Exception: # pragma: no cover
|
|
23
23
|
pass
|
|
24
24
|
|
|
25
|
-
otel_trace: Any
|
|
25
|
+
otel_trace: Any | None
|
|
26
26
|
try:
|
|
27
27
|
from opentelemetry import trace as otel_trace
|
|
28
28
|
except Exception: # pragma: no cover - optional dependency
|
|
29
|
-
|
|
29
|
+
otel_trace = None
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
@dataclass(slots=True)
|
|
@@ -165,12 +165,16 @@ class MemoryBuffer:
|
|
|
165
165
|
return
|
|
166
166
|
|
|
167
167
|
def _on_timer() -> None:
|
|
168
|
+
callback: Callable[[], None] | None = None
|
|
168
169
|
with self._lock:
|
|
169
170
|
self._flush_timer = None
|
|
170
171
|
if self._destroyed:
|
|
171
172
|
return
|
|
172
173
|
if self._flush_callback and self._entries:
|
|
173
|
-
self._flush_callback
|
|
174
|
+
callback = self._flush_callback
|
|
175
|
+
|
|
176
|
+
if callback is not None:
|
|
177
|
+
callback()
|
|
174
178
|
|
|
175
179
|
self._flush_timer = threading.Timer(self._config.flush_interval / 1000, _on_timer)
|
|
176
180
|
self._flush_timer.daemon = True
|
|
@@ -11,6 +11,18 @@ from ...domain.interfaces import ILogger
|
|
|
11
11
|
from ...types import LogLevel
|
|
12
12
|
from ...utils import safe_stringify
|
|
13
13
|
|
|
14
|
+
_IGNORED_LOGGER_PREFIXES = (
|
|
15
|
+
"httpcore",
|
|
16
|
+
"httpx",
|
|
17
|
+
"urllib3",
|
|
18
|
+
"hpack",
|
|
19
|
+
"h2",
|
|
20
|
+
"h11",
|
|
21
|
+
"opentelemetry",
|
|
22
|
+
"logs_interceptor",
|
|
23
|
+
"elven_unified_observability",
|
|
24
|
+
)
|
|
25
|
+
|
|
14
26
|
|
|
15
27
|
class _BridgeLoggingHandler(logging.Handler):
|
|
16
28
|
def __init__(self, logger: ILogger) -> None:
|
|
@@ -19,6 +31,9 @@ class _BridgeLoggingHandler(logging.Handler):
|
|
|
19
31
|
|
|
20
32
|
def emit(self, record: logging.LogRecord) -> None:
|
|
21
33
|
try:
|
|
34
|
+
if record.name.startswith(_IGNORED_LOGGER_PREFIXES):
|
|
35
|
+
return
|
|
36
|
+
|
|
22
37
|
level = self._map_level(record.levelno)
|
|
23
38
|
msg = record.getMessage()
|
|
24
39
|
context = {
|
|
@@ -56,12 +71,17 @@ class RuntimeInterceptor:
|
|
|
56
71
|
self._original_print = builtins.print
|
|
57
72
|
self._original_excepthook = sys.excepthook
|
|
58
73
|
self._root_logger = logging.getLogger()
|
|
74
|
+
self._original_root_level = self._root_logger.level
|
|
59
75
|
self._bridge_handler = _BridgeLoggingHandler(logger)
|
|
60
76
|
self._original_handlers: list[logging.Handler] = []
|
|
61
77
|
|
|
62
78
|
def enable(self) -> None:
|
|
63
79
|
if self._enabled:
|
|
64
80
|
return
|
|
81
|
+
|
|
82
|
+
self._original_print = builtins.print
|
|
83
|
+
self._original_excepthook = sys.excepthook
|
|
84
|
+
self._original_root_level = self._root_logger.level
|
|
65
85
|
self._enabled = True
|
|
66
86
|
|
|
67
87
|
self._patch_print()
|
|
@@ -80,6 +100,8 @@ class RuntimeInterceptor:
|
|
|
80
100
|
except ValueError:
|
|
81
101
|
pass
|
|
82
102
|
|
|
103
|
+
self._root_logger.setLevel(self._original_root_level)
|
|
104
|
+
|
|
83
105
|
if not self._preserve_original:
|
|
84
106
|
self._root_logger.handlers = self._original_handlers
|
|
85
107
|
|
|
@@ -88,7 +88,7 @@ class LokiJsonTransport:
|
|
|
88
88
|
headers: dict[str, str] = {
|
|
89
89
|
"Content-Type": "application/json",
|
|
90
90
|
"X-Scope-OrgID": self._config.tenant_id,
|
|
91
|
-
"User-Agent": "elven-logs-interceptor-python/0.1.
|
|
91
|
+
"User-Agent": "elven-logs-interceptor-python/0.1.3",
|
|
92
92
|
**self._extra_headers,
|
|
93
93
|
}
|
|
94
94
|
if self._config.auth_token:
|
|
@@ -94,7 +94,7 @@ class LokiProtobufTransport:
|
|
|
94
94
|
"Content-Type": "application/x-protobuf",
|
|
95
95
|
"Content-Encoding": "snappy",
|
|
96
96
|
"X-Scope-OrgID": self._config.tenant_id,
|
|
97
|
-
"User-Agent": "elven-logs-interceptor-python/0.1.
|
|
97
|
+
"User-Agent": "elven-logs-interceptor-python/0.1.3",
|
|
98
98
|
**self._extra_headers,
|
|
99
99
|
}
|
|
100
100
|
if self._config.auth_token:
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from ..domain.interfaces import ILogger
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class LoguruSink:
|
|
10
|
+
def __init__(self, logger: ILogger, exclude_prefixes: list[str] | None = None) -> None:
|
|
11
|
+
self.logger = logger
|
|
12
|
+
self._exclude_prefixes = tuple(
|
|
13
|
+
prefix.lower().strip()
|
|
14
|
+
for prefix in (exclude_prefixes or [])
|
|
15
|
+
if prefix and prefix.strip()
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
def __call__(self, message: Any) -> None:
|
|
19
|
+
try:
|
|
20
|
+
record = getattr(message, "record", None)
|
|
21
|
+
if not isinstance(record, Mapping):
|
|
22
|
+
self.logger.info(str(message), {"source": "loguru"})
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
if self._should_ignore(record):
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
level = self._resolve_level(record)
|
|
29
|
+
extra = record.get("extra")
|
|
30
|
+
normalized_extra = dict(extra) if isinstance(extra, Mapping) else extra
|
|
31
|
+
|
|
32
|
+
self.logger.log(
|
|
33
|
+
level, # type: ignore[arg-type]
|
|
34
|
+
str(record.get("message", "")),
|
|
35
|
+
{
|
|
36
|
+
"source": "loguru",
|
|
37
|
+
"logger_name": record.get("name"),
|
|
38
|
+
"module": record.get("module"),
|
|
39
|
+
"function": record.get("function"),
|
|
40
|
+
"line": record.get("line"),
|
|
41
|
+
"extra": normalized_extra,
|
|
42
|
+
},
|
|
43
|
+
)
|
|
44
|
+
except Exception:
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
def _resolve_level(record: Mapping[str, Any]) -> str:
|
|
49
|
+
raw_level = record.get("level")
|
|
50
|
+
if isinstance(raw_level, Mapping):
|
|
51
|
+
level = str(raw_level.get("name", "INFO")).lower()
|
|
52
|
+
level_number = raw_level.get("no")
|
|
53
|
+
else:
|
|
54
|
+
level = str(getattr(raw_level, "name", "INFO")).lower()
|
|
55
|
+
level_number = getattr(raw_level, "no", None)
|
|
56
|
+
|
|
57
|
+
if level == "warning":
|
|
58
|
+
return "warn"
|
|
59
|
+
if level == "critical":
|
|
60
|
+
return "fatal"
|
|
61
|
+
if level not in {"debug", "info", "warn", "error", "fatal"}:
|
|
62
|
+
if not level and isinstance(level_number, (int, float)):
|
|
63
|
+
if level_number >= 50:
|
|
64
|
+
return "fatal"
|
|
65
|
+
if level_number >= 40:
|
|
66
|
+
return "error"
|
|
67
|
+
if level_number >= 30:
|
|
68
|
+
return "warn"
|
|
69
|
+
if level_number >= 20:
|
|
70
|
+
return "info"
|
|
71
|
+
return "debug"
|
|
72
|
+
return "info"
|
|
73
|
+
return level
|
|
74
|
+
|
|
75
|
+
def _should_ignore(self, record: Mapping[str, Any]) -> bool:
|
|
76
|
+
if not self._exclude_prefixes:
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
name = str(record.get("name") or "").lower()
|
|
80
|
+
module = str(record.get("module") or "").lower()
|
|
81
|
+
extra = record.get("extra")
|
|
82
|
+
extra_logger_name = ""
|
|
83
|
+
if isinstance(extra, Mapping):
|
|
84
|
+
extra_logger_name = str(extra.get("logger_name") or "").lower()
|
|
85
|
+
|
|
86
|
+
for prefix in self._exclude_prefixes:
|
|
87
|
+
if (
|
|
88
|
+
name.startswith(prefix)
|
|
89
|
+
or module.startswith(prefix)
|
|
90
|
+
or extra_logger_name.startswith(prefix)
|
|
91
|
+
):
|
|
92
|
+
return True
|
|
93
|
+
return False
|
|
@@ -15,11 +15,11 @@ from ..infrastructure.filter import LogFilter
|
|
|
15
15
|
from ..infrastructure.interceptors import RuntimeInterceptor
|
|
16
16
|
from ..infrastructure.transport import TransportFactory
|
|
17
17
|
|
|
18
|
-
otel_trace: Any
|
|
18
|
+
otel_trace: Any | None
|
|
19
19
|
try:
|
|
20
20
|
from opentelemetry import trace as otel_trace
|
|
21
21
|
except Exception: # pragma: no cover - optional dependency
|
|
22
|
-
|
|
22
|
+
otel_trace = None
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
@dataclass(slots=True)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
LOGS_URL=https://loki.elvenobservability.com/loki/api/v1/push
|
|
2
|
+
LOGS_TENANT=elven
|
|
3
|
+
LOGS_TOKEN=replace-with-your-token
|
|
4
|
+
LOGS_APP_NAME=elven-live-demo
|
|
5
|
+
LOGS_APP_VERSION=1.0.0
|
|
6
|
+
LOGS_ENVIRONMENT=local
|
|
7
|
+
|
|
8
|
+
LOGS_COMPRESSION=gzip
|
|
9
|
+
LOGS_COMPRESSION_LEVEL=4
|
|
10
|
+
LOGS_COMPRESSION_THRESHOLD=1024
|
|
11
|
+
LOGS_CONNECTION_POOLING=true
|
|
12
|
+
LOGS_MAX_SOCKETS=50
|
|
13
|
+
LOGS_TIMEOUT=10000
|
|
14
|
+
LOGS_MAX_RETRIES=3
|
|
15
|
+
LOGS_RETRY_DELAY=1000
|
|
16
|
+
|
|
17
|
+
LOGS_BUFFER_MAX_SIZE=25
|
|
18
|
+
LOGS_BUFFER_FLUSH_INTERVAL=1500
|
|
19
|
+
LOGS_BUFFER_MAX_MEMORY_MB=128
|
|
20
|
+
LOGS_BUFFER_MAX_AGE=30000
|
|
21
|
+
LOGS_BUFFER_AUTO_FLUSH=true
|
|
22
|
+
|
|
23
|
+
LOGS_FILTER_LEVELS=debug,info,warn,error,fatal
|
|
24
|
+
LOGS_FILTER_SAMPLING_RATE=1.0
|
|
25
|
+
LOGS_FILTER_SANITIZE=true
|
|
26
|
+
LOGS_FILTER_MAX_MESSAGE_LENGTH=8192
|
|
27
|
+
|
|
28
|
+
LOGS_CIRCUIT_BREAKER_ENABLED=true
|
|
29
|
+
LOGS_CIRCUIT_BREAKER_FAILURE_THRESHOLD=20
|
|
30
|
+
LOGS_CIRCUIT_BREAKER_RESET_TIMEOUT=15000
|
|
31
|
+
LOGS_CIRCUIT_BREAKER_HALF_OPEN_REQUESTS=3
|
|
32
|
+
|
|
33
|
+
LOGS_DLQ_ENABLED=true
|
|
34
|
+
LOGS_DLQ_TYPE=file
|
|
35
|
+
LOGS_DLQ_MAX_SIZE=1000
|
|
36
|
+
LOGS_DLQ_MAX_RETRIES=3
|
|
37
|
+
LOGS_DLQ_BASE_PATH=./.logs-dlq
|
|
38
|
+
|
|
39
|
+
LOGS_MAX_CONCURRENT_FLUSHES=5
|
|
40
|
+
LOGS_INTERCEPT_CONSOLE=true
|
|
41
|
+
LOGS_PRESERVE_ORIGINAL_CONSOLE=true
|
|
42
|
+
LOGS_ENABLE_METRICS=true
|
|
43
|
+
LOGS_ENABLE_HEALTH_CHECK=true
|
|
44
|
+
LOGS_DEBUG=false
|
|
45
|
+
LOGS_SILENT_ERRORS=false
|
|
46
|
+
LOGS_ENABLED=true
|
|
47
|
+
|
|
48
|
+
LOGS_LABEL_SERVICE=elven-live-demo
|
|
49
|
+
LOGS_LABEL_ENVIRONMENT=local
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Elven Live Demo
|
|
2
|
+
|
|
3
|
+
Local Python app to exercise `elven-logs-interceptor-python` against the Elven Loki endpoint.
|
|
4
|
+
|
|
5
|
+
## Run
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
cd /Users/leonardozwirtes/Documents/Projects/logs-interceptor-python
|
|
9
|
+
chmod +x test-apps/elven-live-demo/run.sh
|
|
10
|
+
./test-apps/elven-live-demo/run.sh
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Notes
|
|
14
|
+
|
|
15
|
+
- The tracked config template is `.env.example`.
|
|
16
|
+
- The real token lives only in local `.env`, which is ignored by Git.
|
|
17
|
+
- The package import remains `logs_interceptor`.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import random
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
from logs_interceptor import destroy, init, logger
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def emit_startup_logs() -> None:
|
|
11
|
+
logger.info("demo app started", {"component": "startup"})
|
|
12
|
+
logger.track_event("demo_started", {"source": "local-run", "at": time.time()})
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def emit_business_logs() -> None:
|
|
16
|
+
for idx in range(10):
|
|
17
|
+
logger.info(
|
|
18
|
+
"processing item",
|
|
19
|
+
{
|
|
20
|
+
"idx": idx,
|
|
21
|
+
"value": random.randint(100, 999),
|
|
22
|
+
"duration_ms": round(random.uniform(5, 30), 2),
|
|
23
|
+
},
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
logger.warn("slow dependency detected", {"dependency": "inventory-service", "latency_ms": 187})
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
raise RuntimeError("simulated business exception")
|
|
30
|
+
except RuntimeError as exc:
|
|
31
|
+
logger.error(
|
|
32
|
+
"handled domain error",
|
|
33
|
+
{
|
|
34
|
+
"error": str(exc),
|
|
35
|
+
"kind": "business",
|
|
36
|
+
},
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def emit_sync_context_logs() -> None:
|
|
41
|
+
def _run() -> None:
|
|
42
|
+
logger.info("sync request started", {"method": "GET", "path": "/products"})
|
|
43
|
+
print("plain print still works and is intercepted")
|
|
44
|
+
|
|
45
|
+
logger.with_context(
|
|
46
|
+
{
|
|
47
|
+
"request_id": "demo-sync-request-1",
|
|
48
|
+
"trace_id": "demo-sync-trace-1",
|
|
49
|
+
"span_id": "demo-sync-span-1",
|
|
50
|
+
},
|
|
51
|
+
_run,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
async def emit_async_context_logs() -> None:
|
|
56
|
+
async def _run() -> None:
|
|
57
|
+
logger.info("async job started", {"job": "catalog-refresh"})
|
|
58
|
+
await asyncio.sleep(0.05)
|
|
59
|
+
logger.track_event("catalog_refresh_finished", {"ok": True})
|
|
60
|
+
|
|
61
|
+
await logger.with_context_async(
|
|
62
|
+
{
|
|
63
|
+
"request_id": "demo-async-request-1",
|
|
64
|
+
"trace_id": "demo-async-trace-1",
|
|
65
|
+
"span_id": "demo-async-span-1",
|
|
66
|
+
},
|
|
67
|
+
_run,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def print_runtime_summary() -> None:
|
|
72
|
+
metrics = logger.get_metrics()
|
|
73
|
+
health = logger.get_health()
|
|
74
|
+
print(
|
|
75
|
+
{
|
|
76
|
+
"processed": metrics.get("logs_processed"),
|
|
77
|
+
"dropped": metrics.get("logs_dropped"),
|
|
78
|
+
"flush_count": metrics.get("flush_count"),
|
|
79
|
+
"error_count": metrics.get("error_count"),
|
|
80
|
+
}
|
|
81
|
+
)
|
|
82
|
+
print({"health": health})
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def main() -> None:
|
|
86
|
+
init()
|
|
87
|
+
emit_startup_logs()
|
|
88
|
+
emit_business_logs()
|
|
89
|
+
emit_sync_context_logs()
|
|
90
|
+
asyncio.run(emit_async_context_logs())
|
|
91
|
+
logger.flush()
|
|
92
|
+
print_runtime_summary()
|
|
93
|
+
destroy()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
if __name__ == "__main__":
|
|
97
|
+
main()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -Eeuo pipefail
|
|
3
|
+
|
|
4
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
5
|
+
ROOT_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
|
6
|
+
|
|
7
|
+
if [[ ! -f "${SCRIPT_DIR}/.env" ]]; then
|
|
8
|
+
echo "Missing .env at ${SCRIPT_DIR}/.env" >&2
|
|
9
|
+
if [[ -f "${SCRIPT_DIR}/.env.example" ]]; then
|
|
10
|
+
echo "Create it with: cp ${SCRIPT_DIR}/.env.example ${SCRIPT_DIR}/.env" >&2
|
|
11
|
+
fi
|
|
12
|
+
exit 1
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
set -a
|
|
16
|
+
source "${SCRIPT_DIR}/.env"
|
|
17
|
+
set +a
|
|
18
|
+
|
|
19
|
+
export PYTHONPATH="${ROOT_DIR}/src:${PYTHONPATH:-}"
|
|
20
|
+
|
|
21
|
+
python3 "${SCRIPT_DIR}/app.py"
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from logs_interceptor.integrations.loguru import LoguruSink
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class _Logger:
|
|
7
|
+
def __init__(self) -> None:
|
|
8
|
+
self.calls: list[tuple[str, str, dict[str, object] | None]] = []
|
|
9
|
+
|
|
10
|
+
def info(self, message, context=None):
|
|
11
|
+
self.calls.append(("info", message, context))
|
|
12
|
+
|
|
13
|
+
def log(self, level, message, context=None):
|
|
14
|
+
self.calls.append((level, message, context))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class _Message:
|
|
18
|
+
def __init__(self, record):
|
|
19
|
+
self.record = record
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class _Level:
|
|
23
|
+
def __init__(self, name: str, no: int) -> None:
|
|
24
|
+
self.name = name
|
|
25
|
+
self.no = no
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_loguru_sink_records_logger_name_and_metadata() -> None:
|
|
29
|
+
logger = _Logger()
|
|
30
|
+
sink = LoguruSink(logger)
|
|
31
|
+
|
|
32
|
+
sink(
|
|
33
|
+
_Message(
|
|
34
|
+
{
|
|
35
|
+
"name": "service.api",
|
|
36
|
+
"module": "app",
|
|
37
|
+
"function": "handler",
|
|
38
|
+
"line": 42,
|
|
39
|
+
"message": "hello",
|
|
40
|
+
"level": {"name": "INFO"},
|
|
41
|
+
"extra": {"request_id": "req-1"},
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
assert logger.calls == [
|
|
47
|
+
(
|
|
48
|
+
"info",
|
|
49
|
+
"hello",
|
|
50
|
+
{
|
|
51
|
+
"source": "loguru",
|
|
52
|
+
"logger_name": "service.api",
|
|
53
|
+
"module": "app",
|
|
54
|
+
"function": "handler",
|
|
55
|
+
"line": 42,
|
|
56
|
+
"extra": {"request_id": "req-1"},
|
|
57
|
+
},
|
|
58
|
+
)
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_loguru_sink_excludes_configured_prefixes() -> None:
|
|
63
|
+
logger = _Logger()
|
|
64
|
+
sink = LoguruSink(logger, exclude_prefixes=["httpx", "httpcore"])
|
|
65
|
+
|
|
66
|
+
sink(
|
|
67
|
+
_Message(
|
|
68
|
+
{
|
|
69
|
+
"name": "httpx",
|
|
70
|
+
"module": "client",
|
|
71
|
+
"function": "send",
|
|
72
|
+
"line": 1,
|
|
73
|
+
"message": "request",
|
|
74
|
+
"level": {"name": "DEBUG"},
|
|
75
|
+
"extra": {},
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
assert logger.calls == []
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_loguru_sink_supports_loguru_record_level_objects() -> None:
|
|
84
|
+
logger = _Logger()
|
|
85
|
+
sink = LoguruSink(logger)
|
|
86
|
+
|
|
87
|
+
sink(
|
|
88
|
+
_Message(
|
|
89
|
+
{
|
|
90
|
+
"name": "service.api",
|
|
91
|
+
"module": "app",
|
|
92
|
+
"function": "handler",
|
|
93
|
+
"line": 42,
|
|
94
|
+
"message": "hello",
|
|
95
|
+
"level": _Level("WARNING", 30),
|
|
96
|
+
"extra": {"request_id": "req-1"},
|
|
97
|
+
}
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
assert logger.calls == [
|
|
102
|
+
(
|
|
103
|
+
"warn",
|
|
104
|
+
"hello",
|
|
105
|
+
{
|
|
106
|
+
"source": "loguru",
|
|
107
|
+
"logger_name": "service.api",
|
|
108
|
+
"module": "app",
|
|
109
|
+
"function": "handler",
|
|
110
|
+
"line": 42,
|
|
111
|
+
"extra": {"request_id": "req-1"},
|
|
112
|
+
},
|
|
113
|
+
)
|
|
114
|
+
]
|
|
@@ -106,6 +106,33 @@ def test_schedule_flush_callback_runs_with_entries() -> None:
|
|
|
106
106
|
buffer.destroy()
|
|
107
107
|
|
|
108
108
|
|
|
109
|
+
def test_schedule_flush_callback_does_not_hold_buffer_lock() -> None:
|
|
110
|
+
callback_started = threading.Event()
|
|
111
|
+
release_callback = threading.Event()
|
|
112
|
+
add_completed = threading.Event()
|
|
113
|
+
buffer = _buffer(flush_interval=5, auto_flush=True)
|
|
114
|
+
|
|
115
|
+
def flush_callback() -> None:
|
|
116
|
+
callback_started.set()
|
|
117
|
+
release_callback.wait(timeout=1)
|
|
118
|
+
|
|
119
|
+
buffer.set_flush_callback(flush_callback)
|
|
120
|
+
buffer.add(_entry("1", "2026-01-01T00:00:00+00:00"))
|
|
121
|
+
assert callback_started.wait(timeout=1)
|
|
122
|
+
|
|
123
|
+
def add_entry() -> None:
|
|
124
|
+
buffer.add(_entry("2", "2026-01-01T00:00:01+00:00"))
|
|
125
|
+
add_completed.set()
|
|
126
|
+
|
|
127
|
+
add_thread = threading.Thread(target=add_entry)
|
|
128
|
+
add_thread.start()
|
|
129
|
+
assert add_completed.wait(timeout=0.2)
|
|
130
|
+
|
|
131
|
+
release_callback.set()
|
|
132
|
+
add_thread.join(timeout=1)
|
|
133
|
+
buffer.destroy()
|
|
134
|
+
|
|
135
|
+
|
|
109
136
|
def test_schedule_flush_destroyed_guard_inside_timer() -> None:
|
|
110
137
|
event = threading.Event()
|
|
111
138
|
buffer = _buffer(flush_interval=40, auto_flush=False)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from logs_interceptor.infrastructure.interceptors.runtime_interceptor import RuntimeInterceptor
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class _Logger:
|
|
9
|
+
def __init__(self) -> None:
|
|
10
|
+
self.records: list[tuple[str, str, dict[str, object] | None]] = []
|
|
11
|
+
|
|
12
|
+
def log(self, level, message, context=None):
|
|
13
|
+
self.records.append((level, message, context))
|
|
14
|
+
|
|
15
|
+
def info(self, message, context=None):
|
|
16
|
+
self.records.append(("info", message, context))
|
|
17
|
+
|
|
18
|
+
def fatal(self, message, context=None):
|
|
19
|
+
self.records.append(("fatal", message, context))
|
|
20
|
+
|
|
21
|
+
def flush(self):
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_runtime_interceptor_restores_root_logger_level() -> None:
|
|
26
|
+
root = logging.getLogger()
|
|
27
|
+
original_level = root.level
|
|
28
|
+
root.setLevel(logging.WARNING)
|
|
29
|
+
|
|
30
|
+
interceptor = RuntimeInterceptor(_Logger())
|
|
31
|
+
interceptor.enable()
|
|
32
|
+
|
|
33
|
+
assert root.level == logging.DEBUG
|
|
34
|
+
|
|
35
|
+
interceptor.restore()
|
|
36
|
+
|
|
37
|
+
assert root.level == logging.WARNING
|
|
38
|
+
root.setLevel(original_level)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_runtime_interceptor_ignores_known_noisy_logger_prefixes() -> None:
|
|
42
|
+
logger = _Logger()
|
|
43
|
+
interceptor = RuntimeInterceptor(logger)
|
|
44
|
+
interceptor.enable()
|
|
45
|
+
|
|
46
|
+
noisy_logger = logging.getLogger("elven_unified_observability.runtime")
|
|
47
|
+
noisy_logger.warning("internal warning")
|
|
48
|
+
noisy_otel_logger = logging.getLogger("opentelemetry.sdk.trace")
|
|
49
|
+
noisy_otel_logger.info("trace noise")
|
|
50
|
+
|
|
51
|
+
assert logger.records == []
|
|
52
|
+
|
|
53
|
+
interceptor.restore()
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from typing import Any
|
|
4
|
-
|
|
5
|
-
from ..domain.interfaces import ILogger
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class LoguruSink:
|
|
9
|
-
def __init__(self, logger: ILogger) -> None:
|
|
10
|
-
self.logger = logger
|
|
11
|
-
|
|
12
|
-
def __call__(self, message: Any) -> None:
|
|
13
|
-
record = getattr(message, "record", None)
|
|
14
|
-
if record is None:
|
|
15
|
-
self.logger.info(str(message), {"source": "loguru"})
|
|
16
|
-
return
|
|
17
|
-
|
|
18
|
-
level = str(record.get("level", {}).get("name", "INFO")).lower()
|
|
19
|
-
if level == "warning":
|
|
20
|
-
level = "warn"
|
|
21
|
-
if level == "critical":
|
|
22
|
-
level = "fatal"
|
|
23
|
-
if level not in {"debug", "info", "warn", "error", "fatal"}:
|
|
24
|
-
level = "info"
|
|
25
|
-
|
|
26
|
-
self.logger.log(
|
|
27
|
-
level, # type: ignore[arg-type]
|
|
28
|
-
str(record.get("message", "")),
|
|
29
|
-
{
|
|
30
|
-
"source": "loguru",
|
|
31
|
-
"module": record.get("module"),
|
|
32
|
-
"function": record.get("function"),
|
|
33
|
-
"line": record.get("line"),
|
|
34
|
-
"extra": record.get("extra"),
|
|
35
|
-
},
|
|
36
|
-
)
|
{elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/.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.2 → elven_logs_interceptor_python-0.1.4}/examples/basic_app.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/examples/high_volume.py
RENAMED
|
File without changes
|
|
File without changes
|
{elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|