log-foundry 0.10.2.dev11__tar.gz → 0.10.2.dev13__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.dev11 → log_foundry-0.10.2.dev13}/PKG-INFO +1 -1
- {log_foundry-0.10.2.dev11 → log_foundry-0.10.2.dev13}/pyproject.toml +1 -1
- log_foundry-0.10.2.dev13/src/log_foundry/_diag.py +215 -0
- {log_foundry-0.10.2.dev11 → log_foundry-0.10.2.dev13}/src/log_foundry/decorator.py +5 -22
- {log_foundry-0.10.2.dev11 → log_foundry-0.10.2.dev13}/src/log_foundry/worker.py +23 -29
- log_foundry-0.10.2.dev11/src/log_foundry/_diag.py +0 -59
- {log_foundry-0.10.2.dev11 → log_foundry-0.10.2.dev13}/LICENSE +0 -0
- {log_foundry-0.10.2.dev11 → log_foundry-0.10.2.dev13}/README.md +0 -0
- {log_foundry-0.10.2.dev11 → log_foundry-0.10.2.dev13}/src/log_foundry/__init__.py +0 -0
- {log_foundry-0.10.2.dev11 → log_foundry-0.10.2.dev13}/src/log_foundry/api.py +0 -0
- {log_foundry-0.10.2.dev11 → log_foundry-0.10.2.dev13}/src/log_foundry/config.py +0 -0
- {log_foundry-0.10.2.dev11 → log_foundry-0.10.2.dev13}/src/log_foundry/console.py +0 -0
- {log_foundry-0.10.2.dev11 → log_foundry-0.10.2.dev13}/src/log_foundry/context.py +0 -0
- {log_foundry-0.10.2.dev11 → log_foundry-0.10.2.dev13}/src/log_foundry/ids.py +0 -0
- {log_foundry-0.10.2.dev11 → log_foundry-0.10.2.dev13}/src/log_foundry/model.py +0 -0
- {log_foundry-0.10.2.dev11 → log_foundry-0.10.2.dev13}/src/log_foundry/py.typed +0 -0
- {log_foundry-0.10.2.dev11 → log_foundry-0.10.2.dev13}/src/log_foundry/sanitize.py +0 -0
- {log_foundry-0.10.2.dev11 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/__init__.py +0 -0
- {log_foundry-0.10.2.dev11 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/_batch.py +0 -0
- {log_foundry-0.10.2.dev11 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/_chunk.py +0 -0
- {log_foundry-0.10.2.dev11 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/_socket.py +0 -0
- {log_foundry-0.10.2.dev11 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/_time.py +0 -0
- {log_foundry-0.10.2.dev11 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/base.py +0 -0
- {log_foundry-0.10.2.dev11 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/callback.py +0 -0
- {log_foundry-0.10.2.dev11 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/clickhouse.py +0 -0
- {log_foundry-0.10.2.dev11 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/datadog.py +0 -0
- {log_foundry-0.10.2.dev11 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/elasticsearch.py +0 -0
- {log_foundry-0.10.2.dev11 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/eventhubs.py +0 -0
- {log_foundry-0.10.2.dev11 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/file.py +0 -0
- {log_foundry-0.10.2.dev11 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/filtering.py +0 -0
- {log_foundry-0.10.2.dev11 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/firehose.py +0 -0
- {log_foundry-0.10.2.dev11 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/honeycomb.py +0 -0
- {log_foundry-0.10.2.dev11 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/http.py +0 -0
- {log_foundry-0.10.2.dev11 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/kafka.py +0 -0
- {log_foundry-0.10.2.dev11 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/kinesis.py +0 -0
- {log_foundry-0.10.2.dev11 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/logging_sink.py +0 -0
- {log_foundry-0.10.2.dev11 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/logstash.py +0 -0
- {log_foundry-0.10.2.dev11 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/loki.py +0 -0
- {log_foundry-0.10.2.dev11 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/mongodb.py +0 -0
- {log_foundry-0.10.2.dev11 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/multi.py +0 -0
- {log_foundry-0.10.2.dev11 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/nats.py +0 -0
- {log_foundry-0.10.2.dev11 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/newrelic.py +0 -0
- {log_foundry-0.10.2.dev11 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/postgres.py +0 -0
- {log_foundry-0.10.2.dev11 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/pubsub.py +0 -0
- {log_foundry-0.10.2.dev11 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/rabbitmq.py +0 -0
- {log_foundry-0.10.2.dev11 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/redis.py +0 -0
- {log_foundry-0.10.2.dev11 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/sentry.py +0 -0
- {log_foundry-0.10.2.dev11 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/sns.py +0 -0
- {log_foundry-0.10.2.dev11 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/splunk.py +0 -0
- {log_foundry-0.10.2.dev11 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/sqlite.py +0 -0
- {log_foundry-0.10.2.dev11 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/sqs.py +0 -0
- {log_foundry-0.10.2.dev11 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/stdout.py +0 -0
- {log_foundry-0.10.2.dev11 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/syslog.py +0 -0
- {log_foundry-0.10.2.dev11 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/transform.py +0 -0
- {log_foundry-0.10.2.dev11 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/util.py +0 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""The library's own diagnostic channel — one line on stderr, for what it lost or absorbed.
|
|
2
|
+
|
|
3
|
+
This library *is* the logger, so it cannot report its own faults through itself: an event
|
|
4
|
+
describing a broken sink would be handed to that same broken sink. Stderr is the channel of last
|
|
5
|
+
resort, and **every** line the library writes about itself goes through here, so the wording and
|
|
6
|
+
the safety rules are decided once rather than remembered at twenty-odd call sites — which is how
|
|
7
|
+
twelve of them came to disagree with the other eight (SPEC-029).
|
|
8
|
+
|
|
9
|
+
Three rules, all load-bearing:
|
|
10
|
+
|
|
11
|
+
* **The exception's type, never its message.** A message can carry a value from the event that
|
|
12
|
+
provoked it, and arch §6 keeps user data out of anything the library emits about itself — the
|
|
13
|
+
same rule SPEC-019 applies to ``Health.stopped_reason`` and ``sanitize`` applies by refusing
|
|
14
|
+
``repr(value)``. ``PostgresSink`` is the sharpest case: ``_row`` binds the whole
|
|
15
|
+
``json.dumps(event)`` as a statement parameter, and a psycopg error repr routinely reprints the
|
|
16
|
+
failing statement *and its parameters* — so a naive diagnostic for a failed insert would reprint
|
|
17
|
+
the event, PII included, into a stream nobody was asked to secure. A type name tells a
|
|
18
|
+
``ConnectionError`` from a ``TypeError``, which is what the reader needs. Where a bare type is
|
|
19
|
+
genuinely not diagnosable — an ``OSError`` alone does not say "refused" from "host unknown" —
|
|
20
|
+
the caller passes a ``detail`` built from values the *library* controls: an ``errno`` (see
|
|
21
|
+
:func:`errno_of`), an HTTP status, an attempt count, a sink's class name. Never the exception's
|
|
22
|
+
text. Any ``detail`` is escaped and bounded regardless, so a value that reached one could not
|
|
23
|
+
forge a second line in an operator's console.
|
|
24
|
+
* **No stream fault escapes.** These writers are called from ``except`` and ``finally`` blocks
|
|
25
|
+
whose entire purpose is to stop an exception reaching the caller, and from the worker thread
|
|
26
|
+
where an escaping exception ends delivery for good. A failure here — a closed stderr at
|
|
27
|
+
interpreter shutdown, a stream that rejects the write, ``sys.stderr`` set to ``None`` — must not
|
|
28
|
+
become the failure those guards existed to prevent. A ``BaseException`` still passes through, as
|
|
29
|
+
everywhere else in this library: a ``KeyboardInterrupt`` landing mid-write is the operator's
|
|
30
|
+
intent, not a stream fault to swallow.
|
|
31
|
+
* **Record first, announce second.** A counter moves *before* its line is attempted, at every call
|
|
32
|
+
site. The write is best-effort by construction; the counter that ``health()`` reports is not, and
|
|
33
|
+
must not be able to ride on it.
|
|
34
|
+
|
|
35
|
+
Stderr stays the channel: routing through the ``logging`` module would make the library's own
|
|
36
|
+
failure reports depend on machinery that may be the thing failing, and ``LoggingSink`` already
|
|
37
|
+
exists for the opposite direction. These lines are for humans — the structured stream is the sink.
|
|
38
|
+
|
|
39
|
+
**This module must import nothing from its own package.** ``decorator``, ``api``, ``worker`` and
|
|
40
|
+
every sink reach it with ``from log_foundry import _diag`` at module scope, which executes while
|
|
41
|
+
the package is still partially initialised and resolves only because ``_diag`` is a leaf — it needs
|
|
42
|
+
``sys`` and nothing else. Give it an intra-package import and every module that is imported *first*
|
|
43
|
+
in a fresh interpreter starts failing.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
from __future__ import annotations
|
|
47
|
+
|
|
48
|
+
import sys
|
|
49
|
+
|
|
50
|
+
__all__ = ["absorbed", "errno_of", "lost", "rejected"]
|
|
51
|
+
|
|
52
|
+
# Bound on any caller-supplied detail string, applied after escaping so it bounds what is
|
|
53
|
+
# *written*. Generous enough for an attempt count plus a sink name plus an errno; far short of a
|
|
54
|
+
# payload.
|
|
55
|
+
_MAX_DETAIL = 200
|
|
56
|
+
|
|
57
|
+
# Bound on how much of a rejected inbound value is echoed (SPEC-014). Tighter than _MAX_DETAIL:
|
|
58
|
+
# the value is attacker-controllable, and its shape is the diagnosis, not its contents.
|
|
59
|
+
_MAX_REJECTED_ECHO = 64
|
|
60
|
+
|
|
61
|
+
_ESCAPES = {"\t": "\\t", "\n": "\\n", "\r": "\\r"}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def absorbed(where: str, exc: BaseException, detail: str = "") -> None:
|
|
65
|
+
"""Report a failure the library swallowed rather than propagated.
|
|
66
|
+
|
|
67
|
+
Never raises on a stream fault; a ``BaseException`` from the write still propagates.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
where: What was being attempted, as a participle phrase that reads after "while" —
|
|
71
|
+
``"closing a span"``, ``"emitting an orphan log"``. A literal, never a runtime value.
|
|
72
|
+
exc: The absorbed exception. Only ``type(exc).__name__`` is written.
|
|
73
|
+
detail: Optional consequence for the reader, appended after a semicolon — most usefully
|
|
74
|
+
what was lost, since an absorbed failure is invisible apart from this line. Escaped
|
|
75
|
+
and truncated to ``_MAX_DETAIL``; must not be derived from ``exc``'s text.
|
|
76
|
+
"""
|
|
77
|
+
try:
|
|
78
|
+
sys.stderr.write(
|
|
79
|
+
f"log-foundry: absorbed a failure while {_escape(where)} "
|
|
80
|
+
f"({_escape(type(exc).__name__)}){_suffix(detail)}\n"
|
|
81
|
+
)
|
|
82
|
+
except Exception:
|
|
83
|
+
# The channel of last resort has no fallback of its own. Reporting is best-effort by
|
|
84
|
+
# construction: losing the line is bad, raising from the guard that exists to protect
|
|
85
|
+
# the caller's exception would be worse (arch §4).
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def lost(what: str, count: int, detail: str = "") -> None:
|
|
90
|
+
"""Report a counted loss — events, messages, batches or rows that will not be delivered.
|
|
91
|
+
|
|
92
|
+
Never raises on a stream fault; a ``BaseException`` from the write still propagates.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
what: Singular noun for the unit lost — ``"event"``, ``"message"``, ``"row"``. Rendered
|
|
96
|
+
with the corpus-wide ``(s)`` suffix, so it must be a literal, never a runtime value.
|
|
97
|
+
count: How many this line reports as lost — normally the increment the caller's counter
|
|
98
|
+
has just taken. A **throttled** site instead passes its running total, because a line
|
|
99
|
+
written on every thousandth drop that said "lost 1" would read as one loss rather than
|
|
100
|
+
a thousand; such a site says so in ``detail``. Recorded before this call either way.
|
|
101
|
+
detail: Optional circumstances — the sink's class name, the attempt count, an ``errno``,
|
|
102
|
+
an exception *type*. Escaped and truncated to ``_MAX_DETAIL``.
|
|
103
|
+
"""
|
|
104
|
+
try:
|
|
105
|
+
sys.stderr.write(f"log-foundry: lost {count} {_escape(what)}(s){_suffix(detail)}\n")
|
|
106
|
+
except Exception:
|
|
107
|
+
pass # best-effort, as above; the counter this line describes is already recorded.
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def rejected(reason: str, value: object) -> None:
|
|
111
|
+
"""Report an inbound trace context the library refused to adopt (SPEC-014).
|
|
112
|
+
|
|
113
|
+
The offending value is echoed as a **bounded ``repr``** — the one place in the library where a
|
|
114
|
+
``repr`` is correct, because the input is an inbound *header* rather than an exception, and its
|
|
115
|
+
exact shape is what makes a rejection diagnosable. Unbounded it would be a log-injection
|
|
116
|
+
surface: the value is attacker-controllable.
|
|
117
|
+
|
|
118
|
+
The ``repr`` is escaped afterwards even though ``repr`` of a ``str`` is already printable
|
|
119
|
+
throughout, which makes this a no-op for every value the call sites actually pass. It is not a
|
|
120
|
+
no-op in general: ``repr`` runs ``__repr__``, which is user code free to *return* a raw newline
|
|
121
|
+
no matter what the built-in reprs would have done. Escaping the result is the difference
|
|
122
|
+
between "the built-ins happen to escape" and "this cannot forge a line".
|
|
123
|
+
|
|
124
|
+
Never raises on a stream fault; a ``BaseException`` from the write still propagates.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
reason: Why it was refused — ``"unparseable traceparent"``, ``"invalid trace_id"``. A
|
|
128
|
+
literal, never a runtime value: unlike the echo, it is not bounded.
|
|
129
|
+
value: The refused value, echoed as a ``repr`` bounded to ``_MAX_REJECTED_ECHO``.
|
|
130
|
+
"""
|
|
131
|
+
try:
|
|
132
|
+
shown = _escape(repr(value))
|
|
133
|
+
if len(shown) > _MAX_REJECTED_ECHO:
|
|
134
|
+
shown = shown[:_MAX_REJECTED_ECHO] + "…"
|
|
135
|
+
sys.stderr.write(
|
|
136
|
+
f"log-foundry: ignoring inbound trace context ({_escape(reason)}): {shown}\n"
|
|
137
|
+
)
|
|
138
|
+
except Exception:
|
|
139
|
+
pass # best-effort, as above.
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def errno_of(exc: BaseException) -> str:
|
|
143
|
+
"""Return ``"errno=N"`` for an exception carrying one, else ``""``.
|
|
144
|
+
|
|
145
|
+
The library-controlled detail that makes an ``OSError`` line actionable: "connection refused"
|
|
146
|
+
and "host unknown" are the same type name and are told apart only by the code. An integer from
|
|
147
|
+
the OS is not caller data, which is what makes it safe to write where the message is not.
|
|
148
|
+
|
|
149
|
+
``urllib``'s ``URLError`` carries the underlying socket error on ``.reason`` rather than
|
|
150
|
+
setting its own ``errno``, so that is consulted too. Total: attribute access on an arbitrary
|
|
151
|
+
exception can run a property that raises, and a helper for a diagnostic must not become one.
|
|
152
|
+
|
|
153
|
+
The value is rendered through ``int()`` rather than interpolated as found. ``isinstance(x,
|
|
154
|
+
int)`` admits any *subclass*, and a driver's error code is routinely one — an ``IntEnum`` or a
|
|
155
|
+
bespoke class whose ``__str__`` returns whatever its author chose, which the f-string would
|
|
156
|
+
then have written verbatim. That is the leak this helper exists to be the alternative to.
|
|
157
|
+
"""
|
|
158
|
+
try:
|
|
159
|
+
code = getattr(exc, "errno", None)
|
|
160
|
+
if code is None:
|
|
161
|
+
code = getattr(getattr(exc, "reason", None), "errno", None)
|
|
162
|
+
return f"errno={int(code)}" if isinstance(code, int) else ""
|
|
163
|
+
except Exception:
|
|
164
|
+
return ""
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _suffix(detail: str) -> str:
|
|
168
|
+
"""Render a detail as a bounded, escaped ``"; ..."`` suffix, or ``""`` when there is none.
|
|
169
|
+
|
|
170
|
+
Escaping precedes truncation so the bound applies to what is written, and so truncation can
|
|
171
|
+
only ever remove characters from an already-safe string rather than split an escape into
|
|
172
|
+
something that isn't one.
|
|
173
|
+
|
|
174
|
+
Guarded in its own right, and returning ``""`` rather than propagating: this runs inside the
|
|
175
|
+
caller's f-string, so a detail that cannot be rendered would otherwise take the *whole* line
|
|
176
|
+
with it — including the count, which is the part an operator cannot reconstruct.
|
|
177
|
+
"""
|
|
178
|
+
if not detail:
|
|
179
|
+
return ""
|
|
180
|
+
try:
|
|
181
|
+
shown = _escape(detail)
|
|
182
|
+
if len(shown) > _MAX_DETAIL:
|
|
183
|
+
shown = shown[:_MAX_DETAIL] + "…"
|
|
184
|
+
return f"; {shown}"
|
|
185
|
+
except Exception:
|
|
186
|
+
return ""
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _escape(text: str) -> str:
|
|
190
|
+
"""Render every non-printable character visibly, so nothing in ``text`` can forge a line.
|
|
191
|
+
|
|
192
|
+
``str.isprintable()`` is the test rather than a table over ``range(0x20)``: it is ``False`` for
|
|
193
|
+
the C0 block *and* for DEL, the C1 block (U+009B is CSI, which a terminal reads as the start of
|
|
194
|
+
an escape sequence), U+0085, U+2028, U+2029, and the bidi format characters. Python's own
|
|
195
|
+
``splitlines()`` breaks on three of those a C0 table misses, so a log shipper or a reader doing
|
|
196
|
+
the obvious thing would see a forged ``log-foundry:`` line that a newline count says is not
|
|
197
|
+
there. Space is printable, so ordinary text is untouched.
|
|
198
|
+
|
|
199
|
+
The whole-string fast path keeps the overwhelmingly common case — a sink name, an attempt
|
|
200
|
+
count, an exception type — to a single C-level call.
|
|
201
|
+
"""
|
|
202
|
+
if text.isprintable():
|
|
203
|
+
return text
|
|
204
|
+
return "".join(char if char.isprintable() else _escaped(char) for char in text)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _escaped(char: str) -> str:
|
|
208
|
+
"""Escape one non-printable character, following Python's own ``repr`` conventions."""
|
|
209
|
+
escape = _ESCAPES.get(char)
|
|
210
|
+
if escape is not None:
|
|
211
|
+
return escape
|
|
212
|
+
code = ord(char)
|
|
213
|
+
if code <= 0xFF:
|
|
214
|
+
return f"\\x{code:02x}"
|
|
215
|
+
return f"\\u{code:04x}" if code <= 0xFFFF else f"\\U{code:08x}"
|
|
@@ -24,7 +24,6 @@ from __future__ import annotations
|
|
|
24
24
|
import asyncio
|
|
25
25
|
import atexit
|
|
26
26
|
import functools
|
|
27
|
-
import sys
|
|
28
27
|
import threading
|
|
29
28
|
from collections.abc import Callable
|
|
30
29
|
from time import monotonic
|
|
@@ -47,9 +46,6 @@ if TYPE_CHECKING:
|
|
|
47
46
|
|
|
48
47
|
__all__ = ["continue_trace", "trace"]
|
|
49
48
|
|
|
50
|
-
# Bound on how much of a rejected inbound value is echoed into a stderr warning.
|
|
51
|
-
_MAX_REJECTED_ECHO = 64
|
|
52
|
-
|
|
53
49
|
# One background worker per process (SPEC-004), created lazily from the configured sink on the
|
|
54
50
|
# first flush. The double-checked lock makes concurrent first-flushes create exactly one.
|
|
55
51
|
_worker: Worker | None = None
|
|
@@ -86,19 +82,6 @@ def _open_span(name: str, defaults: dict[str, object] | None) -> Span:
|
|
|
86
82
|
return span
|
|
87
83
|
|
|
88
84
|
|
|
89
|
-
def _warn_rejected(reason: str, value: object) -> None:
|
|
90
|
-
"""Report a rejected inbound context on stderr, as ``worker`` and ``SQSSink`` do.
|
|
91
|
-
|
|
92
|
-
The offending value is echoed only as a **bounded ``repr``**. Unbounded is a log-injection
|
|
93
|
-
surface — the value is attacker-controllable, and `repr` additionally escapes newlines and
|
|
94
|
-
control characters so it cannot forge a second log line in an operator's console.
|
|
95
|
-
"""
|
|
96
|
-
shown = repr(value)
|
|
97
|
-
if len(shown) > _MAX_REJECTED_ECHO:
|
|
98
|
-
shown = shown[:_MAX_REJECTED_ECHO] + "…"
|
|
99
|
-
sys.stderr.write(f"log-foundry: ignoring inbound trace context ({reason}): {shown}\n")
|
|
100
|
-
|
|
101
|
-
|
|
102
85
|
def continue_trace(
|
|
103
86
|
traceparent: str | None = None,
|
|
104
87
|
*,
|
|
@@ -144,19 +127,19 @@ def continue_trace(
|
|
|
144
127
|
if traceparent is not None:
|
|
145
128
|
if trace_id is not None or parent_span_id is not None:
|
|
146
129
|
# A programming error, not bad input: the caller supplied the same thing twice.
|
|
147
|
-
|
|
130
|
+
_diag.rejected("both traceparent and explicit ids given; traceparent wins", traceparent)
|
|
148
131
|
parsed = parse_traceparent(traceparent)
|
|
149
132
|
if parsed is None:
|
|
150
|
-
|
|
133
|
+
_diag.rejected("unparseable traceparent", traceparent)
|
|
151
134
|
else:
|
|
152
135
|
adopted = parsed
|
|
153
136
|
elif trace_id is not None:
|
|
154
137
|
if not is_valid_trace_id(trace_id):
|
|
155
|
-
|
|
138
|
+
_diag.rejected("invalid trace_id", trace_id)
|
|
156
139
|
elif parent_span_id is not None and not is_valid_span_id(parent_span_id):
|
|
157
140
|
# Drop just the parent and join as another root rather than reject the whole
|
|
158
141
|
# context: being in the right trace without a parent beats being in a fresh one.
|
|
159
|
-
|
|
142
|
+
_diag.rejected("invalid parent_span_id; joining as a root", parent_span_id)
|
|
160
143
|
adopted = (trace_id, None)
|
|
161
144
|
else:
|
|
162
145
|
# parent_span_id may legitimately be omitted — a consumer that knows the trace but
|
|
@@ -175,7 +158,7 @@ def continue_trace(
|
|
|
175
158
|
if parsed_baggage is None:
|
|
176
159
|
# Deliberately independent of the trace context above: losing correlating fields is
|
|
177
160
|
# bad, and losing the trace join because one field was malformed is worse.
|
|
178
|
-
|
|
161
|
+
_diag.rejected("unusable baggage header", baggage)
|
|
179
162
|
else:
|
|
180
163
|
context.set_baggage(**parsed_baggage)
|
|
181
164
|
|
|
@@ -20,7 +20,6 @@ nothing about spans or context (the same dumbness that makes sinks swappable).
|
|
|
20
20
|
from __future__ import annotations
|
|
21
21
|
|
|
22
22
|
import queue
|
|
23
|
-
import sys
|
|
24
23
|
import threading
|
|
25
24
|
import time
|
|
26
25
|
from typing import TYPE_CHECKING, NamedTuple, cast
|
|
@@ -162,15 +161,7 @@ class Worker:
|
|
|
162
161
|
# submitters. Lines may therefore interleave out of order under concurrency; the
|
|
163
162
|
# counts they carry are still exact.
|
|
164
163
|
if total == 1 or total % _DROP_WARN_EVERY == 0:
|
|
165
|
-
|
|
166
|
-
sys.stderr.write(
|
|
167
|
-
f"log-foundry: log queue full, dropped {total} submission(s) so far\n"
|
|
168
|
-
)
|
|
169
|
-
except Exception: # submit() runs on the *caller's* thread, so an
|
|
170
|
-
# unwritable stderr (closed fd, broken pipe, daemonized process) would raise
|
|
171
|
-
# straight into the app. A diagnostic about dropped logs must never itself be
|
|
172
|
-
# the reason a decorated function fails. The counter is already recorded.
|
|
173
|
-
pass
|
|
164
|
+
_diag.lost("submission", total, "log queue full; count is cumulative")
|
|
174
165
|
|
|
175
166
|
def health(self) -> Health:
|
|
176
167
|
"""Snapshot the delivery counters (SPEC-017 FR-005, SPEC-019 FR-003). Never raises.
|
|
@@ -323,36 +314,36 @@ class Worker:
|
|
|
323
314
|
|
|
324
315
|
Recording precedes announcing: stderr may be closed or wedged, and unlike the overflow
|
|
325
316
|
warning this line is written exactly once and cannot be re-emitted later, so the record
|
|
326
|
-
must not be able to ride on it. The exception's *type* is reported and its message is
|
|
327
|
-
|
|
328
|
-
|
|
317
|
+
must not be able to ride on it. The exception's *type* is reported and its message is not
|
|
318
|
+
— the rule ``_diag`` now applies to every line the library writes (SPEC-029), and the
|
|
319
|
+
reason this site had it first.
|
|
329
320
|
|
|
330
|
-
The
|
|
331
|
-
|
|
332
|
-
|
|
321
|
+
The announcement is an :func:`~log_foundry._diag.absorbed`, not a fourth kind of line: the
|
|
322
|
+
thread's death *is* an exception this method caught and did not propagate, and what it
|
|
323
|
+
cost belongs in the detail. The count reports what was *in hand* and what was still
|
|
324
|
+
*queued behind it* (SPEC-021 FR-002). Held alone under-reads the loss: nothing will drain
|
|
325
|
+
the queue either, so an operator reading "1 undrained event-list(s)" could conclude far
|
|
326
|
+
less was lost than was.
|
|
333
327
|
|
|
334
328
|
The queued figure is "items", not "event-lists", and says so: like ``Health.queued`` it is
|
|
335
329
|
read without stopping the world, so it counts any internal flush/shutdown marker sitting
|
|
336
330
|
alongside real submissions, and a producer thread can add to the queue between the death
|
|
337
331
|
and the read. It is a floor on what was lost, which is the useful direction.
|
|
338
332
|
"""
|
|
339
|
-
name = type(exc).__name__
|
|
340
333
|
with self._lock:
|
|
341
|
-
self.stopped_reason =
|
|
334
|
+
self.stopped_reason = type(exc).__name__
|
|
342
335
|
try:
|
|
343
336
|
# In its own guard, and after the record: ``qsize()`` is not guaranteed on every
|
|
344
337
|
# platform's queue, and a diagnostic must not be the reason the diagnosis is lost.
|
|
345
338
|
queued: object = self._queue.qsize()
|
|
346
339
|
except Exception:
|
|
347
340
|
queued = "?"
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
except Exception: # best-effort: the record above is what an operator reads.
|
|
355
|
-
pass
|
|
341
|
+
_diag.absorbed(
|
|
342
|
+
"draining the log queue",
|
|
343
|
+
exc,
|
|
344
|
+
f"worker thread stopped; {undrained} undrained event-list(s) held and {queued} "
|
|
345
|
+
f"queued item(s) undelivered, nothing further will be delivered",
|
|
346
|
+
)
|
|
356
347
|
|
|
357
348
|
def _drain(self, pending: list[list[dict[str, object]]]) -> None:
|
|
358
349
|
"""The drain loop proper. ``pending`` is owned by :meth:`_run`, which reports its size.
|
|
@@ -477,9 +468,12 @@ class Worker:
|
|
|
477
468
|
# than a half-updated pair. No deadlock: shutdown() releases before join().
|
|
478
469
|
with self._lock:
|
|
479
470
|
self.failed_batches += 1
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
471
|
+
# Through ``_diag`` so the write is guarded: unguarded, a broken stderr raised
|
|
472
|
+
# out of here, through ``_drain``, into ``_run``'s handler, and the drain
|
|
473
|
+
# thread died for good — a diagnostic about one lost batch costing every batch
|
|
474
|
+
# after it (SPEC-029 FR-003).
|
|
475
|
+
_diag.lost(
|
|
476
|
+
"event", len(batch), f"batch abandoned after {retries + 1} emit attempts"
|
|
483
477
|
)
|
|
484
478
|
return
|
|
485
479
|
# Backoff between attempts; _stop.wait returns at once during shutdown, so a
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
"""The library's own diagnostic channel — one line on stderr, for failures it absorbed.
|
|
2
|
-
|
|
3
|
-
This library *is* the logger, so it cannot report its own faults through itself: an event
|
|
4
|
-
describing a broken sink would be handed to that same broken sink. Stderr is the channel of last
|
|
5
|
-
resort, and every site that swallows an exception to keep architecture §4's promise (logging never
|
|
6
|
-
breaks the application) announces it through here, so the wording and the safety rules are decided
|
|
7
|
-
once rather than per call site.
|
|
8
|
-
|
|
9
|
-
Two rules, both load-bearing:
|
|
10
|
-
|
|
11
|
-
* **The exception's type, never its message.** A message can carry a value from the event that
|
|
12
|
-
provoked it, and arch §6 keeps user data out of anything the library emits about itself — the
|
|
13
|
-
same rule SPEC-019 applies to ``Health.stopped_reason``. A type name is enough to tell a
|
|
14
|
-
``ConnectionError`` from a ``TypeError``, which is what the reader needs.
|
|
15
|
-
* **No stream fault escapes.** It is called from ``except`` and ``finally`` blocks whose entire
|
|
16
|
-
purpose is to stop an exception reaching the caller, so a failure here — a closed stderr at
|
|
17
|
-
interpreter shutdown, a stream that rejects the write, ``sys.stderr`` set to ``None`` — must not
|
|
18
|
-
become the exception those guards existed to prevent. A ``BaseException`` still passes through,
|
|
19
|
-
as everywhere else in this library: a ``KeyboardInterrupt`` landing mid-write is the operator's
|
|
20
|
-
intent, not a stream fault to swallow.
|
|
21
|
-
|
|
22
|
-
**This module must import nothing from its own package.** ``decorator``, ``api`` and ``worker``
|
|
23
|
-
all reach it with ``from log_foundry import _diag`` at module scope, which executes while the
|
|
24
|
-
package is still partially initialised and resolves only because ``_diag`` is a leaf — it needs
|
|
25
|
-
``sys`` and nothing else. Give it an intra-package import and every module that is imported
|
|
26
|
-
*first* in a fresh interpreter starts failing.
|
|
27
|
-
|
|
28
|
-
SPEC-029 takes ownership of this module and moves the ``repr(exception)`` sink sites onto it.
|
|
29
|
-
"""
|
|
30
|
-
|
|
31
|
-
from __future__ import annotations
|
|
32
|
-
|
|
33
|
-
import sys
|
|
34
|
-
|
|
35
|
-
__all__ = ["absorbed"]
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def absorbed(where: str, exc: BaseException, detail: str = "") -> None:
|
|
39
|
-
"""Report a failure the library swallowed rather than propagated.
|
|
40
|
-
|
|
41
|
-
Never raises on a stream fault; a ``BaseException`` from the write still propagates.
|
|
42
|
-
|
|
43
|
-
Args:
|
|
44
|
-
where: What was being attempted, as a participle phrase that reads after "while" —
|
|
45
|
-
``"closing a span"``, ``"emitting an orphan log"``.
|
|
46
|
-
exc: The absorbed exception. Only ``type(exc).__name__`` is written.
|
|
47
|
-
detail: Optional consequence for the reader, appended after a semicolon — most usefully
|
|
48
|
-
what was lost, since an absorbed failure is invisible apart from this line.
|
|
49
|
-
"""
|
|
50
|
-
try:
|
|
51
|
-
suffix = f"; {detail}" if detail else ""
|
|
52
|
-
sys.stderr.write(
|
|
53
|
-
f"log-foundry: absorbed a failure while {where} ({type(exc).__name__}){suffix}\n"
|
|
54
|
-
)
|
|
55
|
-
except Exception:
|
|
56
|
-
# The channel of last resort has no fallback of its own. Reporting is best-effort by
|
|
57
|
-
# construction: losing the line is bad, raising from the guard that exists to protect
|
|
58
|
-
# the caller's exception would be worse (arch §4).
|
|
59
|
-
pass
|
|
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
|
{log_foundry-0.10.2.dev11 → log_foundry-0.10.2.dev13}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|