log-foundry 0.10.2.dev29__tar.gz → 0.10.2.dev30__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.
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/PKG-INFO +1 -1
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/pyproject.toml +1 -1
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/src/log_foundry/api.py +6 -0
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/src/log_foundry/console.py +13 -2
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/src/log_foundry/model.py +8 -7
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/src/log_foundry/sanitize.py +8 -2
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/src/log_foundry/sinks/_socket.py +35 -7
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/src/log_foundry/sinks/file.py +21 -7
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/src/log_foundry/sinks/logstash.py +12 -0
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/src/log_foundry/sinks/stdout.py +7 -2
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/src/log_foundry/sinks/syslog.py +12 -0
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/src/log_foundry/sinks/util.py +8 -3
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/src/log_foundry/worker.py +6 -0
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/LICENSE +0 -0
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/README.md +0 -0
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/src/log_foundry/__init__.py +0 -0
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/src/log_foundry/_diag.py +0 -0
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/src/log_foundry/config.py +0 -0
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/src/log_foundry/context.py +0 -0
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/src/log_foundry/decorator.py +0 -0
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/src/log_foundry/ids.py +0 -0
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/src/log_foundry/py.typed +0 -0
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/src/log_foundry/sinks/__init__.py +0 -0
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/src/log_foundry/sinks/_batch.py +0 -0
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/src/log_foundry/sinks/_chunk.py +0 -0
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/src/log_foundry/sinks/_retry.py +0 -0
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/src/log_foundry/sinks/_time.py +0 -0
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/src/log_foundry/sinks/base.py +0 -0
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/src/log_foundry/sinks/callback.py +0 -0
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/src/log_foundry/sinks/clickhouse.py +0 -0
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/src/log_foundry/sinks/datadog.py +0 -0
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/src/log_foundry/sinks/elasticsearch.py +0 -0
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/src/log_foundry/sinks/eventhubs.py +0 -0
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/src/log_foundry/sinks/filtering.py +0 -0
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/src/log_foundry/sinks/firehose.py +0 -0
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/src/log_foundry/sinks/honeycomb.py +0 -0
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/src/log_foundry/sinks/http.py +0 -0
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/src/log_foundry/sinks/kafka.py +0 -0
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/src/log_foundry/sinks/kinesis.py +0 -0
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/src/log_foundry/sinks/logging_sink.py +0 -0
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/src/log_foundry/sinks/loki.py +0 -0
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/src/log_foundry/sinks/mongodb.py +0 -0
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/src/log_foundry/sinks/multi.py +0 -0
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/src/log_foundry/sinks/nats.py +0 -0
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/src/log_foundry/sinks/newrelic.py +0 -0
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/src/log_foundry/sinks/postgres.py +0 -0
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/src/log_foundry/sinks/pubsub.py +0 -0
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/src/log_foundry/sinks/rabbitmq.py +0 -0
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/src/log_foundry/sinks/redis.py +0 -0
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/src/log_foundry/sinks/sentry.py +0 -0
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/src/log_foundry/sinks/sns.py +0 -0
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/src/log_foundry/sinks/splunk.py +0 -0
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/src/log_foundry/sinks/sqlite.py +0 -0
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/src/log_foundry/sinks/sqs.py +0 -0
- {log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/src/log_foundry/sinks/transform.py +0 -0
|
@@ -30,6 +30,12 @@ def _log(level: str, message: str, echo: bool, fields: dict[str, object]) -> Non
|
|
|
30
30
|
the sink resolved through ``_ensure_sink`` so a zero-config orphan log falls back to
|
|
31
31
|
``StdoutSink`` rather than crashing.
|
|
32
32
|
|
|
33
|
+
That direct handoff is **settled, not pending**: SPEC-004's worker took over the traced
|
|
34
|
+
path and this branch was deliberately left synchronous, so a level call outside a span is
|
|
35
|
+
never silently dropped for want of a worker (``architecture.md`` §12 Resolved, "Orphan
|
|
36
|
+
logs"). It carried a comment saying the worker "will later" own it long after the
|
|
37
|
+
decision was made (SPEC-031 FR-003).
|
|
38
|
+
|
|
33
39
|
The orphan branch is the one that reaches the sink on the caller's own thread, with no
|
|
34
40
|
worker between them to absorb a failure, so the whole branch is guarded (SPEC-025
|
|
35
41
|
FR-003) — ``_ensure_sink`` constructs the sink on first use, so a sink that fails to
|
|
@@ -16,13 +16,24 @@ class ConsoleWriter:
|
|
|
16
16
|
operator sees it without waiting for the async flush. It is deliberately dumb, rendering
|
|
17
17
|
an already-built event dict and knowing nothing about spans, and echo is additive — an
|
|
18
18
|
echoed event still rides the normal pipeline to the sink.
|
|
19
|
+
|
|
20
|
+
The default stream is **stderr**, not stdout (SPEC-031 FR-003, which corrected two
|
|
21
|
+
documents that said otherwise). It is the twelve-factor convention ``StderrSink`` already
|
|
22
|
+
cites — logs on stderr, the application's own output on stdout — so an echo cannot corrupt
|
|
23
|
+
a program whose stdout is a data stream someone pipes.
|
|
19
24
|
"""
|
|
20
25
|
|
|
21
26
|
def __init__(self, stream: TextIO | None = None) -> None:
|
|
22
|
-
"""Binds the writer to an output stream.
|
|
27
|
+
"""Binds the writer to an output stream, once, at construction.
|
|
28
|
+
|
|
29
|
+
The binding is deliberate and permanent for the life of the writer: a later
|
|
30
|
+
``contextlib.redirect_stderr`` or a test's capture of ``sys.stderr`` is not honoured,
|
|
31
|
+
because the attribute was resolved here. ``api._console`` is built at import, so in
|
|
32
|
+
practice a process's echo stream is fixed before any test runs. Passing ``stream=``
|
|
33
|
+
explicitly is how a caller — a test above all — captures the output (SPEC-031 FR-003).
|
|
23
34
|
|
|
24
35
|
Args:
|
|
25
|
-
stream: The stream to write to, defaulting to ``sys.stderr
|
|
36
|
+
stream: The stream to write to, defaulting to ``sys.stderr`` as resolved now.
|
|
26
37
|
|
|
27
38
|
Returns:
|
|
28
39
|
None.
|
|
@@ -8,6 +8,8 @@ from dataclasses import dataclass, field
|
|
|
8
8
|
from datetime import UTC, datetime
|
|
9
9
|
from typing import TYPE_CHECKING
|
|
10
10
|
|
|
11
|
+
from log_foundry.config import get_config
|
|
12
|
+
from log_foundry.ids import new_log_id
|
|
11
13
|
from log_foundry.sanitize import sanitize_fields, truncate_str, truncate_tail
|
|
12
14
|
|
|
13
15
|
if TYPE_CHECKING:
|
|
@@ -72,6 +74,12 @@ def build_event(
|
|
|
72
74
|
bounded too, since both are caller-supplied text and leaving either out would keep
|
|
73
75
|
``info(huge_string)`` unbounded.
|
|
74
76
|
|
|
77
|
+
``get_config`` and ``new_log_id`` are imported at module scope (SPEC-031 FR-004). They were
|
|
78
|
+
function-local to avoid a cycle, but there is none to avoid: neither ``config`` nor ``ids``
|
|
79
|
+
imports this module, and ``config``'s own back-references to ``decorator`` stay local for
|
|
80
|
+
the reason its docstrings give. This is the hottest path in the library, so resolving them
|
|
81
|
+
once at import is what ``sanitize`` already does with its one-time bindings.
|
|
82
|
+
|
|
75
83
|
Args:
|
|
76
84
|
span: The span the event belongs to, supplying identity and defaults.
|
|
77
85
|
level: The severity label, such as ``"INFO"``.
|
|
@@ -85,9 +93,6 @@ def build_event(
|
|
|
85
93
|
Raises:
|
|
86
94
|
None.
|
|
87
95
|
"""
|
|
88
|
-
from log_foundry.config import get_config
|
|
89
|
-
from log_foundry.ids import new_log_id
|
|
90
|
-
|
|
91
96
|
cfg = get_config()
|
|
92
97
|
merged: dict[str, object] = {**cfg.defaults, **span.defaults, **baggage, **fields}
|
|
93
98
|
safe, clipped = sanitize_fields(merged, cfg=cfg)
|
|
@@ -226,8 +231,6 @@ def end_event(
|
|
|
226
231
|
Raises:
|
|
227
232
|
None.
|
|
228
233
|
"""
|
|
229
|
-
from log_foundry.config import get_config
|
|
230
|
-
|
|
231
234
|
level = "INFO" if status == "ok" else "ERROR"
|
|
232
235
|
event = build_event(span, level, _END_MESSAGE, fields={}, baggage={})
|
|
233
236
|
event["duration_ms"] = (time.monotonic() - span.start_ts) * 1000.0
|
|
@@ -266,8 +269,6 @@ def backfill_baggage(span: Span, baggage: dict[str, object]) -> None:
|
|
|
266
269
|
"""
|
|
267
270
|
if not baggage:
|
|
268
271
|
return
|
|
269
|
-
from log_foundry.config import get_config
|
|
270
|
-
|
|
271
272
|
safe, clipped = sanitize_fields(baggage, cfg=get_config())
|
|
272
273
|
for event in span.events:
|
|
273
274
|
if event.get("message") in (_START_MESSAGE, _END_MESSAGE):
|
|
@@ -142,7 +142,7 @@ class _Coercer:
|
|
|
142
142
|
``build_event`` already allocates per event, it is noise.
|
|
143
143
|
"""
|
|
144
144
|
|
|
145
|
-
__slots__ = ("_cfg", "_parents", "truncated")
|
|
145
|
+
__slots__ = ("_cfg", "_int_ceiling", "_parents", "truncated")
|
|
146
146
|
|
|
147
147
|
def __init__(self, cfg: Config) -> None:
|
|
148
148
|
"""Starts a pass with the configured ceilings and an empty ancestor chain.
|
|
@@ -152,6 +152,11 @@ class _Coercer:
|
|
|
152
152
|
recursion for the whole descent, so its ``id()`` cannot be recycled underneath us, and
|
|
153
153
|
two siblings referencing the same object are not a cycle and must both render.
|
|
154
154
|
|
|
155
|
+
The integer ceiling is resolved here, once per pass rather than once per integer
|
|
156
|
+
(SPEC-031 FR-004): it reads ``sys.get_int_max_str_digits()``, which cannot change
|
|
157
|
+
during a coercion pass, and the alternative sat four lines below an ``int.__lt__``
|
|
158
|
+
binding justified by this being a per-value hot path.
|
|
159
|
+
|
|
155
160
|
Args:
|
|
156
161
|
cfg: The config supplying ``max_value_bytes``, ``max_keys`` and ``max_depth``.
|
|
157
162
|
|
|
@@ -162,6 +167,7 @@ class _Coercer:
|
|
|
162
167
|
None.
|
|
163
168
|
"""
|
|
164
169
|
self._cfg = cfg
|
|
170
|
+
self._int_ceiling = _int_digit_ceiling(cfg.max_value_bytes)
|
|
165
171
|
self.truncated = False
|
|
166
172
|
self._parents: list[int] = []
|
|
167
173
|
|
|
@@ -368,7 +374,7 @@ class _Coercer:
|
|
|
368
374
|
"""
|
|
369
375
|
digits = value.bit_length() * _LOG10_2_NUM // _LOG10_2_DEN + 1
|
|
370
376
|
rendered = digits + 1 if _INT_LT(value, 0) else digits
|
|
371
|
-
if rendered <=
|
|
377
|
+
if rendered <= self._int_ceiling:
|
|
372
378
|
return value
|
|
373
379
|
self.truncated = True
|
|
374
380
|
return f"<int: ~{digits} digits>"
|
|
@@ -33,21 +33,45 @@ def _make_tcp(host: str, port: int, timeout: float) -> socket.socket:
|
|
|
33
33
|
return socket.create_connection((host, port), timeout=timeout)
|
|
34
34
|
|
|
35
35
|
|
|
36
|
-
def _make_udp() -> socket.socket:
|
|
37
|
-
"""Opens an unconnected UDP socket.
|
|
36
|
+
def _make_udp(host: str) -> socket.socket:
|
|
37
|
+
"""Opens an unconnected UDP socket in an address family the host resolves to (SPEC-031).
|
|
38
|
+
|
|
39
|
+
The family is resolved rather than assumed: a hardcoded ``AF_INET`` made every ``sendto``
|
|
40
|
+
to an IPv6 destination fail, silently, until the retry bound abandoned the message. TCP
|
|
41
|
+
never had the defect because ``socket.create_connection`` resolves for itself.
|
|
42
|
+
|
|
43
|
+
**IPv4 wins when the host offers it**, and taking the first result instead was measured
|
|
44
|
+
losing logs. ``getaddrinfo`` sorts by RFC 6724, which puts AAAA first, so a dual-stack
|
|
45
|
+
name like ``localhost`` would move from IPv4 — where every deployment of this library has
|
|
46
|
+
sent — to IPv6, and a collector bound to ``0.0.0.0:514`` would never see the datagram. UDP
|
|
47
|
+
is unconnected, so that failure is *silent*: ``sendto`` succeeds locally, ``emit`` returns,
|
|
48
|
+
and no counter moves. FR-002 AC-2 requires delivery to a hostname to be unchanged, and
|
|
49
|
+
this is what makes it so while AC-1 still holds. It is a fixed preference, not
|
|
50
|
+
happy-eyeballs, address caching, or a setting — none of which this FR builds.
|
|
38
51
|
|
|
39
52
|
This is a module-level seam so tests can substitute a fake socket without network access.
|
|
40
53
|
|
|
41
54
|
Args:
|
|
42
|
-
|
|
55
|
+
host: The destination host, resolved to choose the family.
|
|
43
56
|
|
|
44
57
|
Returns:
|
|
45
58
|
The socket.
|
|
46
59
|
|
|
47
60
|
Raises:
|
|
48
|
-
OSError: If the socket cannot be created.
|
|
61
|
+
OSError: If the host resolves to nothing, or the socket cannot be created. Both reach
|
|
62
|
+
``_send_one``'s handler, which counts and announces rather than raising — a
|
|
63
|
+
``gaierror`` is an ``OSError``, so an unresolvable host fails exactly as an
|
|
64
|
+
unreachable one already did. The empty-result case is raised as one explicitly: CPython
|
|
65
|
+
raises rather than returning ``[]``, but indexing it would produce an ``IndexError``,
|
|
66
|
+
which is *not* an ``OSError`` and would escape that handler into the caller — the one
|
|
67
|
+
thing SPEC-025 says this library may never do.
|
|
49
68
|
"""
|
|
50
|
-
|
|
69
|
+
families = [entry[0] for entry in socket.getaddrinfo(host, None, type=socket.SOCK_DGRAM)]
|
|
70
|
+
if not families:
|
|
71
|
+
raise socket.gaierror(socket.EAI_NONAME, "resolution returned no address family")
|
|
72
|
+
return socket.socket(
|
|
73
|
+
socket.AF_INET if socket.AF_INET in families else families[0], socket.SOCK_DGRAM
|
|
74
|
+
)
|
|
51
75
|
|
|
52
76
|
|
|
53
77
|
class SocketTransport:
|
|
@@ -235,6 +259,10 @@ class SocketTransport:
|
|
|
235
259
|
def _socket(self) -> socket.socket:
|
|
236
260
|
"""Returns the held socket, opening one if none is held.
|
|
237
261
|
|
|
262
|
+
The UDP address family is resolved here rather than per message, because this is the
|
|
263
|
+
only place a socket is created and the socket outlives every send made through it
|
|
264
|
+
(SPEC-031 FR-002).
|
|
265
|
+
|
|
238
266
|
Args:
|
|
239
267
|
None.
|
|
240
268
|
|
|
@@ -242,11 +270,11 @@ class SocketTransport:
|
|
|
242
270
|
The socket.
|
|
243
271
|
|
|
244
272
|
Raises:
|
|
245
|
-
OSError: If the socket cannot be created or connected.
|
|
273
|
+
OSError: If the socket cannot be created, resolved or connected.
|
|
246
274
|
"""
|
|
247
275
|
if self._sock is None:
|
|
248
276
|
self._sock = (
|
|
249
|
-
_make_udp() if self._transport == "udp" else _make_tcp(
|
|
277
|
+
_make_udp(self._host) if self._transport == "udp" else _make_tcp(
|
|
250
278
|
self._host, self._port, self._timeout
|
|
251
279
|
)
|
|
252
280
|
)
|
|
@@ -113,12 +113,16 @@ class RotatingFileSink:
|
|
|
113
113
|
Two independent triggers may be enabled, either or both. With a positive ``max_bytes`` it
|
|
114
114
|
rotates before the write that would push the active file past that size, so the file never
|
|
115
115
|
grows unbounded; with a ``when`` unit code and an interval it rotates on the first emit after
|
|
116
|
-
that period has elapsed since the last rotation.
|
|
116
|
+
that period has elapsed since the last rotation. That period is measured on the monotonic
|
|
117
|
+
clock (SPEC-031 FR-001), so a wall-clock step in either direction neither defers a rotation
|
|
118
|
+
nor forces an early one.
|
|
117
119
|
|
|
118
120
|
Rotation renames the active file through numbered backups, prunes any beyond the backup
|
|
119
121
|
count, and opens a fresh active file — a backup count of zero keeps none, simply replacing
|
|
120
|
-
the active file.
|
|
121
|
-
|
|
122
|
+
the active file. Backups are numbered rather than timestamped, so no filename derives from
|
|
123
|
+
a clock at all and the monotonic deadline has no naming consequence. No event is lost across
|
|
124
|
+
a rotation, because the rotate happens before the pending event is written and the event
|
|
125
|
+
lands in the fresh file.
|
|
122
126
|
|
|
123
127
|
A rotation rebinds the active stream, so it is the sink where concurrent writers did real
|
|
124
128
|
damage: a second thread mid-``emit`` could write to the handle rotation had just closed, or
|
|
@@ -249,24 +253,34 @@ class RotatingFileSink:
|
|
|
249
253
|
return unit * interval
|
|
250
254
|
|
|
251
255
|
def _schedule_next(self) -> float | None:
|
|
252
|
-
"""Returns the
|
|
256
|
+
"""Returns the monotonic-clock deadline for the next time-based rotation (SPEC-031).
|
|
257
|
+
|
|
258
|
+
Monotonic rather than wall-clock for the reason ``Span.start_ts`` is: a backward step —
|
|
259
|
+
an NTP correction, a container clock sync — larger than the interval would otherwise
|
|
260
|
+
defer every time-based rotation until wall-clock caught up, silently defeating this
|
|
261
|
+
class's promise to bound on-disk growth. Nothing here is a timestamp anyone reads; it is
|
|
262
|
+
only ever compared against another reading of the same clock.
|
|
253
263
|
|
|
254
264
|
Args:
|
|
255
265
|
None.
|
|
256
266
|
|
|
257
267
|
Returns:
|
|
258
|
-
The
|
|
268
|
+
The deadline as a ``time.monotonic()`` reading, or ``None`` when there is no time
|
|
269
|
+
trigger.
|
|
259
270
|
|
|
260
271
|
Raises:
|
|
261
272
|
None.
|
|
262
273
|
"""
|
|
263
274
|
if self._interval_seconds is None:
|
|
264
275
|
return None
|
|
265
|
-
return time.
|
|
276
|
+
return time.monotonic() + self._interval_seconds
|
|
266
277
|
|
|
267
278
|
def _should_rotate(self, incoming: int) -> bool:
|
|
268
279
|
"""Decides whether to rotate before writing the next event.
|
|
269
280
|
|
|
281
|
+
The time trigger reads ``time.monotonic()``, the same clock :meth:`_schedule_next`
|
|
282
|
+
wrote the deadline on; comparing the two clocks is what the SPEC-031 fix removes.
|
|
283
|
+
|
|
270
284
|
Args:
|
|
271
285
|
incoming: The byte cost of the event about to be written.
|
|
272
286
|
|
|
@@ -282,7 +296,7 @@ class RotatingFileSink:
|
|
|
282
296
|
and self._size + incoming > self._max_bytes
|
|
283
297
|
):
|
|
284
298
|
return True
|
|
285
|
-
return self._next_rollover is not None and time.
|
|
299
|
+
return self._next_rollover is not None and time.monotonic() >= self._next_rollover
|
|
286
300
|
|
|
287
301
|
def _rotate(self) -> None:
|
|
288
302
|
"""Closes the active file, shifts and prunes backups, then opens a fresh active file.
|
|
@@ -24,6 +24,18 @@ class LogstashSink:
|
|
|
24
24
|
:class:`~log_foundry.sinks._socket.SocketTransport`. Either backend handles its own bounded
|
|
25
25
|
retry and raises on total failure of its own accord, so ``emit`` needs no rule of its own.
|
|
26
26
|
|
|
27
|
+
In socket mode both IPv4 and IPv6 destinations are supported, over either transport
|
|
28
|
+
(SPEC-031 FR-002): the UDP socket's address family is resolved from ``host`` rather than
|
|
29
|
+
assumed, which is what an unconditional ``AF_INET`` used to make impossible.
|
|
30
|
+
|
|
31
|
+
**A hostname that resolves to both families goes to IPv4**, because that is where every
|
|
32
|
+
release before this one sent and moving it silently would strand a collector bound to
|
|
33
|
+
``0.0.0.0``. The consequence is the mirror case, stated because nothing else states it: a
|
|
34
|
+
dual-stack *name* whose collector listens on IPv6 only will not be reached over UDP, and
|
|
35
|
+
since UDP is unconnected that failure is silent — no exception, no counter. Give the IPv6
|
|
36
|
+
literal, or a name with no ``A`` record, to select IPv6 for such a destination. TCP is
|
|
37
|
+
unaffected either way; ``create_connection`` tries each candidate in turn.
|
|
38
|
+
|
|
27
39
|
It takes **no** transport lock (SPEC-028 FR-002) of its own: whichever backend it built owns
|
|
28
40
|
that decision — ``SocketTransport`` locks its sends, ``HTTPSink`` holds no transport to
|
|
29
41
|
guard. The post-close rule follows the same split (SPEC-032 FR-004), and the two modes
|
|
@@ -29,10 +29,15 @@ class StdoutSink:
|
|
|
29
29
|
"""
|
|
30
30
|
|
|
31
31
|
def __init__(self, stream: TextIO | None = None) -> None:
|
|
32
|
-
"""Binds the sink to an output stream.
|
|
32
|
+
"""Binds the sink to an output stream, once, at construction.
|
|
33
|
+
|
|
34
|
+
The binding is deliberate and permanent for the life of the sink: a later
|
|
35
|
+
``contextlib.redirect_stdout`` or a test's capture of ``sys.stdout`` is not honoured,
|
|
36
|
+
because the attribute was resolved here. Passing ``stream=`` explicitly is how a caller
|
|
37
|
+
— a test above all — captures the output (SPEC-031 FR-003).
|
|
33
38
|
|
|
34
39
|
Args:
|
|
35
|
-
stream: The stream to write to, defaulting to ``sys.stdout
|
|
40
|
+
stream: The stream to write to, defaulting to ``sys.stdout`` as resolved now.
|
|
36
41
|
|
|
37
42
|
Returns:
|
|
38
43
|
None.
|
|
@@ -34,6 +34,18 @@ class SyslogSink:
|
|
|
34
34
|
and a severity mapped from the event's level. UDP sends one datagram per event, TCP uses
|
|
35
35
|
octet-counted framing (RFC 6587), and the whole sink is dependency-free.
|
|
36
36
|
|
|
37
|
+
Both IPv4 and IPv6 destinations are supported, on either transport (SPEC-031 FR-002): the
|
|
38
|
+
UDP socket's address family is resolved from ``host`` rather than assumed, which is what an
|
|
39
|
+
unconditional ``AF_INET`` used to make impossible.
|
|
40
|
+
|
|
41
|
+
**A hostname that resolves to both families goes to IPv4**, because that is where every
|
|
42
|
+
release before this one sent and moving it silently would strand a collector bound to
|
|
43
|
+
``0.0.0.0``. The consequence is the mirror case, stated because nothing else states it: a
|
|
44
|
+
dual-stack *name* whose collector listens on IPv6 only will not be reached over UDP, and
|
|
45
|
+
since UDP is unconnected that failure is silent — no exception, no counter. Give the IPv6
|
|
46
|
+
literal, or a name with no ``A`` record, to select IPv6 for such a destination. TCP is
|
|
47
|
+
unaffected either way; ``create_connection`` tries each candidate in turn.
|
|
48
|
+
|
|
37
49
|
It takes **no** transport lock (SPEC-028 FR-002) of its own: the socket it holds is a
|
|
38
50
|
:class:`~log_foundry.sinks._socket.SocketTransport`, which locks its own sends. Its
|
|
39
51
|
post-close refusal comes from there too (SPEC-032 FR-004) — a batch emitted after
|
|
@@ -20,11 +20,16 @@ class StderrSink(StdoutSink):
|
|
|
20
20
|
"""
|
|
21
21
|
|
|
22
22
|
def __init__(self, stream: TextIO | None = None) -> None:
|
|
23
|
-
"""Binds the sink to an output stream.
|
|
23
|
+
"""Binds the sink to an output stream, once, at construction.
|
|
24
|
+
|
|
25
|
+
The binding is resolved here and not re-read per write, so a later
|
|
26
|
+
``contextlib.redirect_stderr`` is not honoured — the same property
|
|
27
|
+
:class:`~log_foundry.sinks.stdout.StdoutSink` documents (SPEC-031 FR-003), restated
|
|
28
|
+
because this override means none of that docstring is inherited.
|
|
24
29
|
|
|
25
30
|
Args:
|
|
26
|
-
stream: The stream to write to, defaulting to ``sys.stderr
|
|
27
|
-
as a ``StringIO``, can be injected for capture.
|
|
31
|
+
stream: The stream to write to, defaulting to ``sys.stderr`` as resolved now. An
|
|
32
|
+
explicit one, such as a ``StringIO``, can be injected for capture.
|
|
28
33
|
|
|
29
34
|
Returns:
|
|
30
35
|
None.
|
|
@@ -854,6 +854,12 @@ class Worker:
|
|
|
854
854
|
this sweep, and that one waits out its timeout — then returns False, which is correct
|
|
855
855
|
either way.
|
|
856
856
|
|
|
857
|
+
The reliance on ``queue.Queue``'s private ``mutex`` and ``queue`` is deliberate and is
|
|
858
|
+
recorded in ``architecture.md`` §13 Known Constraints (SPEC-031 FR-005): there is no
|
|
859
|
+
public "inspect without consuming", and the draining alternative would destroy the
|
|
860
|
+
evidence the terminal-failure line reports. A CPython change would surface as the test
|
|
861
|
+
that exercises this against a mixed queue, rather than as waiters silently timing out.
|
|
862
|
+
|
|
857
863
|
Args:
|
|
858
864
|
None.
|
|
859
865
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{log_foundry-0.10.2.dev29 → log_foundry-0.10.2.dev30}/src/log_foundry/sinks/elasticsearch.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|