yapl-kit 0.1.0__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 (53) hide show
  1. yapl/__init__.py +97 -0
  2. yapl/config.py +266 -0
  3. yapl/extensions/buffering/__init__.py +24 -0
  4. yapl/extensions/buffering/capture.py +66 -0
  5. yapl/extensions/buffering/context.py +162 -0
  6. yapl/extensions/buffering/flush.py +71 -0
  7. yapl/extensions/buffering/models.py +65 -0
  8. yapl/extensions/coloring/__init__.py +45 -0
  9. yapl/extensions/coloring/colors.py +86 -0
  10. yapl/extensions/coloring/models.py +121 -0
  11. yapl/extensions/coloring/plain.py +113 -0
  12. yapl/extensions/coloring/policy.py +31 -0
  13. yapl/extensions/coloring/registry.py +119 -0
  14. yapl/extensions/coloring/structured.py +100 -0
  15. yapl/extensions/coloring/text.py +36 -0
  16. yapl/extensions/coloring/themes.py +111 -0
  17. yapl/extensions/formatting/__init__.py +54 -0
  18. yapl/extensions/formatting/_helpers.py +53 -0
  19. yapl/extensions/formatting/_types.py +21 -0
  20. yapl/extensions/formatting/formatters.py +115 -0
  21. yapl/extensions/formatting/json.py +6 -0
  22. yapl/extensions/formatting/logfmt.py +6 -0
  23. yapl/extensions/formatting/plain.py +103 -0
  24. yapl/extensions/formatting/processors.py +391 -0
  25. yapl/extensions/formatting/structured.py +656 -0
  26. yapl/extensions/formatting/text.py +6 -0
  27. yapl/extensions/queueing/__init__.py +25 -0
  28. yapl/extensions/queueing/handler.py +276 -0
  29. yapl/extensions/queueing/models.py +126 -0
  30. yapl/extensions/queueing/policy.py +22 -0
  31. yapl/extensions/queueing/queue.py +101 -0
  32. yapl/extensions/queueing/worker.py +50 -0
  33. yapl/extensions/webhook/__init__.py +54 -0
  34. yapl/extensions/webhook/client.py +159 -0
  35. yapl/extensions/webhook/handler.py +244 -0
  36. yapl/extensions/webhook/models.py +210 -0
  37. yapl/extensions/webhook/payloads.py +252 -0
  38. yapl/factory.py +367 -0
  39. yapl/filters/__init__.py +22 -0
  40. yapl/filters/enrichment.py +116 -0
  41. yapl/filters/suppression.py +69 -0
  42. yapl/levels.py +134 -0
  43. yapl/logger.py +247 -0
  44. yapl/py.typed +1 -0
  45. yapl/stack/__init__.py +14 -0
  46. yapl/stack/callsite.py +75 -0
  47. yapl/stack/tracer.py +157 -0
  48. yapl/types.py +41 -0
  49. yapl_kit/__init__.py +2 -0
  50. yapl_kit/py.typed +1 -0
  51. yapl_kit-0.1.0.dist-info/METADATA +345 -0
  52. yapl_kit-0.1.0.dist-info/RECORD +53 -0
  53. yapl_kit-0.1.0.dist-info/WHEEL +4 -0
