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.
- elven_logs_interceptor_python-0.1.2.dist-info/METADATA +262 -0
- elven_logs_interceptor_python-0.1.2.dist-info/RECORD +56 -0
- elven_logs_interceptor_python-0.1.2.dist-info/WHEEL +4 -0
- logs_interceptor/__init__.py +333 -0
- logs_interceptor/application/__init__.py +27 -0
- logs_interceptor/application/config_service.py +232 -0
- logs_interceptor/application/log_service.py +383 -0
- logs_interceptor/config.py +190 -0
- logs_interceptor/domain/__init__.py +25 -0
- logs_interceptor/domain/entities.py +41 -0
- logs_interceptor/domain/interfaces.py +149 -0
- logs_interceptor/domain/value_objects.py +40 -0
- logs_interceptor/infrastructure/__init__.py +48 -0
- logs_interceptor/infrastructure/buffer/__init__.py +3 -0
- logs_interceptor/infrastructure/buffer/memory_buffer.py +187 -0
- logs_interceptor/infrastructure/circuit_breaker/__init__.py +3 -0
- logs_interceptor/infrastructure/circuit_breaker/circuit_breaker.py +110 -0
- logs_interceptor/infrastructure/compression/__init__.py +14 -0
- logs_interceptor/infrastructure/compression/base.py +20 -0
- logs_interceptor/infrastructure/compression/brotli_compressor.py +27 -0
- logs_interceptor/infrastructure/compression/factory.py +18 -0
- logs_interceptor/infrastructure/compression/gzip_compressor.py +20 -0
- logs_interceptor/infrastructure/compression/noop_compressor.py +14 -0
- logs_interceptor/infrastructure/context/__init__.py +3 -0
- logs_interceptor/infrastructure/context/context_provider.py +44 -0
- logs_interceptor/infrastructure/dlq/__init__.py +4 -0
- logs_interceptor/infrastructure/dlq/file_dlq.py +170 -0
- logs_interceptor/infrastructure/dlq/memory_dlq.py +59 -0
- logs_interceptor/infrastructure/filter/__init__.py +3 -0
- logs_interceptor/infrastructure/filter/log_filter.py +55 -0
- logs_interceptor/infrastructure/interceptors/__init__.py +3 -0
- logs_interceptor/infrastructure/interceptors/runtime_interceptor.py +139 -0
- logs_interceptor/infrastructure/memory/__init__.py +3 -0
- logs_interceptor/infrastructure/memory/memory_tracker.py +95 -0
- logs_interceptor/infrastructure/metrics/__init__.py +3 -0
- logs_interceptor/infrastructure/metrics/metrics_collector.py +104 -0
- logs_interceptor/infrastructure/transport/__init__.py +12 -0
- logs_interceptor/infrastructure/transport/loki_json_transport.py +226 -0
- logs_interceptor/infrastructure/transport/loki_protobuf_transport.py +209 -0
- logs_interceptor/infrastructure/transport/resilient_transport.py +161 -0
- logs_interceptor/infrastructure/transport/transport_factory.py +39 -0
- logs_interceptor/infrastructure/workers/__init__.py +3 -0
- logs_interceptor/infrastructure/workers/worker_pool.py +57 -0
- logs_interceptor/integrations/__init__.py +17 -0
- logs_interceptor/integrations/celery.py +53 -0
- logs_interceptor/integrations/django.py +44 -0
- logs_interceptor/integrations/fastapi.py +53 -0
- logs_interceptor/integrations/flask.py +50 -0
- logs_interceptor/integrations/logging_handler.py +43 -0
- logs_interceptor/integrations/loguru.py +36 -0
- logs_interceptor/integrations/structlog.py +21 -0
- logs_interceptor/preload.py +61 -0
- logs_interceptor/presentation/__init__.py +3 -0
- logs_interceptor/presentation/factory.py +128 -0
- logs_interceptor/types.py +89 -0
- 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,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
|