without-logging 0.0.1__tar.gz → 0.0.2__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.
- {without_logging-0.0.1 → without_logging-0.0.2}/PKG-INFO +3 -3
- {without_logging-0.0.1 → without_logging-0.0.2}/README.md +1 -1
- without_logging-0.0.2/pyproject.toml +31 -0
- without_logging-0.0.1/pyproject.toml → without_logging-0.0.2/pyproject.toml.orig +3 -3
- {without_logging-0.0.1 → without_logging-0.0.2}/src/without_logging/capture.py +12 -3
- {without_logging-0.0.1 → without_logging-0.0.2}/src/without_logging/processors.py +2 -2
- {without_logging-0.0.1 → without_logging-0.0.2}/src/without_logging/record.py +1 -1
- {without_logging-0.0.1 → without_logging-0.0.2}/src/without_logging/renderers.py +11 -7
- {without_logging-0.0.1 → without_logging-0.0.2}/src/without_logging/sinks.py +2 -2
- {without_logging-0.0.1 → without_logging-0.0.2}/src/without_logging/__init__.py +0 -0
- {without_logging-0.0.1 → without_logging-0.0.2}/src/without_logging/context.py +0 -0
- {without_logging-0.0.1 → without_logging-0.0.2}/src/without_logging/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: without-logging
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.2
|
|
4
4
|
Summary: A without pipeline for logs: stdlib log records parsed into immutable values, filtered and enriched as processors, drained to a sink.
|
|
5
5
|
Author: Josh Karpel
|
|
6
6
|
Author-email: Josh Karpel <josh.karpel@gmail.com>
|
|
@@ -15,7 +15,7 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
15
15
|
Classifier: Topic :: Software Development :: Libraries
|
|
16
16
|
Classifier: Topic :: System :: Logging
|
|
17
17
|
Classifier: Typing :: Typed
|
|
18
|
-
Requires-Dist: without-core==0.0.
|
|
18
|
+
Requires-Dist: without-core==0.0.2
|
|
19
19
|
Requires-Python: >=3.14
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
21
|
|
|
@@ -77,7 +77,7 @@ async with offload(writer) as sink:
|
|
|
77
77
|
```
|
|
78
78
|
|
|
79
79
|
See the
|
|
80
|
-
[`without-logging` guide](https://without.help/
|
|
80
|
+
[`without-logging` guide](https://without.help/without-logging/)
|
|
81
81
|
(with the [API reference](https://without.help/reference/without_logging/))
|
|
82
82
|
for the design narrative: why stdlib becomes a one-way source, why filtering is
|
|
83
83
|
just the core `from_selector` builder, and where fan-out to multiple sinks slots
|
|
@@ -56,7 +56,7 @@ async with offload(writer) as sink:
|
|
|
56
56
|
```
|
|
57
57
|
|
|
58
58
|
See the
|
|
59
|
-
[`without-logging` guide](https://without.help/
|
|
59
|
+
[`without-logging` guide](https://without.help/without-logging/)
|
|
60
60
|
(with the [API reference](https://without.help/reference/without_logging/))
|
|
61
61
|
for the design narrative: why stdlib becomes a one-way source, why filtering is
|
|
62
62
|
just the core `from_selector` builder, and where fan-out to multiple sinks slots
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["uv_build>=0.11.32,<0.12"]
|
|
3
|
+
build-backend = "uv_build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "without-logging"
|
|
7
|
+
version = "0.0.2"
|
|
8
|
+
description = "A without pipeline for logs: stdlib log records parsed into immutable values, filtered and enriched as processors, drained to a sink."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.14"
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 2 - Pre-Alpha",
|
|
14
|
+
"Framework :: AsyncIO",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
19
|
+
"Programming Language :: Python :: 3.14",
|
|
20
|
+
"Topic :: Software Development :: Libraries",
|
|
21
|
+
"Topic :: System :: Logging",
|
|
22
|
+
"Typing :: Typed",
|
|
23
|
+
]
|
|
24
|
+
dependencies = ["without-core==0.0.2"]
|
|
25
|
+
|
|
26
|
+
[[project.authors]]
|
|
27
|
+
name = "Josh Karpel"
|
|
28
|
+
email = "josh.karpel@gmail.com"
|
|
29
|
+
|
|
30
|
+
[tool.uv.sources.without-core]
|
|
31
|
+
workspace = true
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["uv_build>=0.11.
|
|
2
|
+
requires = ["uv_build>=0.11.32,<0.12"]
|
|
3
3
|
build-backend = "uv_build"
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "without-logging"
|
|
7
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.2"
|
|
8
8
|
description = "A without pipeline for logs: stdlib log records parsed into immutable values, filtered and enriched as processors, drained to a sink."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -25,7 +25,7 @@ classifiers = [
|
|
|
25
25
|
"Typing :: Typed",
|
|
26
26
|
]
|
|
27
27
|
dependencies = [
|
|
28
|
-
"without-core==0.0.
|
|
28
|
+
"without-core==0.0.2",
|
|
29
29
|
]
|
|
30
30
|
|
|
31
31
|
[tool.uv.sources]
|
|
@@ -2,11 +2,12 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import logging
|
|
5
|
+
import threading
|
|
5
6
|
from collections.abc import AsyncIterator
|
|
6
7
|
from collections.abc import Callable
|
|
7
8
|
from contextlib import asynccontextmanager
|
|
8
9
|
|
|
9
|
-
from without.
|
|
10
|
+
from without.interfaces import Sink
|
|
10
11
|
from without.wiring import stream_from_queue
|
|
11
12
|
|
|
12
13
|
from without_logging.context import merge_context
|
|
@@ -31,7 +32,11 @@ class CaptureHandler(logging.Handler):
|
|
|
31
32
|
Control only ever flows outward from stdlib into `without`, never back.
|
|
32
33
|
|
|
33
34
|
A log call MAY happen off the event loop thread, so the parsed value is handed
|
|
34
|
-
to the loop with `call_soon_threadsafe
|
|
35
|
+
to the loop with `call_soon_threadsafe`, which writes the loop's self-pipe to
|
|
36
|
+
wake it. A log call *on* the loop thread (an async handler logging) does not
|
|
37
|
+
need that wakeup, so those take the cheaper `call_soon`; the construction thread
|
|
38
|
+
is the loop thread (`capture` builds this right after `get_running_loop`), so its
|
|
39
|
+
id captured here identifies loop-thread emits. The queue is bounded, so a burst that
|
|
35
40
|
outruns the sink is dropped rather than growing memory without limit;
|
|
36
41
|
`dropped` counts how many so an operator can see it. The overflow policy is a
|
|
37
42
|
boundary decision the app owns: raise `capacity`, or pass `capacity=None` for
|
|
@@ -46,13 +51,17 @@ class CaptureHandler(logging.Handler):
|
|
|
46
51
|
) -> None:
|
|
47
52
|
super().__init__()
|
|
48
53
|
self._loop = loop
|
|
54
|
+
self._loop_thread_id = threading.get_ident()
|
|
49
55
|
self._queue = queue
|
|
50
56
|
self._parse = parse
|
|
51
57
|
self.dropped = 0
|
|
52
58
|
|
|
53
59
|
def emit(self, log_record: logging.LogRecord) -> None:
|
|
54
60
|
record = self._parse(log_record)
|
|
55
|
-
|
|
61
|
+
if threading.get_ident() == self._loop_thread_id:
|
|
62
|
+
self._loop.call_soon(self._offer, record)
|
|
63
|
+
else:
|
|
64
|
+
self._loop.call_soon_threadsafe(self._offer, record)
|
|
56
65
|
|
|
57
66
|
def _offer(self, record: Record) -> None:
|
|
58
67
|
try:
|
|
@@ -3,8 +3,8 @@ from __future__ import annotations
|
|
|
3
3
|
from collections.abc import Awaitable
|
|
4
4
|
from collections.abc import Callable
|
|
5
5
|
|
|
6
|
-
from without.
|
|
7
|
-
from without.
|
|
6
|
+
from without.interfaces import Processor
|
|
7
|
+
from without.interfaces import from_map
|
|
8
8
|
|
|
9
9
|
from without_logging.record import Record
|
|
10
10
|
|
|
@@ -39,7 +39,7 @@ class Level(IntEnum):
|
|
|
39
39
|
# that is deliberately dropped: it is gated by module flags (`logThreads`,
|
|
40
40
|
# `logProcesses`, `logAsyncioTasks`) and a swappable factory, so it may be `None` or
|
|
41
41
|
# absent, and it exists for stdlib's formatter `%(...)s` placeholders rather than as
|
|
42
|
-
# a stable per-event
|
|
42
|
+
# a stable per-event guarantee. A caller who wants some of it lifts it into `fields`
|
|
43
43
|
# with a custom `parse=` on `capture`.
|
|
44
44
|
RESERVED: frozenset[str] = frozenset(
|
|
45
45
|
{
|
|
@@ -5,8 +5,8 @@ from collections.abc import Callable
|
|
|
5
5
|
from datetime import datetime
|
|
6
6
|
from traceback import TracebackException
|
|
7
7
|
|
|
8
|
-
from without.
|
|
9
|
-
from without.
|
|
8
|
+
from without.interfaces import Processor
|
|
9
|
+
from without.interfaces import from_map
|
|
10
10
|
|
|
11
11
|
from without_logging.record import Record
|
|
12
12
|
|
|
@@ -111,21 +111,25 @@ def render_console(
|
|
|
111
111
|
`TIMESTAMP LEVEL logger "message" {key=value, ...}`: the message is quoted and
|
|
112
112
|
the fields grouped in braces (each value `repr`'d), so the free-text message and
|
|
113
113
|
the structured fields never blur into each other or the leading metadata. The
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
114
|
+
message is quoted with `json.dumps` and each field value is `repr`'d, so a control
|
|
115
|
+
character in either (a newline from a decoded request path, say) is escaped rather
|
|
116
|
+
than forging a new log line. The braces are omitted when there are no
|
|
117
|
+
fields. An exception is appended as its full traceback text (`exception_to_text`,
|
|
118
|
+
cause chain and all), indented, on following lines. `timestamp` chooses the time
|
|
119
|
+
format (default `iso_timestamp`). No coloring: wrap the line, or write your own
|
|
120
|
+
`from_map(Record -> str)`, if you want ANSI.
|
|
118
121
|
|
|
119
122
|
Optional and opt-in, like `render_json`: `compose(render_console(),
|
|
120
123
|
offload(to_stream(sys.stderr)))`.
|
|
121
124
|
"""
|
|
122
125
|
|
|
123
126
|
async def render(record: Record) -> str:
|
|
127
|
+
quoted = json.dumps(record.message, ensure_ascii=False) # pragma: no mutate - None acts as False here
|
|
124
128
|
parts = [
|
|
125
129
|
timestamp(record.timestamp),
|
|
126
130
|
record.level_name,
|
|
127
131
|
record.logger,
|
|
128
|
-
|
|
132
|
+
quoted,
|
|
129
133
|
]
|
|
130
134
|
if record.fields:
|
|
131
135
|
fields = ", ".join(f"{key}={value!r}" for key, value in record.fields.items())
|
|
@@ -15,8 +15,8 @@ from datetime import tzinfo
|
|
|
15
15
|
from pathlib import Path
|
|
16
16
|
from typing import TextIO
|
|
17
17
|
|
|
18
|
-
from without.
|
|
19
|
-
from without.
|
|
18
|
+
from without.interfaces import Sink
|
|
19
|
+
from without.interfaces import Stream
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
@asynccontextmanager
|
|
File without changes
|
|
File without changes
|
|
File without changes
|