elven-logs-interceptor-python 0.1.2__py3-none-any.whl

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 (56) hide show
  1. elven_logs_interceptor_python-0.1.2.dist-info/METADATA +262 -0
  2. elven_logs_interceptor_python-0.1.2.dist-info/RECORD +56 -0
  3. elven_logs_interceptor_python-0.1.2.dist-info/WHEEL +4 -0
  4. logs_interceptor/__init__.py +333 -0
  5. logs_interceptor/application/__init__.py +27 -0
  6. logs_interceptor/application/config_service.py +232 -0
  7. logs_interceptor/application/log_service.py +383 -0
  8. logs_interceptor/config.py +190 -0
  9. logs_interceptor/domain/__init__.py +25 -0
  10. logs_interceptor/domain/entities.py +41 -0
  11. logs_interceptor/domain/interfaces.py +149 -0
  12. logs_interceptor/domain/value_objects.py +40 -0
  13. logs_interceptor/infrastructure/__init__.py +48 -0
  14. logs_interceptor/infrastructure/buffer/__init__.py +3 -0
  15. logs_interceptor/infrastructure/buffer/memory_buffer.py +187 -0
  16. logs_interceptor/infrastructure/circuit_breaker/__init__.py +3 -0
  17. logs_interceptor/infrastructure/circuit_breaker/circuit_breaker.py +110 -0
  18. logs_interceptor/infrastructure/compression/__init__.py +14 -0
  19. logs_interceptor/infrastructure/compression/base.py +20 -0
  20. logs_interceptor/infrastructure/compression/brotli_compressor.py +27 -0
  21. logs_interceptor/infrastructure/compression/factory.py +18 -0
  22. logs_interceptor/infrastructure/compression/gzip_compressor.py +20 -0
  23. logs_interceptor/infrastructure/compression/noop_compressor.py +14 -0
  24. logs_interceptor/infrastructure/context/__init__.py +3 -0
  25. logs_interceptor/infrastructure/context/context_provider.py +44 -0
  26. logs_interceptor/infrastructure/dlq/__init__.py +4 -0
  27. logs_interceptor/infrastructure/dlq/file_dlq.py +170 -0
  28. logs_interceptor/infrastructure/dlq/memory_dlq.py +59 -0
  29. logs_interceptor/infrastructure/filter/__init__.py +3 -0
  30. logs_interceptor/infrastructure/filter/log_filter.py +55 -0
  31. logs_interceptor/infrastructure/interceptors/__init__.py +3 -0
  32. logs_interceptor/infrastructure/interceptors/runtime_interceptor.py +139 -0
  33. logs_interceptor/infrastructure/memory/__init__.py +3 -0
  34. logs_interceptor/infrastructure/memory/memory_tracker.py +95 -0
  35. logs_interceptor/infrastructure/metrics/__init__.py +3 -0
  36. logs_interceptor/infrastructure/metrics/metrics_collector.py +104 -0
  37. logs_interceptor/infrastructure/transport/__init__.py +12 -0
  38. logs_interceptor/infrastructure/transport/loki_json_transport.py +226 -0
  39. logs_interceptor/infrastructure/transport/loki_protobuf_transport.py +209 -0
  40. logs_interceptor/infrastructure/transport/resilient_transport.py +161 -0
  41. logs_interceptor/infrastructure/transport/transport_factory.py +39 -0
  42. logs_interceptor/infrastructure/workers/__init__.py +3 -0
  43. logs_interceptor/infrastructure/workers/worker_pool.py +57 -0
  44. logs_interceptor/integrations/__init__.py +17 -0
  45. logs_interceptor/integrations/celery.py +53 -0
  46. logs_interceptor/integrations/django.py +44 -0
  47. logs_interceptor/integrations/fastapi.py +53 -0
  48. logs_interceptor/integrations/flask.py +50 -0
  49. logs_interceptor/integrations/logging_handler.py +43 -0
  50. logs_interceptor/integrations/loguru.py +36 -0
  51. logs_interceptor/integrations/structlog.py +21 -0
  52. logs_interceptor/preload.py +61 -0
  53. logs_interceptor/presentation/__init__.py +3 -0
  54. logs_interceptor/presentation/factory.py +128 -0
  55. logs_interceptor/types.py +89 -0
  56. logs_interceptor/utils.py +508 -0
