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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: without-logging
3
- Version: 0.0.1
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.1
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/guides/without-logging/)
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/guides/without-logging/)
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.25,<0.12"]
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.1"
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.1",
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.contracts import Sink
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`. The queue is bounded, so a burst that
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
- self._loop.call_soon_threadsafe(self._offer, record)
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.contracts import Processor
7
- from without.contracts import from_map
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 contract. A caller who wants some of it lifts it into `fields`
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.contracts import Processor
9
- from without.contracts import from_map
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
- braces are omitted when there are no fields. An exception is appended as its full
115
- traceback text (`exception_to_text`, cause chain and all), indented, on following
116
- lines. `timestamp` chooses the time format (default `iso_timestamp`). No coloring:
117
- wrap the line, or write your own `from_map(Record -> str)`, if you want ANSI.
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
- f'"{record.message}"',
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.contracts import Sink
19
- from without.contracts import Stream
18
+ from without.interfaces import Sink
19
+ from without.interfaces import Stream
20
20
 
21
21
 
22
22
  @asynccontextmanager