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.
Files changed (90) hide show
  1. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/PKG-INFO +1 -1
  2. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/pyproject.toml +1 -1
  3. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/application/log_service.py +2 -2
  4. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/buffer/memory_buffer.py +5 -1
  5. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/interceptors/runtime_interceptor.py +22 -0
  6. {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
  7. {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
  8. elven_logs_interceptor_python-0.1.4/src/logs_interceptor/integrations/loguru.py +93 -0
  9. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/presentation/factory.py +2 -2
  10. elven_logs_interceptor_python-0.1.4/test-apps/elven-live-demo/.env.example +49 -0
  11. elven_logs_interceptor_python-0.1.4/test-apps/elven-live-demo/README.md +17 -0
  12. elven_logs_interceptor_python-0.1.4/test-apps/elven-live-demo/app.py +97 -0
  13. elven_logs_interceptor_python-0.1.4/test-apps/elven-observability-smoke/run.sh +21 -0
  14. elven_logs_interceptor_python-0.1.4/tests/unit/test_loguru_sink.py +114 -0
  15. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/tests/unit/test_memory_buffer_extra.py +27 -0
  16. elven_logs_interceptor_python-0.1.4/tests/unit/test_runtime_interceptor.py +53 -0
  17. elven_logs_interceptor_python-0.1.2/src/logs_interceptor/integrations/loguru.py +0 -36
  18. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/.github/workflows/ci.yml +0 -0
  19. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/.github/workflows/publish.yml +0 -0
  20. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/.gitignore +0 -0
  21. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/ARCHITECTURE.md +0 -0
  22. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/Makefile +0 -0
  23. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/README.md +0 -0
  24. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/examples/basic_app.py +0 -0
  25. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/examples/fastapi_integration.py +0 -0
  26. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/examples/full_config_reference.py +0 -0
  27. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/examples/high_volume.py +0 -0
  28. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/examples/tracking_usage.py +0 -0
  29. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/scripts/publish.sh +0 -0
  30. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/__init__.py +0 -0
  31. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/application/__init__.py +0 -0
  32. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/application/config_service.py +0 -0
  33. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/config.py +0 -0
  34. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/domain/__init__.py +0 -0
  35. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/domain/entities.py +0 -0
  36. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/domain/interfaces.py +0 -0
  37. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/domain/value_objects.py +0 -0
  38. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/__init__.py +0 -0
  39. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/buffer/__init__.py +0 -0
  40. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/circuit_breaker/__init__.py +0 -0
  41. {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
  42. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/compression/__init__.py +0 -0
  43. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/compression/base.py +0 -0
  44. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/compression/brotli_compressor.py +0 -0
  45. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/compression/factory.py +0 -0
  46. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/compression/gzip_compressor.py +0 -0
  47. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/compression/noop_compressor.py +0 -0
  48. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/context/__init__.py +0 -0
  49. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/context/context_provider.py +0 -0
  50. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/dlq/__init__.py +0 -0
  51. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/dlq/file_dlq.py +0 -0
  52. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/dlq/memory_dlq.py +0 -0
  53. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/filter/__init__.py +0 -0
  54. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/filter/log_filter.py +0 -0
  55. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/interceptors/__init__.py +0 -0
  56. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/memory/__init__.py +0 -0
  57. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/memory/memory_tracker.py +0 -0
  58. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/metrics/__init__.py +0 -0
  59. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/metrics/metrics_collector.py +0 -0
  60. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/transport/__init__.py +0 -0
  61. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/transport/resilient_transport.py +0 -0
  62. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/transport/transport_factory.py +0 -0
  63. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/workers/__init__.py +0 -0
  64. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/infrastructure/workers/worker_pool.py +0 -0
  65. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/integrations/__init__.py +0 -0
  66. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/integrations/celery.py +0 -0
  67. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/integrations/django.py +0 -0
  68. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/integrations/fastapi.py +0 -0
  69. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/integrations/flask.py +0 -0
  70. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/integrations/logging_handler.py +0 -0
  71. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/integrations/structlog.py +0 -0
  72. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/preload.py +0 -0
  73. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/presentation/__init__.py +0 -0
  74. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/types.py +0 -0
  75. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/src/logs_interceptor/utils.py +0 -0
  76. {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
  77. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/test-apps/elven-observability-smoke/.env.example +0 -0
  78. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/test-apps/elven-observability-smoke/README.md +0 -0
  79. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/test-apps/elven-observability-smoke/app.py +0 -0
  80. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/tests/integration/test_api.py +0 -0
  81. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/tests/unit/test_circuit_breaker_extra.py +0 -0
  82. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/tests/unit/test_config_service.py +0 -0
  83. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/tests/unit/test_core_components.py +0 -0
  84. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/tests/unit/test_env_config.py +0 -0
  85. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/tests/unit/test_log_filter_extra.py +0 -0
  86. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/tests/unit/test_log_service_unit.py +0 -0
  87. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/tests/unit/test_loki_json_transport.py +0 -0
  88. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/tests/unit/test_protobuf_transport_safety.py +0 -0
  89. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/tests/unit/test_resilient_transport.py +0 -0
  90. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.4}/tests/unit/test_utils_extra.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: elven-logs-interceptor-python
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: Production-grade logs interceptor for Python with Loki transport, resilience, batching, and framework integrations.
5
5
  Author: Elven Observability
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "elven-logs-interceptor-python"
7
- version = "0.1.2"
7
+ version = "0.1.4"
8
8
  description = "Production-grade logs interceptor for Python with Loki transport, resilience, batching, and framework integrations."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -22,11 +22,11 @@ try:
22
22
  except Exception: # pragma: no cover
23
23
  pass
24
24
 
25
- otel_trace: Any = None
25
+ 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
- pass
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.2",
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.2",
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 = None
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
- pass
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
- )