yapl/__init__.py ADDED
@@ -0,0 +1,97 @@
1
+ """YAPL's small, typed public logging setup API.
2
+
3
+ Typical use is ``setup_logging(...)`` once during application startup followed
4
+ by ``get_logger(__name__)`` in modules that emit records. Queue and webhook
5
+ configuration classes are re-exported here for ergonomic setup; specialised
6
+ handlers and formatters remain in ``yapl.extensions``.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from .config import (
12
+ ConsoleConfig,
13
+ LoggerOverrides,
14
+ OutputConfig,
15
+ StackConfig,
16
+ SuppressionRule,
17
+ SuppressionRules,
18
+ YaplConfig,
19
+ )
20
+ from .extensions.queueing.models import QueueDiagnostics, QueueingConfig
21
+ from .extensions.webhook.models import (
22
+ WebhookAuthConfig,
23
+ WebhookConfig,
24
+ WebhookDiagnostics,
25
+ WebhookFailurePolicy,
26
+ WebhookMethod,
27
+ WebhookPayloadMode,
28
+ WebhookProvider,
29
+ WebhookRetryConfig,
30
+ )
31
+ from .factory import get_logger, setup_logging, shutdown_logging
32
+ from .levels import (
33
+ level_name,
34
+ register_default_levels,
35
+ register_level,
36
+ registered_levels,
37
+ resolve_level,
38
+ )
39
+ from .logger import (
40
+ RecordFlagPolicy,
41
+ YaplLogger,
42
+ register_level_method,
43
+ set_as_default_logger_class,
44
+ )
45
+ from .types import (
46
+ ColorMode,
47
+ ConsoleStream,
48
+ ContextEnrichValue,
49
+ LevelLike,
50
+ LevelMode,
51
+ OutputDestination,
52
+ OutputFormat,
53
+ QueueOverflowPolicy,
54
+ StackMode,
55
+ )
56
+
57
+
58
+ __all__ = [
59
+ "ColorMode",
60
+ "ConsoleConfig",
61
+ "ConsoleStream",
62
+ "ContextEnrichValue",
63
+ "LevelLike",
64
+ "LevelMode",
65
+ "LoggerOverrides",
66
+ "OutputConfig",
67
+ "OutputDestination",
68
+ "OutputFormat",
69
+ "QueueDiagnostics",
70
+ "QueueOverflowPolicy",
71
+ "QueueingConfig",
72
+ "RecordFlagPolicy",
73
+ "StackConfig",
74
+ "StackMode",
75
+ "SuppressionRule",
76
+ "SuppressionRules",
77
+ "WebhookAuthConfig",
78
+ "WebhookConfig",
79
+ "WebhookDiagnostics",
80
+ "WebhookFailurePolicy",
81
+ "WebhookMethod",
82
+ "WebhookPayloadMode",
83
+ "WebhookProvider",
84
+ "WebhookRetryConfig",
85
+ "YaplConfig",
86
+ "YaplLogger",
87
+ "get_logger",
88
+ "level_name",
89
+ "register_default_levels",
90
+ "register_level",
91
+ "register_level_method",
92
+ "registered_levels",
93
+ "resolve_level",
94
+ "set_as_default_logger_class",
95
+ "setup_logging",
96
+ "shutdown_logging",
97
+ ]
yapl/config.py ADDED
@@ -0,0 +1,266 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable, Mapping
4
+ from dataclasses import dataclass, field
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ from .levels import resolve_level
8
+ from .types import (
9
+ ColorMode,
10
+ ConsoleStream,
11
+ LevelLike,
12
+ OutputDestination,
13
+ OutputFormat,
14
+ QueueOverflowPolicy,
15
+ StackMode,
16
+ )
17
+
18
+
19
+ if TYPE_CHECKING:
20
+ from .extensions.formatting.processors import LogEventProcessor
21
+ from .extensions.webhook.models import WebhookConfig
22
+
23
+
24
+ @dataclass(frozen=True, slots=True)
25
+ class SuppressionRule:
26
+ """Match records that should be suppressed before they reach a sink.
27
+
28
+ Every non-``None`` field must match. ``logger_prefix`` changes ``logger``
29
+ from an exact logger name to a prefix match.
30
+
31
+ Attributes:
32
+ logger: Logger name to match.
33
+ logger_prefix: Whether ``logger`` matches descendant logger names.
34
+ filename: Source filename to match.
35
+ function: Source function name to match.
36
+ line: Source line number to match.
37
+ message_contains: Text that must occur in the rendered message.
38
+ """
39
+
40
+ logger: str | None = None
41
+ logger_prefix: bool = False
42
+
43
+ filename: str | None = None
44
+ function: str | None = None
45
+ line: int | None = None
46
+ message_contains: str | None = None
47
+
48
+
49
+ @dataclass(slots=True)
50
+ class SuppressionRules:
51
+ """Mutable collection of :class:`SuppressionRule` instances.
52
+
53
+ The fluent helpers return this collection so rules can be assembled while
54
+ building a :class:`YaplConfig`.
55
+ """
56
+
57
+ rules: list[SuppressionRule] = field(default_factory=list)
58
+
59
+ def drop(
60
+ self,
61
+ *,
62
+ logger: str | None = None,
63
+ logger_prefix: bool = False,
64
+ filename: str | None = None,
65
+ function: str | None = None,
66
+ line: int | None = None,
67
+ message_contains: str | None = None,
68
+ ) -> SuppressionRules:
69
+ """Append one suppression rule and return this collection.
70
+
71
+ Args:
72
+ logger: Logger name to match.
73
+ logger_prefix: Match descendant logger names when ``True``.
74
+ filename: Source filename to match.
75
+ function: Source function name to match.
76
+ line: Source line number to match.
77
+ message_contains: Text that must occur in the rendered message.
78
+ """
79
+ self.rules.append(
80
+ SuppressionRule(
81
+ logger=logger,
82
+ logger_prefix=logger_prefix,
83
+ filename=filename,
84
+ function=function,
85
+ line=line,
86
+ message_contains=message_contains,
87
+ )
88
+ )
89
+ return self
90
+
91
+ def extend(self, items: Iterable[SuppressionRule]) -> SuppressionRules:
92
+ """Append ``items`` and return this collection."""
93
+ self.rules.extend(items)
94
+ return self
95
+
96
+
97
+ @dataclass(slots=True)
98
+ class LoggerOverrides:
99
+ """Per-logger level overrides applied by :func:`yapl.setup_logging`."""
100
+
101
+ levels: dict[str, int] = field(default_factory=dict)
102
+
103
+ def set(self, logger_name: str, level: LevelLike) -> LoggerOverrides:
104
+ """Set ``logger_name`` to ``level`` and return this collection.
105
+
106
+ Raises:
107
+ ValueError: If a string level is not registered.
108
+ """
109
+ self.levels[logger_name] = resolve_level(level)
110
+ return self
111
+
112
+ def update(self, mapping: Mapping[str, LevelLike]) -> LoggerOverrides:
113
+ """Apply several logger-level overrides and return this collection."""
114
+ for k, v in mapping.items():
115
+ self.set(k, v)
116
+ return self
117
+
118
+
119
+ @dataclass(frozen=True, slots=True)
120
+ class ConsoleConfig:
121
+ """Configuration for YAPL's default console output.
122
+
123
+ Attributes:
124
+ enabled: Whether a console sink is created when no explicit outputs exist.
125
+ stream: Standard stream used by the console sink.
126
+ level: Optional sink-level override; the root level is used by default.
127
+ color: Colour policy for text output.
128
+ theme_name: Optional registered colour theme name.
129
+ """
130
+
131
+ enabled: bool = True
132
+ stream: ConsoleStream = "stdout"
133
+ level: LevelLike | None = None
134
+ color: ColorMode = "never"
135
+ theme_name: str | None = None
136
+
137
+
138
+ @dataclass(frozen=True, slots=True)
139
+ class OutputConfig:
140
+ """Describe one built-in output sink.
141
+
142
+ Attributes:
143
+ destination: ``"stdout"``, ``"stderr"``, ``"file"``, or ``"webhook"``.
144
+ format: Built-in formatter used by the sink.
145
+ level: Optional minimum level for this sink.
146
+ color: Colour policy; supported only by text console outputs.
147
+ file_path: UTF-8 file path required for ``destination="file"``.
148
+ theme_name: Optional registered colour theme name.
149
+
150
+ Raises:
151
+ ValueError: If colour is requested for an unsupported sink or format.
152
+ """
153
+
154
+ destination: OutputDestination
155
+ format: OutputFormat = "text"
156
+ level: LevelLike | None = None
157
+ color: ColorMode = "never"
158
+ file_path: str | None = None
159
+ theme_name: str | None = None
160
+
161
+ def __post_init__(self) -> None:
162
+ if self.destination not in {"stdout", "stderr"} and self.color != "never":
163
+ raise ValueError("color is only supported for stdout and stderr outputs")
164
+ if self.color != "never" and self.format not in {"text", "plain"}:
165
+ raise ValueError("color is only supported with text or plain output")
166
+
167
+
168
+ @dataclass(frozen=True, slots=True)
169
+ class StackConfig:
170
+ """Control the stack metadata collected by YAPL formatters.
171
+
172
+ ``"record"`` preserves the record callsite; ``"full"`` additionally
173
+ captures a rendered stack when requested by the configured stack helpers.
174
+ """
175
+
176
+ mode: StackMode = "record"
177
+
178
+
179
+ @dataclass(slots=True)
180
+ class YaplConfig:
181
+ """Top-level configuration accepted by :func:`yapl.setup_logging`.
182
+
183
+ Most applications only need ``level`` and the default console settings.
184
+ When queueing is enabled (the default), built-in sinks run in a background
185
+ worker and queue insertion does not intentionally wait. Queue capacity is
186
+ measured in records, not bytes, so large arguments or ``extra`` values can
187
+ still retain substantial memory while queued.
188
+
189
+ Attributes:
190
+ level: Root logging level; defaults to ``logging.INFO`` (20).
191
+ console: Default console-sink settings.
192
+ stack: Stack metadata policy.
193
+ outputs: Legacy shorthand output destinations used when ``sinks`` is empty.
194
+ format: Formatter for shorthand outputs.
195
+ sinks: Explicit built-in output definitions.
196
+ queue_enabled: Put configured sinks behind a bounded queue.
197
+ queue_max_size: Maximum queued records, not a byte limit.
198
+ queue_overflow_policy: Action on a full queue; ``"block"`` may wait.
199
+ processors: Structured-event processors run by compatible formatters.
200
+ resource: Static fields added to structured log events.
201
+ webhook: Optional configuration for a ``"webhook"`` output.
202
+ file_path: File used by shorthand ``"file"`` output.
203
+ overrides: Per-logger level overrides.
204
+ suppression: Rules applied to configured handlers.
205
+ """
206
+
207
+ level: int = 20
208
+ console: ConsoleConfig = field(default_factory=ConsoleConfig)
209
+ stack: StackConfig = field(default_factory=StackConfig)
210
+
211
+ outputs: tuple[OutputDestination, ...] = ()
212
+ format: OutputFormat = "text"
213
+ sinks: tuple[OutputConfig, ...] = ()
214
+ queue_enabled: bool = True
215
+ queue_max_size: int = 10_000
216
+ queue_overflow_policy: QueueOverflowPolicy = "drop_debug_only"
217
+ processors: tuple[LogEventProcessor, ...] = field(default_factory=tuple)
218
+ resource: Mapping[str, Any] = field(default_factory=dict)
219
+ webhook: WebhookConfig | None = None
220
+ file_path: str | None = None
221
+
222
+ overrides: LoggerOverrides = field(default_factory=LoggerOverrides)
223
+ suppression: SuppressionRules = field(default_factory=SuppressionRules)
224
+
225
+ def with_level(self, level: LevelLike) -> YaplConfig:
226
+ """Set the root level and return this configuration.
227
+
228
+ Raises:
229
+ ValueError: If a string level is not registered.
230
+ """
231
+ self.level = resolve_level(level)
232
+ return self
233
+
234
+ def with_console_level(self, level: LevelLike | None) -> YaplConfig:
235
+ """Set the default console sink level and return this configuration."""
236
+ self.console = ConsoleConfig(
237
+ enabled=self.console.enabled,
238
+ stream=self.console.stream,
239
+ level=level,
240
+ color=self.console.color,
241
+ theme_name=self.console.theme_name,
242
+ )
243
+ return self
244
+
245
+ def with_console(
246
+ self,
247
+ *,
248
+ enabled: bool | None = None,
249
+ stream: ConsoleStream | None = None,
250
+ color: ColorMode | None = None,
251
+ theme_name: str | None = None,
252
+ ) -> YaplConfig:
253
+ """Update selected default console options and return this configuration."""
254
+ self.console = ConsoleConfig(
255
+ enabled=self.console.enabled if enabled is None else enabled,
256
+ stream=self.console.stream if stream is None else stream,
257
+ level=self.console.level,
258
+ color=self.console.color if color is None else color,
259
+ theme_name=self.console.theme_name if theme_name is None else theme_name,
260
+ )
261
+ return self
262
+
263
+ def with_stack(self, mode: StackMode) -> YaplConfig:
264
+ """Set the stack metadata policy and return this configuration."""
265
+ self.stack = StackConfig(mode=mode)
266
+ return self
@@ -0,0 +1,24 @@
1
+ """Context-local capture and replay of standard logging records."""
2
+
3
+ from .capture import BufferCaptureFilter
4
+ from .context import BufferedLogs, BufferingFilter, install_buffering
5
+ from .models import (
6
+ BufferedLogEntry,
7
+ BufferedLogStore,
8
+ BufferExitBehavior,
9
+ BufferFlushMode,
10
+ BufferingConfig,
11
+ )
12
+
13
+
14
+ __all__ = [
15
+ "BufferCaptureFilter",
16
+ "BufferExitBehavior",
17
+ "BufferFlushMode",
18
+ "BufferedLogEntry",
19
+ "BufferedLogStore",
20
+ "BufferedLogs",
21
+ "BufferingConfig",
22
+ "BufferingFilter",
23
+ "install_buffering",
24
+ ]
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+
3
+ from contextvars import ContextVar, Token
4
+ from copy import copy
5
+ import logging
6
+
7
+ from .models import BufferedLogEntry, BufferingState
8
+
9
+
10
+ _active_buffer_state: ContextVar[BufferingState | None] = ContextVar(
11
+ "yapl_buffering_active_state",
12
+ default=None,
13
+ )
14
+
15
+ _capture_enabled: ContextVar[bool] = ContextVar(
16
+ "yapl_buffering_capture_enabled",
17
+ default=True,
18
+ )
19
+
20
+
21
+ def get_active_buffer_state() -> BufferingState | None:
22
+ return _active_buffer_state.get()
23
+
24
+
25
+ def set_active_buffer_state(state: BufferingState | None) -> Token[BufferingState | None]:
26
+ return _active_buffer_state.set(state)
27
+
28
+
29
+ def reset_active_buffer_state(token: Token[BufferingState | None]) -> None:
30
+ _active_buffer_state.reset(token)
31
+
32
+
33
+ def is_capture_enabled() -> bool:
34
+ return _capture_enabled.get()
35
+
36
+
37
+ def set_capture_enabled(enabled: bool) -> Token[bool]:
38
+ return _capture_enabled.set(enabled)
39
+
40
+
41
+ def reset_capture_enabled(token: Token[bool]) -> None:
42
+ _capture_enabled.reset(token)
43
+
44
+
45
+ class BufferCaptureFilter(logging.Filter):
46
+ """Copy records into the active context-local buffer exactly once."""
47
+
48
+ def filter(self, record: logging.LogRecord) -> bool:
49
+ """Capture ``record`` when buffering is active, then allow it onward."""
50
+ if not is_capture_enabled():
51
+ return True
52
+
53
+ if getattr(record, "_yapl_buffer_capture_seen", False):
54
+ return True
55
+
56
+ state = get_active_buffer_state()
57
+ if state is None:
58
+ return True
59
+
60
+ buffered_record = copy(record)
61
+ state.store.add(
62
+ BufferedLogEntry(record=buffered_record),
63
+ max_entries=state.config.max_entries,
64
+ )
65
+ record._yapl_buffer_capture_seen = True
66
+ return True
@@ -0,0 +1,162 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterator
4
+ import logging
5
+ from types import TracebackType
6
+ from typing import Self
7
+
8
+ from .capture import (
9
+ BufferCaptureFilter,
10
+ get_active_buffer_state,
11
+ is_capture_enabled,
12
+ reset_active_buffer_state,
13
+ reset_capture_enabled,
14
+ set_active_buffer_state,
15
+ set_capture_enabled,
16
+ )
17
+ from .flush import discard_buffer_store, flush_buffer_store
18
+ from .models import BufferedLogStore, BufferingConfig, BufferingState
19
+
20
+
21
+ class BufferedLogs:
22
+ """Capture records in a context and replay or discard them on exit.
23
+
24
+ Context state uses :mod:`contextvars`, so independent asyncio tasks and
25
+ threads do not share buffers accidentally. Nested contexts share the outer
26
+ buffer by default; set ``include_nested_contexts=False`` for isolation.
27
+ """
28
+
29
+ def __init__(self, config: BufferingConfig | None = None) -> None:
30
+ """Create an inactive context with the supplied buffering policy."""
31
+ self._cfg = config or BufferingConfig()
32
+ self._store = BufferedLogStore()
33
+ self._state = BufferingState(store=self._store, config=self._cfg)
34
+
35
+ self._token = None
36
+ self._previous_state: BufferingState | None = None
37
+ self._owns_state: bool = False
38
+
39
+ @property
40
+ def store(self) -> BufferedLogStore:
41
+ """Return the mutable store owned by this context."""
42
+ return self._store
43
+
44
+ @property
45
+ def config(self) -> BufferingConfig:
46
+ """Return the active buffering configuration."""
47
+ return self._cfg
48
+
49
+ @property
50
+ def records(self) -> list[logging.LogRecord]:
51
+ """Return a snapshot list of records currently retained by the context."""
52
+ return [entry.record for entry in self._store.entries]
53
+
54
+ def __len__(self) -> int:
55
+ return len(self._store.entries)
56
+
57
+ def __iter__(self) -> Iterator[logging.LogRecord]:
58
+ """Iterate over currently buffered records in capture order."""
59
+ for entry in self._store.entries:
60
+ yield entry.record
61
+
62
+ def __enter__(self) -> Self:
63
+ """Activate this buffer for the current context."""
64
+ return self._enter()
65
+
66
+ def __exit__(
67
+ self,
68
+ exc_type: type[BaseException] | None,
69
+ exc: BaseException | None,
70
+ tb: TracebackType | None,
71
+ ) -> bool:
72
+ """Flush or discard records according to the configured exit policy."""
73
+ return self._exit(exc_type)
74
+
75
+ async def __aenter__(self) -> Self:
76
+ """Activate this buffer for an asynchronous context manager block."""
77
+ return self._enter()
78
+
79
+ async def __aexit__(
80
+ self,
81
+ exc_type: type[BaseException] | None,
82
+ exc: BaseException | None,
83
+ tb: TracebackType | None,
84
+ ) -> bool:
85
+ """Flush or discard records when leaving an async context manager block."""
86
+ return self._exit(exc_type)
87
+
88
+ def _enter(self) -> Self:
89
+ self._previous_state = get_active_buffer_state()
90
+
91
+ if self._previous_state is not None and self._cfg.include_nested_contexts:
92
+ self._state = self._previous_state
93
+ self._store = self._state.store
94
+ self._cfg = self._state.config
95
+ self._token = None
96
+ self._owns_state = False
97
+ return self
98
+
99
+ self._token = set_active_buffer_state(self._state)
100
+ self._owns_state = True
101
+ return self
102
+
103
+ def _exit(self, exc_type: type[BaseException] | None) -> bool:
104
+ if not self._owns_state:
105
+ return False
106
+
107
+ capture_token = set_capture_enabled(False)
108
+
109
+ try:
110
+ if self._should_flush(exc_type):
111
+ flush_buffer_store(
112
+ self._store,
113
+ flush_mode=self._cfg.flush_mode,
114
+ )
115
+ else:
116
+ discard_buffer_store(self._store)
117
+ finally:
118
+ if capture_token is not None:
119
+ reset_capture_enabled(capture_token)
120
+
121
+ if self._token is not None:
122
+ reset_active_buffer_state(self._token)
123
+
124
+ return False
125
+
126
+ def _should_flush(self, exc_type: type[BaseException] | None) -> bool:
127
+ if exc_type is None:
128
+ return self._cfg.on_success == "flush"
129
+ return self._cfg.on_error == "flush"
130
+
131
+
132
+ class BufferingFilter(logging.Filter):
133
+ """Suppress direct sink delivery while records are being captured."""
134
+
135
+ def filter(self, record: logging.LogRecord) -> bool:
136
+ """Allow records only when capture is inactive for this context."""
137
+ return (not is_capture_enabled()) or get_active_buffer_state() is None
138
+
139
+
140
+ def install_buffering(*, stream_handlers_only: bool = False) -> int:
141
+ """Install buffering filters on root handlers and return the count updated.
142
+
143
+ Install once after configuring handlers. Repeated installation adds another
144
+ filter, so retain the result of setup rather than calling it per request.
145
+ """
146
+ root = logging.getLogger()
147
+
148
+ capture_filter = BufferCaptureFilter()
149
+ root.addFilter(capture_filter)
150
+
151
+ buffering_filter = BufferingFilter()
152
+ updated = 0
153
+
154
+ for handler in root.handlers:
155
+ if stream_handlers_only and not isinstance(handler, logging.StreamHandler):
156
+ continue
157
+
158
+ handler.addFilter(capture_filter)
159
+ handler.addFilter(buffering_filter)
160
+ updated += 1
161
+
162
+ return updated
@@ -0,0 +1,71 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+
5
+ from .models import BufferedLogEntry, BufferedLogStore, BufferFlushMode
6
+
7
+
8
+ def _replay_record(record: logging.LogRecord) -> None:
9
+ logger = logging.getLogger(record.name)
10
+ logger.handle(record)
11
+
12
+
13
+ def flush_buffer_store(
14
+ store: BufferedLogStore,
15
+ *,
16
+ flush_mode: BufferFlushMode = "ordered",
17
+ ) -> int:
18
+ """Replay retained records through their named stdlib loggers.
19
+
20
+ Returns the number of replayed records. Replaying may invoke configured
21
+ handlers synchronously unless those handlers are queue-backed.
22
+ """
23
+ if store.is_empty():
24
+ return 0
25
+
26
+ entries = _prepare_entries_for_flush(store.entries, flush_mode=flush_mode)
27
+
28
+ for entry in entries:
29
+ _replay_record(entry.record)
30
+
31
+ flushed = len(entries)
32
+ store.clear()
33
+ return flushed
34
+
35
+
36
+ def discard_buffer_store(store: BufferedLogStore) -> int:
37
+ """Discard all retained records and return their count."""
38
+ discarded = len(store)
39
+ store.clear()
40
+ return discarded
41
+
42
+
43
+ def _prepare_entries_for_flush(
44
+ entries: list[BufferedLogEntry],
45
+ *,
46
+ flush_mode: BufferFlushMode,
47
+ ) -> list[BufferedLogEntry]:
48
+ if flush_mode == "ordered":
49
+ return list(entries)
50
+
51
+ if flush_mode == "grouped":
52
+ return _group_entries(entries)
53
+
54
+ return list(entries)
55
+
56
+
57
+ def _group_entries(entries: list[BufferedLogEntry]) -> list[BufferedLogEntry]:
58
+ grouped: dict[int, list[BufferedLogEntry]] = {}
59
+
60
+ for entry in entries:
61
+ level = entry.record.levelno
62
+ grouped.setdefault(level, []).append(entry)
63
+
64
+ ordered_levels = sorted(grouped.keys(), reverse=True)
65
+
66
+ result: list[BufferedLogEntry] = []
67
+
68
+ for level in ordered_levels:
69
+ result.extend(grouped[level])
70
+
71
+ return result