@@ -0,0 +1,190 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from dataclasses import dataclass, field
5
+ from typing import Literal
6
+
7
+ from .types import LogLevel
8
+
9
+ CompressionType = Literal["none", "gzip", "brotli", "snappy"]
10
+ DLQType = Literal["memory", "file"]
11
+
12
+
13
+ @dataclass(slots=True)
14
+ class TransportConfig:
15
+ url: str = ""
16
+ tenant_id: str = ""
17
+ auth_token: str | None = None
18
+ timeout: int | None = None
19
+ max_retries: int | None = None
20
+ retry_delay: int | None = None
21
+ compression: CompressionType | bool | None = None
22
+ compression_level: int | None = None
23
+ compression_threshold: int | None = None
24
+ use_workers: bool | None = None
25
+ max_workers: int | None = None
26
+ worker_timeout: int | None = None
27
+ enable_connection_pooling: bool | None = None
28
+ max_sockets: int | None = None
29
+
30
+
31
+ @dataclass(slots=True)
32
+ class BufferConfig:
33
+ max_size: int | None = None
34
+ flush_interval: int | None = None
35
+ max_age: int | None = None
36
+ auto_flush: bool | None = None
37
+ max_memory_mb: int | None = None
38
+
39
+
40
+ @dataclass(slots=True)
41
+ class FilterConfig:
42
+ levels: list[LogLevel] | None = None
43
+ patterns: list[str] | None = None
44
+ sampling_rate: float | None = None
45
+ max_message_length: int | None = None
46
+ sanitize: bool | None = None
47
+ sensitive_patterns: list[str] | None = None
48
+
49
+
50
+ @dataclass(slots=True)
51
+ class CircuitBreakerConfig:
52
+ enabled: bool | None = None
53
+ failure_threshold: int | None = None
54
+ reset_timeout: int | None = None
55
+ half_open_requests: int | None = None
56
+
57
+
58
+ @dataclass(slots=True)
59
+ class PerformanceConfig:
60
+ use_workers: bool | None = None
61
+ max_concurrent_flushes: int | None = None
62
+ compression_level: int | None = None
63
+ max_workers: int | None = None
64
+ worker_timeout: int | None = None
65
+
66
+
67
+ @dataclass(slots=True)
68
+ class DeadLetterQueueConfig:
69
+ enabled: bool | None = None
70
+ type: DLQType | None = None
71
+ max_size: int | None = None
72
+ max_file_size_mb: int | None = None
73
+ max_retries: int | None = None
74
+ base_path: str | None = None
75
+
76
+
77
+ @dataclass(slots=True)
78
+ class WinstonIntegrationConfig:
79
+ enabled: bool = True
80
+ levels: dict[str, LogLevel] | None = None
81
+
82
+
83
+ @dataclass(slots=True)
84
+ class MorganIntegrationConfig:
85
+ enabled: bool = True
86
+ format: str | None = None
87
+
88
+
89
+ @dataclass(slots=True)
90
+ class IntegrationsConfig:
91
+ winston: bool | WinstonIntegrationConfig | None = None
92
+ morgan: bool | MorganIntegrationConfig | None = None
93
+
94
+
95
+ @dataclass(slots=True)
96
+ class LogsInterceptorConfig:
97
+ transport: TransportConfig = field(default_factory=TransportConfig)
98
+ app_name: str = ""
99
+ version: str | None = None
100
+ environment: str | None = None
101
+ labels: dict[str, str] | None = None
102
+ dynamic_labels: dict[str, Callable[[], str | int]] | None = None
103
+ buffer: BufferConfig | None = None
104
+ filter: FilterConfig | None = None
105
+ circuit_breaker: CircuitBreakerConfig | None = None
106
+ integrations: IntegrationsConfig | None = None
107
+ performance: PerformanceConfig | None = None
108
+ dead_letter_queue: DeadLetterQueueConfig | None = None
109
+ enable_metrics: bool | None = None
110
+ enable_health_check: bool | None = None
111
+ intercept_console: bool | None = None
112
+ preserve_original_console: bool | None = None
113
+ debug: bool | None = None
114
+ silent_errors: bool | None = None
115
+
116
+
117
+ @dataclass(slots=True)
118
+ class ResolvedTransportConfig:
119
+ url: str
120
+ tenant_id: str
121
+ auth_token: str
122
+ timeout: int
123
+ max_retries: int
124
+ retry_delay: int
125
+ compression: CompressionType
126
+ compression_level: int
127
+ compression_threshold: int
128
+ use_workers: bool
129
+ max_workers: int | None
130
+ worker_timeout: int
131
+ enable_connection_pooling: bool
132
+ max_sockets: int
133
+
134
+
135
+ @dataclass(slots=True)
136
+ class ResolvedBufferConfig:
137
+ max_size: int
138
+ flush_interval: int
139
+ max_age: int
140
+ auto_flush: bool
141
+ max_memory_mb: int
142
+
143
+
144
+ @dataclass(slots=True)
145
+ class ResolvedFilterConfig:
146
+ levels: list[LogLevel]
147
+ patterns: list[str]
148
+ sampling_rate: float
149
+ max_message_length: int
150
+ sanitize: bool
151
+ sensitive_patterns: list[str]
152
+
153
+
154
+ @dataclass(slots=True)
155
+ class ResolvedCircuitBreakerConfig:
156
+ enabled: bool
157
+ failure_threshold: int
158
+ reset_timeout: int
159
+ half_open_requests: int
160
+
161
+
162
+ @dataclass(slots=True)
163
+ class ResolvedPerformanceConfig:
164
+ use_workers: bool
165
+ max_concurrent_flushes: int
166
+ compression_level: int
167
+ max_workers: int | None
168
+ worker_timeout: int
169
+
170
+
171
+ @dataclass(slots=True)
172
+ class ResolvedLogsInterceptorConfig:
173
+ transport: ResolvedTransportConfig
174
+ app_name: str
175
+ version: str
176
+ environment: str
177
+ labels: dict[str, str]
178
+ dynamic_labels: dict[str, Callable[[], str | int]]
179
+ buffer: ResolvedBufferConfig
180
+ filter: ResolvedFilterConfig
181
+ circuit_breaker: ResolvedCircuitBreakerConfig
182
+ integrations: IntegrationsConfig
183
+ performance: ResolvedPerformanceConfig
184
+ dead_letter_queue: DeadLetterQueueConfig | None
185
+ enable_metrics: bool
186
+ enable_health_check: bool
187
+ intercept_console: bool
188
+ preserve_original_console: bool
189
+ debug: bool
190
+ silent_errors: bool
@@ -0,0 +1,25 @@
1
+ from .entities import LogEntryEntity
2
+ from .interfaces import (
3
+ ICircuitBreaker,
4
+ IContextProvider,
5
+ IDeadLetterQueue,
6
+ ILogBuffer,
7
+ ILogFilter,
8
+ ILogger,
9
+ ILogInterceptor,
10
+ ILogTransport,
11
+ )
12
+ from .value_objects import LogLevelVO
13
+
14
+ __all__ = [
15
+ "LogEntryEntity",
16
+ "LogLevelVO",
17
+ "ILogger",
18
+ "ILogTransport",
19
+ "ILogBuffer",
20
+ "ILogFilter",
21
+ "IContextProvider",
22
+ "ICircuitBreaker",
23
+ "IDeadLetterQueue",
24
+ "ILogInterceptor",
25
+ ]
@@ -0,0 +1,41 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any
5
+
6
+ from ..types import LogEntry, LogLevel
7
+
8
+
9
+ @dataclass(slots=True)
10
+ class LogEntryEntity:
11
+ id: str
12
+ timestamp: str
13
+ level: LogLevel
14
+ message: str
15
+ context: dict[str, Any] | None = None
16
+ trace_id: str | None = None
17
+ span_id: str | None = None
18
+ request_id: str | None = None
19
+ labels: dict[str, str] | None = None
20
+ metadata: dict[str, Any] | None = None
21
+
22
+ def to_dict(self) -> LogEntry:
23
+ payload: LogEntry = {
24
+ "id": self.id,
25
+ "timestamp": self.timestamp,
26
+ "level": self.level,
27
+ "message": self.message,
28
+ }
29
+ if self.context is not None:
30
+ payload["context"] = self.context
31
+ if self.trace_id:
32
+ payload["trace_id"] = self.trace_id
33
+ if self.span_id:
34
+ payload["span_id"] = self.span_id
35
+ if self.request_id:
36
+ payload["request_id"] = self.request_id
37
+ if self.labels:
38
+ payload["labels"] = self.labels
39
+ if self.metadata:
40
+ payload["metadata"] = self.metadata
41
+ return payload
@@ -0,0 +1,149 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Protocol, runtime_checkable
4
+
5
+ from ..types import (
6
+ CircuitBreakerState,
7
+ HealthStatus,
8
+ LoggerMetrics,
9
+ LogLevel,
10
+ TransportHealth,
11
+ TransportMetrics,
12
+ )
13
+ from .entities import LogEntryEntity
14
+
15
+
16
+ class BufferMetrics(Protocol):
17
+ size: int
18
+ max_size: int
19
+ oldest_entry: float | None
20
+ newest_entry: float | None
21
+ memory_usage_mb: float
22
+ dropped_entries: int
23
+
24
+
25
+ class ILogBuffer(Protocol):
26
+ def add(self, entry: LogEntryEntity) -> None: ...
27
+
28
+ def flush(self) -> list[LogEntryEntity]: ...
29
+
30
+ def peek(self) -> list[LogEntryEntity]: ...
31
+
32
+ def size(self) -> int: ...
33
+
34
+ def is_full(self) -> bool: ...
35
+
36
+ def should_flush(self) -> bool: ...
37
+
38
+ def clear(self) -> None: ...
39
+
40
+ def destroy(self) -> None: ...
41
+
42
+ def get_metrics(self) -> dict[str, Any]: ...
43
+
44
+
45
+ class ILogFilter(Protocol):
46
+ def should_process(self, entry: LogEntryEntity) -> bool: ...
47
+
48
+ def filter(self, entry: LogEntryEntity) -> LogEntryEntity: ...
49
+
50
+ def is_level_enabled(self, level: LogLevel) -> bool: ...
51
+
52
+
53
+ class ILogTransport(Protocol):
54
+ def send(self, entries: list[LogEntryEntity]) -> None: ...
55
+
56
+ def is_available(self) -> bool: ...
57
+
58
+ def get_health(self) -> TransportHealth: ...
59
+
60
+ def get_metrics(self) -> TransportMetrics | None: ...
61
+
62
+ def destroy(self) -> None: ...
63
+
64
+
65
+ class IDeadLetterQueue(Protocol):
66
+ def add(self, entry: LogEntryEntity, reason: str) -> dict[str, int]: ...
67
+
68
+ def add_batch(self, entries: list[LogEntryEntity], reason: str) -> dict[str, int]: ...
69
+
70
+ def flush(self) -> int: ...
71
+
72
+ def size(self) -> int: ...
73
+
74
+ def clear(self) -> None: ...
75
+
76
+ def get_entries(self, limit: int = 100) -> list[dict[str, Any]]: ...
77
+
78
+ def get_stats(self) -> dict[str, int]: ...
79
+
80
+
81
+ class ICircuitBreaker(Protocol):
82
+ def execute(self, operation: Any) -> Any: ...
83
+
84
+ def record_success(self) -> None: ...
85
+
86
+ def record_failure(self, error: Exception | None = None) -> None: ...
87
+
88
+ def get_state(self) -> dict[str, Any]: ...
89
+
90
+ def reset(self) -> None: ...
91
+
92
+
93
+ class IContextProvider(Protocol):
94
+ def get_context(self) -> dict[str, Any]: ...
95
+
96
+ def run_with_context(self, context: dict[str, Any], fn: Any) -> Any: ...
97
+
98
+ async def run_with_context_async(self, context: dict[str, Any], fn: Any) -> Any: ...
99
+
100
+ def set(self, key: str, value: Any) -> None: ...
101
+
102
+ def get(self, key: str, default: Any = None) -> Any: ...
103
+
104
+ def clear(self) -> None: ...
105
+
106
+
107
+ @runtime_checkable
108
+ class ILogger(Protocol):
109
+ def debug(self, message: str, context: dict[str, Any] | None = None) -> None: ...
110
+
111
+ def info(self, message: str, context: dict[str, Any] | None = None) -> None: ...
112
+
113
+ def warn(self, message: str, context: dict[str, Any] | None = None) -> None: ...
114
+
115
+ def error(self, message: str, context: dict[str, Any] | None = None) -> None: ...
116
+
117
+ def fatal(self, message: str, context: dict[str, Any] | None = None) -> None: ...
118
+
119
+ def log(self, level: LogLevel, message: str, context: dict[str, Any] | None = None) -> None: ...
120
+
121
+ def track_event(self, event_name: str, properties: dict[str, Any] | None = None) -> None: ...
122
+
123
+ def flush(self) -> None: ...
124
+
125
+ async def aflush(self) -> None: ...
126
+
127
+ def with_context(self, context: dict[str, Any], fn: Any) -> Any: ...
128
+
129
+ async def with_context_async(self, context: dict[str, Any], fn: Any) -> Any: ...
130
+
131
+ def get_metrics(self) -> LoggerMetrics: ...
132
+
133
+ def get_health(self) -> HealthStatus: ...
134
+
135
+ def destroy(self) -> None: ...
136
+
137
+ async def adestroy(self) -> None: ...
138
+
139
+
140
+ class ILogInterceptor(Protocol):
141
+ def enable(self) -> None: ...
142
+
143
+ def restore(self) -> None: ...
144
+
145
+ def is_enabled(self) -> bool: ...
146
+
147
+
148
+ class CircuitBreakerSnapshot(Protocol):
149
+ state: CircuitBreakerState
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from ..types import LogLevel
6
+
7
+ VALID_LOG_LEVELS: tuple[LogLevel, ...] = ("debug", "info", "warn", "error", "fatal")
8
+ LEVEL_PRIORITY: dict[LogLevel, int] = {
9
+ "debug": 0,
10
+ "info": 1,
11
+ "warn": 2,
12
+ "error": 3,
13
+ "fatal": 4,
14
+ }
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class LogLevelVO:
19
+ value: LogLevel
20
+
21
+ def __post_init__(self) -> None:
22
+ if not self.is_valid(self.value):
23
+ raise ValueError(f"Invalid log level: {self.value}")
24
+
25
+ @staticmethod
26
+ def is_valid(level: str) -> bool:
27
+ return level in VALID_LOG_LEVELS
28
+
29
+ @staticmethod
30
+ def from_string(level: str) -> LogLevelVO:
31
+ normalized = level.lower().strip()
32
+ if not LogLevelVO.is_valid(normalized):
33
+ raise ValueError(f"Invalid log level: {level}")
34
+ return LogLevelVO(normalized) # type: ignore[arg-type]
35
+
36
+ def compare_to(self, other: LogLevelVO) -> int:
37
+ return LEVEL_PRIORITY[self.value] - LEVEL_PRIORITY[other.value]
38
+
39
+ def is_greater_than_or_equal(self, other: LogLevelVO) -> bool:
40
+ return self.compare_to(other) >= 0
@@ -0,0 +1,48 @@
1
+ from .buffer import MemoryBuffer
2
+ from .circuit_breaker import CircuitBreaker
3
+ from .compression import (
4
+ BrotliCompressor,
5
+ Compressor,
6
+ CompressorConfig,
7
+ CompressorFactory,
8
+ GzipCompressor,
9
+ NoOpCompressor,
10
+ )
11
+ from .context import ContextVarProvider
12
+ from .dlq import FileDeadLetterQueue, MemoryDeadLetterQueue
13
+ from .filter import LogFilter
14
+ from .interceptors import RuntimeInterceptor
15
+ from .memory import MemoryTracker
16
+ from .metrics import MetricsCollector
17
+ from .transport import (
18
+ LokiJsonTransport,
19
+ LokiProtobufTransport,
20
+ ResilientTransport,
21
+ ResilientTransportConfig,
22
+ TransportFactory,
23
+ )
24
+ from .workers import WorkerPool
25
+
26
+ __all__ = [
27
+ "MemoryBuffer",
28
+ "CircuitBreaker",
29
+ "Compressor",
30
+ "CompressorConfig",
31
+ "CompressorFactory",
32
+ "GzipCompressor",
33
+ "BrotliCompressor",
34
+ "NoOpCompressor",
35
+ "ContextVarProvider",
36
+ "FileDeadLetterQueue",
37
+ "MemoryDeadLetterQueue",
38
+ "LogFilter",
39
+ "RuntimeInterceptor",
40
+ "MemoryTracker",
41
+ "MetricsCollector",
42
+ "LokiJsonTransport",
43
+ "LokiProtobufTransport",
44
+ "ResilientTransport",
45
+ "ResilientTransportConfig",
46
+ "TransportFactory",
47
+ "WorkerPool",
48
+ ]
@@ -0,0 +1,3 @@
1
+ from .memory_buffer import MemoryBuffer
2
+
3
+ __all__ = ["MemoryBuffer"]
@@ -0,0 +1,187 @@
1
+ from __future__ import annotations
2
+
3
+ import threading
4
+ import time
5
+ from collections.abc import Callable
6
+ from datetime import datetime
7
+
8
+ from ...config import ResolvedBufferConfig
9
+ from ...domain.entities import LogEntryEntity
10
+ from ..memory.memory_tracker import MemoryTracker
11
+
12
+
13
+ class MemoryBuffer:
14
+ def __init__(self, config: ResolvedBufferConfig) -> None:
15
+ self._config = config
16
+ self._entries: list[LogEntryEntity] = []
17
+ self._last_flush_time = time.time()
18
+ self._flush_timer: threading.Timer | None = None
19
+ self._memory_tracker = MemoryTracker()
20
+ self._flush_callback: Callable[[], None] | None = None
21
+ self._dropped_entries = 0
22
+ self._destroyed = False
23
+ self._lock = threading.RLock()
24
+
25
+ if self._config.auto_flush:
26
+ self._schedule_flush()
27
+
28
+ def set_flush_callback(self, callback: Callable[[], None]) -> None:
29
+ self._flush_callback = callback
30
+
31
+ def add(self, entry: LogEntryEntity) -> None:
32
+ with self._lock:
33
+ if self._destroyed:
34
+ return
35
+
36
+ self._entries.append(entry)
37
+ self._memory_tracker.add_entry(entry)
38
+
39
+ self._enforce_max_size()
40
+
41
+ if self._memory_tracker.get_total_size_mb() > self._config.max_memory_mb:
42
+ self._remove_old_entries()
43
+ self._enforce_max_size()
44
+
45
+ if self._config.auto_flush:
46
+ self._schedule_flush()
47
+
48
+ if len(self._entries) >= self._config.max_size and self._config.auto_flush:
49
+ self._trigger_immediate_flush()
50
+
51
+ def flush(self) -> list[LogEntryEntity]:
52
+ with self._lock:
53
+ if self._flush_timer:
54
+ self._flush_timer.cancel()
55
+ self._flush_timer = None
56
+
57
+ flushed = list(self._entries)
58
+ self._memory_tracker.remove_entries(flushed)
59
+ self._entries.clear()
60
+ self._last_flush_time = time.time()
61
+
62
+ if self._config.auto_flush and not self._destroyed:
63
+ self._schedule_flush()
64
+
65
+ return flushed
66
+
67
+ def peek(self) -> list[LogEntryEntity]:
68
+ with self._lock:
69
+ return list(self._entries)
70
+
71
+ def size(self) -> int:
72
+ with self._lock:
73
+ return len(self._entries)
74
+
75
+ def is_full(self) -> bool:
76
+ with self._lock:
77
+ return len(self._entries) >= self._config.max_size
78
+
79
+ def should_flush(self) -> bool:
80
+ with self._lock:
81
+ elapsed_ms = int((time.time() - self._last_flush_time) * 1000)
82
+ return len(self._entries) >= self._config.max_size or (
83
+ self._config.auto_flush and elapsed_ms >= self._config.flush_interval
84
+ )
85
+
86
+ def clear(self) -> None:
87
+ with self._lock:
88
+ self._memory_tracker.remove_entries(self._entries)
89
+ self._entries.clear()
90
+ if self._flush_timer:
91
+ self._flush_timer.cancel()
92
+ self._flush_timer = None
93
+
94
+ def destroy(self) -> None:
95
+ with self._lock:
96
+ self._destroyed = True
97
+ if self._flush_timer:
98
+ self._flush_timer.cancel()
99
+ self._flush_timer = None
100
+ self.clear()
101
+ self._memory_tracker.reset()
102
+ self._flush_callback = None
103
+
104
+ def get_metrics(self) -> dict[str, float | int | None]:
105
+ with self._lock:
106
+ oldest = self._entries[0].timestamp if self._entries else None
107
+ newest = self._entries[-1].timestamp if self._entries else None
108
+ oldest_ts = self._to_timestamp(oldest)
109
+ newest_ts = self._to_timestamp(newest)
110
+ return {
111
+ "size": len(self._entries),
112
+ "max_size": self._config.max_size,
113
+ "oldest_entry": oldest_ts,
114
+ "newest_entry": newest_ts,
115
+ "memory_usage_mb": self._memory_tracker.get_total_size_mb(),
116
+ "dropped_entries": self._dropped_entries,
117
+ }
118
+
119
+ def _enforce_max_size(self) -> None:
120
+ if len(self._entries) <= self._config.max_size:
121
+ return
122
+ remove_count = len(self._entries) - self._config.max_size
123
+ self._drop_oldest(remove_count)
124
+
125
+ def _drop_oldest(self, count: int) -> None:
126
+ if count <= 0 or not self._entries:
127
+ return
128
+ dropped = self._entries[:count]
129
+ self._entries = self._entries[count:]
130
+ self._memory_tracker.remove_entries(dropped)
131
+ self._dropped_entries += len(dropped)
132
+
133
+ def _trigger_immediate_flush(self) -> None:
134
+ if self._destroyed or self._flush_callback is None:
135
+ return
136
+ callback = self._flush_callback
137
+ timer = threading.Timer(0.001, callback)
138
+ timer.daemon = True
139
+ timer.start()
140
+
141
+ def _remove_old_entries(self) -> None:
142
+ now = time.time()
143
+ max_age_s = self._config.max_age / 1000
144
+
145
+ kept: list[LogEntryEntity] = []
146
+ removed: list[LogEntryEntity] = []
147
+ for entry in self._entries:
148
+ ts = self._to_timestamp(entry.timestamp)
149
+ if ts is None or (now - ts) < max_age_s:
150
+ kept.append(entry)
151
+ else:
152
+ removed.append(entry)
153
+
154
+ self._entries = kept
155
+ if removed:
156
+ self._memory_tracker.remove_entries(removed)
157
+ self._dropped_entries += len(removed)
158
+
159
+ if self._memory_tracker.get_total_size_mb() > self._config.max_memory_mb and self._entries:
160
+ remove_count = max(1, len(self._entries) // 10)
161
+ self._drop_oldest(remove_count)
162
+
163
+ def _schedule_flush(self) -> None:
164
+ if self._destroyed or self._flush_timer is not None:
165
+ return
166
+
167
+ def _on_timer() -> None:
168
+ with self._lock:
169
+ self._flush_timer = None
170
+ if self._destroyed:
171
+ return
172
+ if self._flush_callback and self._entries:
173
+ self._flush_callback()
174
+
175
+ self._flush_timer = threading.Timer(self._config.flush_interval / 1000, _on_timer)
176
+ self._flush_timer.daemon = True
177
+ self._flush_timer.start()
178
+
179
+ @staticmethod
180
+ def _to_timestamp(value: str | None) -> float | None:
181
+ if value is None:
182
+ return None
183
+ try:
184
+ normalized = value.replace("Z", "+00:00")
185
+ return datetime.fromisoformat(normalized).timestamp()
186
+ except Exception:
187
+ return None
@@ -0,0 +1,3 @@
1
+ from .circuit_breaker import CircuitBreaker
2
+
3
+ __all__ = ["CircuitBreaker"]