yapl-kit 0.2.0__tar.gz → 0.2.1__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.
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/PKG-INFO +10 -32
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/PUBLIC.md +8 -25
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/pyproject.toml +5 -9
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/pyproject.toml.orig +7 -11
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/src/yapl/config.py +3 -0
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/src/yapl/extensions/buffering/capture.py +19 -1
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/src/yapl/extensions/buffering/context.py +15 -2
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/src/yapl/extensions/buffering/models.py +1 -0
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/src/yapl/extensions/coloring/models.py +1 -1
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/src/yapl/extensions/coloring/plain.py +14 -3
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/src/yapl/extensions/formatting/processors.py +6 -1
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/src/yapl/extensions/queueing/__init__.py +2 -0
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/src/yapl/extensions/queueing/handler.py +88 -9
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/src/yapl/extensions/queueing/models.py +27 -1
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/src/yapl/extensions/webhook/__init__.py +1 -5
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/src/yapl/extensions/webhook/client.py +7 -1
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/src/yapl/extensions/webhook/handler.py +3 -0
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/src/yapl/extensions/webhook/models.py +10 -3
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/src/yapl/extensions/webhook/payloads.py +16 -10
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/src/yapl/factory.py +173 -106
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/src/yapl/filters/suppression.py +1 -1
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/src/yapl/stack/tracer.py +111 -14
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/src/yapl/status.py +2 -2
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/src/yapl/__init__.py +0 -0
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/src/yapl/context.py +0 -0
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/src/yapl/extensions/buffering/__init__.py +0 -0
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/src/yapl/extensions/buffering/flush.py +0 -0
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/src/yapl/extensions/coloring/__init__.py +0 -0
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/src/yapl/extensions/coloring/colors.py +0 -0
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/src/yapl/extensions/coloring/policy.py +0 -0
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/src/yapl/extensions/coloring/registry.py +0 -0
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/src/yapl/extensions/coloring/structured.py +0 -0
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/src/yapl/extensions/coloring/text.py +0 -0
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/src/yapl/extensions/coloring/themes.py +0 -0
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/src/yapl/extensions/formatting/__init__.py +0 -0
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/src/yapl/extensions/formatting/_helpers.py +0 -0
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/src/yapl/extensions/formatting/_types.py +0 -0
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/src/yapl/extensions/formatting/formatters.py +0 -0
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/src/yapl/extensions/formatting/json.py +0 -0
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/src/yapl/extensions/formatting/logfmt.py +0 -0
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/src/yapl/extensions/formatting/plain.py +0 -0
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/src/yapl/extensions/formatting/structured.py +0 -0
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/src/yapl/extensions/formatting/text.py +0 -0
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/src/yapl/extensions/queueing/policy.py +0 -0
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/src/yapl/extensions/queueing/queue.py +0 -0
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/src/yapl/extensions/queueing/worker.py +0 -0
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/src/yapl/filters/__init__.py +0 -0
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/src/yapl/filters/enrichment.py +0 -0
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/src/yapl/levels.py +0 -0
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/src/yapl/logger.py +0 -0
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/src/yapl/py.typed +0 -0
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/src/yapl/stack/__init__.py +0 -0
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/src/yapl/stack/callsite.py +0 -0
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/src/yapl/testing.py +0 -0
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/src/yapl/types.py +0 -0
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/src/yapl_kit/__init__.py +0 -0
- {yapl_kit-0.2.0 → yapl_kit-0.2.1}/src/yapl_kit/py.typed +0 -0
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: yapl-kit
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: YetAnotherPythonLogger - a minimal, extensible logging toolkit built on top of Python's standard logging module.
|
|
5
5
|
Author: SirKaiMartin
|
|
6
|
-
Requires-Dist: httpx>=0.28.1
|
|
6
|
+
Requires-Dist: httpx>=0.28.1
|
|
7
7
|
Requires-Python: >=3.13
|
|
8
|
-
Provides-Extra: buffering
|
|
9
|
-
Provides-Extra: coloring
|
|
10
|
-
Provides-Extra: formatting
|
|
11
|
-
Provides-Extra: queueing
|
|
12
|
-
Provides-Extra: webhook
|
|
13
8
|
Description-Content-Type: text/markdown
|
|
14
9
|
|
|
15
10
|
# YAPL Kit
|
|
@@ -18,9 +13,10 @@ Description-Content-Type: text/markdown
|
|
|
18
13
|
|
|
19
14
|
> A modern, minimal, and extensible logging toolkit built on top of Python's standard `logging` module.
|
|
20
15
|
|
|
21
|
-
YAPL keeps the stdlib logging ecosystem intact while adding structured logging, queue-backed delivery, formatting, coloring, buffering, stack helpers, and
|
|
16
|
+
YAPL keeps the stdlib logging ecosystem intact while adding structured logging, queue-backed delivery, formatting, coloring, buffering, stack helpers, and webhook support.
|
|
22
17
|
|
|
23
|
-
|
|
18
|
+
All YAPL feature families ship in the single `yapl-kit` distribution. Webhook
|
|
19
|
+
support uses `httpx`, which is installed automatically.
|
|
24
20
|
|
|
25
21
|
## Installation
|
|
26
22
|
|
|
@@ -38,18 +34,6 @@ With `pip`:
|
|
|
38
34
|
pip install yapl-kit
|
|
39
35
|
```
|
|
40
36
|
|
|
41
|
-
Install all optional features:
|
|
42
|
-
|
|
43
|
-
```bash
|
|
44
|
-
uv add "yapl-kit[formatting,coloring,buffering,queueing,webhook]"
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
or:
|
|
48
|
-
|
|
49
|
-
```bash
|
|
50
|
-
pip install "yapl-kit[formatting,coloring,buffering,queueing,webhook]"
|
|
51
|
-
```
|
|
52
|
-
|
|
53
37
|
The canonical import namespace is `yapl`:
|
|
54
38
|
|
|
55
39
|
```python
|
|
@@ -119,7 +103,7 @@ YAPL does not replace Python logging. It builds on stdlib handlers, formatters,
|
|
|
119
103
|
|
|
120
104
|
The main design goals are:
|
|
121
105
|
|
|
122
|
-
- keep the
|
|
106
|
+
- keep the distribution small and focused
|
|
123
107
|
- preserve compatibility with Python's logging ecosystem
|
|
124
108
|
- keep expensive formatting and I/O off the application hot path
|
|
125
109
|
- make queueing, retries, and shutdown behavior bounded
|
|
@@ -225,13 +209,7 @@ Queue capacity limits the number of retained records, not their total byte size.
|
|
|
225
209
|
|
|
226
210
|
## Webhooks
|
|
227
211
|
|
|
228
|
-
Webhook delivery
|
|
229
|
-
|
|
230
|
-
Install it with:
|
|
231
|
-
|
|
232
|
-
```bash
|
|
233
|
-
uv add "yapl-kit[webhook]"
|
|
234
|
-
```
|
|
212
|
+
Webhook delivery uses `httpx`, included automatically with `yapl-kit`.
|
|
235
213
|
|
|
236
214
|
Configure a webhook:
|
|
237
215
|
|
|
@@ -339,9 +317,9 @@ Python cannot safely terminate arbitrary user handler code. Keep custom handlers
|
|
|
339
317
|
|
|
340
318
|
`queue_enabled=False` runs normal stdlib handlers directly, so file, console, or custom sink work may occur on the caller thread.
|
|
341
319
|
|
|
342
|
-
##
|
|
320
|
+
## Extension Modules
|
|
343
321
|
|
|
344
|
-
YAPL keeps
|
|
322
|
+
YAPL keeps feature families under explicit modules within the main distribution:
|
|
345
323
|
|
|
346
324
|
```python
|
|
347
325
|
from yapl.stack import FunctionCallTracer, get_callsite
|
|
@@ -352,7 +330,7 @@ from yapl.extensions.queueing import QueueingConfig
|
|
|
352
330
|
from yapl.extensions.webhook import WebhookConfig
|
|
353
331
|
```
|
|
354
332
|
|
|
355
|
-
The `yapl.extensions` package
|
|
333
|
+
The `yapl.extensions` package contains the concrete feature implementations.
|
|
356
334
|
|
|
357
335
|
## Package Philosophy
|
|
358
336
|
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
> A modern, minimal, and extensible logging toolkit built on top of Python's standard `logging` module.
|
|
6
6
|
|
|
7
|
-
YAPL keeps the stdlib logging ecosystem intact while adding structured logging, queue-backed delivery, formatting, coloring, buffering, stack helpers, and
|
|
7
|
+
YAPL keeps the stdlib logging ecosystem intact while adding structured logging, queue-backed delivery, formatting, coloring, buffering, stack helpers, and webhook support.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
All YAPL feature families ship in the single `yapl-kit` distribution. Webhook
|
|
10
|
+
support uses `httpx`, which is installed automatically.
|
|
10
11
|
|
|
11
12
|
## Installation
|
|
12
13
|
|
|
@@ -24,18 +25,6 @@ With `pip`:
|
|
|
24
25
|
pip install yapl-kit
|
|
25
26
|
```
|
|
26
27
|
|
|
27
|
-
Install all optional features:
|
|
28
|
-
|
|
29
|
-
```bash
|
|
30
|
-
uv add "yapl-kit[formatting,coloring,buffering,queueing,webhook]"
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
or:
|
|
34
|
-
|
|
35
|
-
```bash
|
|
36
|
-
pip install "yapl-kit[formatting,coloring,buffering,queueing,webhook]"
|
|
37
|
-
```
|
|
38
|
-
|
|
39
28
|
The canonical import namespace is `yapl`:
|
|
40
29
|
|
|
41
30
|
```python
|
|
@@ -105,7 +94,7 @@ YAPL does not replace Python logging. It builds on stdlib handlers, formatters,
|
|
|
105
94
|
|
|
106
95
|
The main design goals are:
|
|
107
96
|
|
|
108
|
-
- keep the
|
|
97
|
+
- keep the distribution small and focused
|
|
109
98
|
- preserve compatibility with Python's logging ecosystem
|
|
110
99
|
- keep expensive formatting and I/O off the application hot path
|
|
111
100
|
- make queueing, retries, and shutdown behavior bounded
|
|
@@ -211,13 +200,7 @@ Queue capacity limits the number of retained records, not their total byte size.
|
|
|
211
200
|
|
|
212
201
|
## Webhooks
|
|
213
202
|
|
|
214
|
-
Webhook delivery
|
|
215
|
-
|
|
216
|
-
Install it with:
|
|
217
|
-
|
|
218
|
-
```bash
|
|
219
|
-
uv add "yapl-kit[webhook]"
|
|
220
|
-
```
|
|
203
|
+
Webhook delivery uses `httpx`, included automatically with `yapl-kit`.
|
|
221
204
|
|
|
222
205
|
Configure a webhook:
|
|
223
206
|
|
|
@@ -325,9 +308,9 @@ Python cannot safely terminate arbitrary user handler code. Keep custom handlers
|
|
|
325
308
|
|
|
326
309
|
`queue_enabled=False` runs normal stdlib handlers directly, so file, console, or custom sink work may occur on the caller thread.
|
|
327
310
|
|
|
328
|
-
##
|
|
311
|
+
## Extension Modules
|
|
329
312
|
|
|
330
|
-
YAPL keeps
|
|
313
|
+
YAPL keeps feature families under explicit modules within the main distribution:
|
|
331
314
|
|
|
332
315
|
```python
|
|
333
316
|
from yapl.stack import FunctionCallTracer, get_callsite
|
|
@@ -338,7 +321,7 @@ from yapl.extensions.queueing import QueueingConfig
|
|
|
338
321
|
from yapl.extensions.webhook import WebhookConfig
|
|
339
322
|
```
|
|
340
323
|
|
|
341
|
-
The `yapl.extensions` package
|
|
324
|
+
The `yapl.extensions` package contains the concrete feature implementations.
|
|
342
325
|
|
|
343
326
|
## Package Philosophy
|
|
344
327
|
|
|
@@ -1,21 +1,14 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "yapl-kit"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.1"
|
|
4
4
|
description = "YetAnotherPythonLogger - a minimal, extensible logging toolkit built on top of Python's standard logging module."
|
|
5
5
|
readme = "PUBLIC.md"
|
|
6
6
|
requires-python = ">=3.13"
|
|
7
|
-
dependencies = []
|
|
7
|
+
dependencies = ["httpx>=0.28.1"]
|
|
8
8
|
|
|
9
9
|
[[project.authors]]
|
|
10
10
|
name = "SirKaiMartin"
|
|
11
11
|
|
|
12
|
-
[project.optional-dependencies]
|
|
13
|
-
formatting = []
|
|
14
|
-
coloring = []
|
|
15
|
-
buffering = []
|
|
16
|
-
queueing = []
|
|
17
|
-
webhook = ["httpx>=0.28.1"]
|
|
18
|
-
|
|
19
12
|
[build-system]
|
|
20
13
|
requires = ["uv_build>=0.12.16,<0.13.0"]
|
|
21
14
|
build-backend = "uv_build"
|
|
@@ -71,6 +64,9 @@ section-order = [
|
|
|
71
64
|
"local-folder",
|
|
72
65
|
]
|
|
73
66
|
|
|
67
|
+
[tool.ty.src]
|
|
68
|
+
include = ["src"]
|
|
69
|
+
|
|
74
70
|
[dependency-groups]
|
|
75
71
|
dev = [
|
|
76
72
|
"pytest>=9.1.1",
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "yapl-kit"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.1"
|
|
4
4
|
description = "YetAnotherPythonLogger - a minimal, extensible logging toolkit built on top of Python's standard logging module."
|
|
5
5
|
readme = "PUBLIC.md"
|
|
6
6
|
authors = [
|
|
7
7
|
{ name = "SirKaiMartin" }
|
|
8
8
|
]
|
|
9
9
|
requires-python = ">=3.13"
|
|
10
|
-
dependencies = [
|
|
10
|
+
dependencies = [
|
|
11
|
+
"httpx>=0.28.1",
|
|
12
|
+
]
|
|
11
13
|
|
|
12
14
|
[build-system]
|
|
13
15
|
requires = ["uv_build>=0.12.16,<0.13.0"]
|
|
@@ -30,15 +32,6 @@ markers = [
|
|
|
30
32
|
"performance: caller-latency, throughput, and backpressure regression coverage",
|
|
31
33
|
]
|
|
32
34
|
|
|
33
|
-
[project.optional-dependencies]
|
|
34
|
-
formatting = []
|
|
35
|
-
coloring = []
|
|
36
|
-
buffering = []
|
|
37
|
-
queueing = []
|
|
38
|
-
webhook = [
|
|
39
|
-
"httpx>=0.28.1",
|
|
40
|
-
]
|
|
41
|
-
|
|
42
35
|
[tool.ruff]
|
|
43
36
|
target-version = "py313"
|
|
44
37
|
line-length = 100
|
|
@@ -78,3 +71,6 @@ section-order = [
|
|
|
78
71
|
"first-party",
|
|
79
72
|
"local-folder",
|
|
80
73
|
]
|
|
74
|
+
|
|
75
|
+
[tool.ty.src]
|
|
76
|
+
include = ["src"]
|
|
@@ -18,6 +18,7 @@ from .types import (
|
|
|
18
18
|
|
|
19
19
|
if TYPE_CHECKING:
|
|
20
20
|
from .extensions.formatting.processors import LogEventProcessor
|
|
21
|
+
from .extensions.queueing.models import QueueErrorCallback
|
|
21
22
|
from .extensions.webhook.models import WebhookConfig
|
|
22
23
|
|
|
23
24
|
|
|
@@ -216,6 +217,7 @@ class YaplConfig:
|
|
|
216
217
|
file_path: File used by shorthand ``"file"`` output.
|
|
217
218
|
overrides: Per-logger level overrides.
|
|
218
219
|
suppression: Rules applied to configured handlers.
|
|
220
|
+
queue_error_callback: Optional callback for isolated target-handler failures.
|
|
219
221
|
"""
|
|
220
222
|
|
|
221
223
|
level: int = 20
|
|
@@ -236,6 +238,7 @@ class YaplConfig:
|
|
|
236
238
|
overrides: LoggerOverrides = field(default_factory=LoggerOverrides)
|
|
237
239
|
suppression: SuppressionRules = field(default_factory=SuppressionRules)
|
|
238
240
|
shutdown_timeout_seconds: float = 5.0
|
|
241
|
+
queue_error_callback: QueueErrorCallback | None = None
|
|
239
242
|
|
|
240
243
|
def with_level(self, level: LevelLike) -> YaplConfig:
|
|
241
244
|
"""Set the root level and return this configuration.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
3
4
|
from contextvars import ContextVar, Token
|
|
4
5
|
from copy import copy
|
|
5
6
|
import logging
|
|
@@ -18,6 +19,23 @@ _capture_enabled: ContextVar[bool] = ContextVar(
|
|
|
18
19
|
)
|
|
19
20
|
|
|
20
21
|
|
|
22
|
+
def current_buffer_owner() -> object | None:
|
|
23
|
+
"""Return the current async task, or ``None`` outside an event loop."""
|
|
24
|
+
try:
|
|
25
|
+
return asyncio.current_task()
|
|
26
|
+
except RuntimeError:
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def owns_buffer_state(state: BufferingState) -> bool:
|
|
31
|
+
"""Return whether ``state`` belongs to this synchronous context or task.
|
|
32
|
+
|
|
33
|
+
Context variables already isolate ordinary threads. Only task-owned states
|
|
34
|
+
need an asyncio lookup because child tasks inherit their parent's context.
|
|
35
|
+
"""
|
|
36
|
+
return state.owner_task is None or state.owner_task is current_buffer_owner()
|
|
37
|
+
|
|
38
|
+
|
|
21
39
|
def get_active_buffer_state() -> BufferingState | None:
|
|
22
40
|
return _active_buffer_state.get()
|
|
23
41
|
|
|
@@ -54,7 +72,7 @@ class BufferCaptureFilter(logging.Filter):
|
|
|
54
72
|
return True
|
|
55
73
|
|
|
56
74
|
state = get_active_buffer_state()
|
|
57
|
-
if state is None:
|
|
75
|
+
if state is None or not owns_buffer_state(state):
|
|
58
76
|
return True
|
|
59
77
|
|
|
60
78
|
buffered_record = copy(record)
|
|
@@ -7,8 +7,10 @@ from typing import Self
|
|
|
7
7
|
|
|
8
8
|
from .capture import (
|
|
9
9
|
BufferCaptureFilter,
|
|
10
|
+
current_buffer_owner,
|
|
10
11
|
get_active_buffer_state,
|
|
11
12
|
is_capture_enabled,
|
|
13
|
+
owns_buffer_state,
|
|
12
14
|
reset_active_buffer_state,
|
|
13
15
|
reset_capture_enabled,
|
|
14
16
|
set_active_buffer_state,
|
|
@@ -87,8 +89,13 @@ class BufferedLogs:
|
|
|
87
89
|
|
|
88
90
|
def _enter(self) -> Self:
|
|
89
91
|
self._previous_state = get_active_buffer_state()
|
|
92
|
+
owner_task = current_buffer_owner()
|
|
90
93
|
|
|
91
|
-
if
|
|
94
|
+
if (
|
|
95
|
+
self._previous_state is not None
|
|
96
|
+
and self._cfg.include_nested_contexts
|
|
97
|
+
and self._previous_state.owner_task is owner_task
|
|
98
|
+
):
|
|
92
99
|
self._state = self._previous_state
|
|
93
100
|
self._store = self._state.store
|
|
94
101
|
self._cfg = self._state.config
|
|
@@ -96,6 +103,11 @@ class BufferedLogs:
|
|
|
96
103
|
self._owns_state = False
|
|
97
104
|
return self
|
|
98
105
|
|
|
106
|
+
self._state = BufferingState(
|
|
107
|
+
store=self._store,
|
|
108
|
+
config=self._cfg,
|
|
109
|
+
owner_task=owner_task,
|
|
110
|
+
)
|
|
99
111
|
self._token = set_active_buffer_state(self._state)
|
|
100
112
|
self._owns_state = True
|
|
101
113
|
return self
|
|
@@ -134,7 +146,8 @@ class BufferingFilter(logging.Filter):
|
|
|
134
146
|
|
|
135
147
|
def filter(self, record: logging.LogRecord) -> bool:
|
|
136
148
|
"""Allow records only when capture is inactive for this context."""
|
|
137
|
-
|
|
149
|
+
state = get_active_buffer_state()
|
|
150
|
+
return (not is_capture_enabled()) or state is None or not owns_buffer_state(state)
|
|
138
151
|
|
|
139
152
|
|
|
140
153
|
def install_buffering(*, stream_handlers_only: bool = False) -> int:
|
|
@@ -10,7 +10,7 @@ LEVEL_COLOR_TOKEN = "level_color"
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
def _resolve_dict_color(value: Mapping[str, str]) -> dict[str, str]:
|
|
13
|
-
return {key: resolve_color(color) for key, color in value.items()}
|
|
13
|
+
return {key.upper(): resolve_color(color) for key, color in value.items()}
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
def _resolve_style_color(value: str) -> str:
|
|
@@ -29,6 +29,7 @@ class ColorPlainFormatter(logging.Formatter):
|
|
|
29
29
|
|
|
30
30
|
message = record.getMessage()
|
|
31
31
|
level_name = record.levelname
|
|
32
|
+
level_color = theme.color_for_level(level_name)
|
|
32
33
|
|
|
33
34
|
parts: list[str] = []
|
|
34
35
|
|
|
@@ -43,7 +44,7 @@ class ColorPlainFormatter(logging.Formatter):
|
|
|
43
44
|
parts.append(self._color_field("logger", record.name, theme))
|
|
44
45
|
|
|
45
46
|
if self._config.include_message:
|
|
46
|
-
parts.append(self._color_field("message", message, theme))
|
|
47
|
+
parts.append(self._color_field("message", message, theme, level_color=level_color))
|
|
47
48
|
|
|
48
49
|
rendered = self._config.separator.join(parts)
|
|
49
50
|
|
|
@@ -83,9 +84,19 @@ class ColorPlainFormatter(logging.Formatter):
|
|
|
83
84
|
return text
|
|
84
85
|
return f"{color}{text}{reset}"
|
|
85
86
|
|
|
86
|
-
def _color_field(
|
|
87
|
+
def _color_field(
|
|
88
|
+
self,
|
|
89
|
+
field_name: str,
|
|
90
|
+
value: Any,
|
|
91
|
+
theme: Any,
|
|
92
|
+
*,
|
|
93
|
+
level_color: str | None = None,
|
|
94
|
+
) -> str:
|
|
87
95
|
style = theme.style_for_field(field_name)
|
|
88
|
-
|
|
96
|
+
color = style.value_color
|
|
97
|
+
if color == "level_color":
|
|
98
|
+
color = level_color or ""
|
|
99
|
+
return self._wrap(str(value), color, theme.reset)
|
|
89
100
|
|
|
90
101
|
def _color_level(self, level_name: str, theme: Any) -> str:
|
|
91
102
|
style = theme.style_for_field("level")
|
|
@@ -302,8 +302,13 @@ class ParseMessageFields:
|
|
|
302
302
|
if not isinstance(message, str) or "=" not in message:
|
|
303
303
|
return event
|
|
304
304
|
|
|
305
|
+
try:
|
|
306
|
+
tokens = shlex.split(message)
|
|
307
|
+
except ValueError:
|
|
308
|
+
return event
|
|
309
|
+
|
|
305
310
|
parsed: dict[str, Any] = {}
|
|
306
|
-
for token in
|
|
311
|
+
for token in tokens:
|
|
307
312
|
if "=" not in token:
|
|
308
313
|
continue
|
|
309
314
|
key, value = token.split("=", 1)
|
|
@@ -3,6 +3,7 @@ from .models import (
|
|
|
3
3
|
QueueBatch,
|
|
4
4
|
QueueDiagnostics,
|
|
5
5
|
QueuedLogItem,
|
|
6
|
+
QueueErrorCallback,
|
|
6
7
|
QueueingConfig,
|
|
7
8
|
QueueOverflowPolicy,
|
|
8
9
|
)
|
|
@@ -14,6 +15,7 @@ __all__ = [
|
|
|
14
15
|
"LogQueue",
|
|
15
16
|
"QueueBatch",
|
|
16
17
|
"QueueDiagnostics",
|
|
18
|
+
"QueueErrorCallback",
|
|
17
19
|
"QueueOverflowPolicy",
|
|
18
20
|
"QueueWorker",
|
|
19
21
|
"QueuedLogItem",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import contextlib
|
|
4
|
+
from dataclasses import dataclass
|
|
4
5
|
import logging
|
|
5
6
|
from logging.handlers import QueueHandler
|
|
6
7
|
from queue import Empty, Full, Queue
|
|
@@ -11,6 +12,12 @@ from .models import QueueDiagnostics, QueueingConfig
|
|
|
11
12
|
from .policy import should_fallback_to_file
|
|
12
13
|
|
|
13
14
|
|
|
15
|
+
@dataclass(slots=True)
|
|
16
|
+
class _TargetFlushState:
|
|
17
|
+
completed: threading.Event
|
|
18
|
+
failed: bool = False
|
|
19
|
+
|
|
20
|
+
|
|
14
21
|
class QueueingHandler(QueueHandler):
|
|
15
22
|
"""Move handler work off the logging thread with bounded shutdown waits.
|
|
16
23
|
|
|
@@ -35,6 +42,7 @@ class QueueingHandler(QueueHandler):
|
|
|
35
42
|
self._cfg = config or QueueingConfig()
|
|
36
43
|
self._diagnostics = QueueDiagnostics()
|
|
37
44
|
self._target_handlers = tuple(handler for handler in target_handlers if handler is not self)
|
|
45
|
+
self._target_labels = self._build_target_labels()
|
|
38
46
|
self._queue: Queue[logging.LogRecord] = Queue(maxsize=self._cfg.max_queue_size)
|
|
39
47
|
super().__init__(self._queue)
|
|
40
48
|
self._fallback_handler = self._find_fallback_handler()
|
|
@@ -48,12 +56,14 @@ class QueueingHandler(QueueHandler):
|
|
|
48
56
|
self._accepting = True
|
|
49
57
|
self._stop_event = threading.Event()
|
|
50
58
|
self._state_lock = threading.Lock()
|
|
59
|
+
self._target_flush_lock = threading.Lock()
|
|
60
|
+
self._target_flush_states: dict[int, _TargetFlushState] = {}
|
|
51
61
|
self._active_blocking_enqueues = 0
|
|
52
62
|
self._targets_closed = False
|
|
53
63
|
self._workers_remaining = 1 + (self._fallback_queue is not None)
|
|
54
64
|
self._worker = threading.Thread(
|
|
55
65
|
target=self._run,
|
|
56
|
-
args=(self._queue, self._target_handlers),
|
|
66
|
+
args=(self._queue, self._target_handlers, self._target_labels),
|
|
57
67
|
name=self._cfg.worker_name,
|
|
58
68
|
daemon=True,
|
|
59
69
|
)
|
|
@@ -62,7 +72,11 @@ class QueueingHandler(QueueHandler):
|
|
|
62
72
|
if self._fallback_queue is not None and self._fallback_handler is not None:
|
|
63
73
|
self._fallback_worker = threading.Thread(
|
|
64
74
|
target=self._run,
|
|
65
|
-
args=(
|
|
75
|
+
args=(
|
|
76
|
+
self._fallback_queue,
|
|
77
|
+
(self._fallback_handler,),
|
|
78
|
+
(self._target_labels[self._target_handlers.index(self._fallback_handler)],),
|
|
79
|
+
),
|
|
66
80
|
name=f"{self._cfg.worker_name}-fallback",
|
|
67
81
|
daemon=True,
|
|
68
82
|
)
|
|
@@ -90,7 +104,7 @@ class QueueingHandler(QueueHandler):
|
|
|
90
104
|
self._fallback_worker is not None and self._fallback_worker.is_alive()
|
|
91
105
|
)
|
|
92
106
|
|
|
93
|
-
def diagnostics_snapshot(self) -> dict[str,
|
|
107
|
+
def diagnostics_snapshot(self) -> dict[str, object]:
|
|
94
108
|
"""Return queue counters and the current combined queued-record count."""
|
|
95
109
|
queue_size = self._queue.qsize()
|
|
96
110
|
if self._fallback_queue is not None:
|
|
@@ -125,6 +139,8 @@ class QueueingHandler(QueueHandler):
|
|
|
125
139
|
try:
|
|
126
140
|
with self._state_lock:
|
|
127
141
|
if not self._accepting:
|
|
142
|
+
self._diagnostics.dropped += 1
|
|
143
|
+
self._diagnostics.dropped_shutdown += 1
|
|
128
144
|
return
|
|
129
145
|
self._queue.put_nowait(record)
|
|
130
146
|
except Full:
|
|
@@ -136,6 +152,8 @@ class QueueingHandler(QueueHandler):
|
|
|
136
152
|
def _enqueue_blocking(self, record: logging.LogRecord) -> None:
|
|
137
153
|
with self._state_lock:
|
|
138
154
|
if not self._accepting:
|
|
155
|
+
self._diagnostics.dropped += 1
|
|
156
|
+
self._diagnostics.dropped_shutdown += 1
|
|
139
157
|
return
|
|
140
158
|
self._active_blocking_enqueues += 1
|
|
141
159
|
|
|
@@ -180,7 +198,9 @@ class QueueingHandler(QueueHandler):
|
|
|
180
198
|
if timed_flush(remaining) is False:
|
|
181
199
|
drained = False
|
|
182
200
|
else:
|
|
183
|
-
|
|
201
|
+
remaining = max(0.0, deadline - time.monotonic())
|
|
202
|
+
if not self._flush_target_with_timeout(handler, remaining):
|
|
203
|
+
drained = False
|
|
184
204
|
except Exception:
|
|
185
205
|
drained = False
|
|
186
206
|
if drained:
|
|
@@ -227,6 +247,7 @@ class QueueingHandler(QueueHandler):
|
|
|
227
247
|
self,
|
|
228
248
|
queue: Queue[logging.LogRecord],
|
|
229
249
|
handlers: tuple[logging.Handler, ...],
|
|
250
|
+
labels: tuple[str, ...],
|
|
230
251
|
) -> None:
|
|
231
252
|
try:
|
|
232
253
|
while True:
|
|
@@ -242,7 +263,7 @@ class QueueingHandler(QueueHandler):
|
|
|
242
263
|
self._diagnostics.dropped += 1
|
|
243
264
|
self._diagnostics.dropped_shutdown += 1
|
|
244
265
|
else:
|
|
245
|
-
self._dispatch(record, handlers)
|
|
266
|
+
self._dispatch(record, handlers, labels)
|
|
246
267
|
finally:
|
|
247
268
|
queue.task_done()
|
|
248
269
|
finally:
|
|
@@ -258,16 +279,34 @@ class QueueingHandler(QueueHandler):
|
|
|
258
279
|
# until it resolves so a late record is discarded instead of stranded.
|
|
259
280
|
return self._active_blocking_enqueues == 0
|
|
260
281
|
|
|
261
|
-
def _dispatch(
|
|
262
|
-
|
|
282
|
+
def _dispatch(
|
|
283
|
+
self,
|
|
284
|
+
record: logging.LogRecord,
|
|
285
|
+
handlers: tuple[logging.Handler, ...],
|
|
286
|
+
labels: tuple[str, ...],
|
|
287
|
+
) -> None:
|
|
288
|
+
for handler, label in zip(handlers, labels, strict=True):
|
|
263
289
|
if self._cfg.respect_handler_level and record.levelno < handler.level:
|
|
264
290
|
continue
|
|
265
291
|
try:
|
|
266
292
|
handler.handle(record)
|
|
267
|
-
except Exception:
|
|
293
|
+
except Exception as error:
|
|
294
|
+
self._diagnostics.record_handler_failure(label)
|
|
295
|
+
if self._cfg.error_callback is not None:
|
|
296
|
+
with contextlib.suppress(Exception):
|
|
297
|
+
self._cfg.error_callback(handler, record, error)
|
|
268
298
|
# Isolate one target failure so it cannot stop shared delivery.
|
|
269
299
|
continue
|
|
270
300
|
|
|
301
|
+
def _build_target_labels(self) -> tuple[str, ...]:
|
|
302
|
+
counts: dict[str, int] = {}
|
|
303
|
+
labels: list[str] = []
|
|
304
|
+
for handler in self._target_handlers:
|
|
305
|
+
name = handler.get_name() or type(handler).__name__
|
|
306
|
+
counts[name] = counts.get(name, 0) + 1
|
|
307
|
+
labels.append(name if counts[name] == 1 else f"{name}#{counts[name]}")
|
|
308
|
+
return tuple(labels)
|
|
309
|
+
|
|
271
310
|
def _close_targets_if_finished(self) -> None:
|
|
272
311
|
with self._state_lock:
|
|
273
312
|
if self._workers_remaining or self._targets_closed:
|
|
@@ -275,7 +314,7 @@ class QueueingHandler(QueueHandler):
|
|
|
275
314
|
self._targets_closed = True
|
|
276
315
|
for handler in self._target_handlers:
|
|
277
316
|
with contextlib.suppress(Exception):
|
|
278
|
-
|
|
317
|
+
self._flush_target_with_timeout(handler, None)
|
|
279
318
|
with contextlib.suppress(Exception):
|
|
280
319
|
handler.close()
|
|
281
320
|
|
|
@@ -302,6 +341,46 @@ class QueueingHandler(QueueHandler):
|
|
|
302
341
|
None,
|
|
303
342
|
)
|
|
304
343
|
|
|
344
|
+
def _flush_target_with_timeout(
|
|
345
|
+
self,
|
|
346
|
+
handler: logging.Handler,
|
|
347
|
+
timeout_seconds: float | None,
|
|
348
|
+
) -> bool:
|
|
349
|
+
"""Flush one target with one reusable in-flight timeout operation."""
|
|
350
|
+
handler_key = id(handler)
|
|
351
|
+
with self._target_flush_lock:
|
|
352
|
+
state = self._target_flush_states.get(handler_key)
|
|
353
|
+
if state is None:
|
|
354
|
+
state = _TargetFlushState(completed=threading.Event())
|
|
355
|
+
self._target_flush_states[handler_key] = state
|
|
356
|
+
threading.Thread(
|
|
357
|
+
target=self._run_target_flush,
|
|
358
|
+
args=(handler, state),
|
|
359
|
+
name="yapl-queue-target-flush",
|
|
360
|
+
daemon=True,
|
|
361
|
+
).start()
|
|
362
|
+
|
|
363
|
+
if not state.completed.wait(timeout_seconds):
|
|
364
|
+
return False
|
|
365
|
+
|
|
366
|
+
return not state.failed
|
|
367
|
+
|
|
368
|
+
def _run_target_flush(
|
|
369
|
+
self,
|
|
370
|
+
handler: logging.Handler,
|
|
371
|
+
state: _TargetFlushState,
|
|
372
|
+
) -> None:
|
|
373
|
+
try:
|
|
374
|
+
handler.flush()
|
|
375
|
+
except Exception:
|
|
376
|
+
state.failed = True
|
|
377
|
+
finally:
|
|
378
|
+
state.completed.set()
|
|
379
|
+
with self._target_flush_lock:
|
|
380
|
+
handler_key = id(handler)
|
|
381
|
+
if self._target_flush_states.get(handler_key) is state:
|
|
382
|
+
del self._target_flush_states[handler_key]
|
|
383
|
+
|
|
305
384
|
|
|
306
385
|
def enable_queueing(
|
|
307
386
|
config: QueueingConfig | None = None,
|