log-foundry 0.10.2.dev10__tar.gz → 0.10.2.dev12__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.
Files changed (55) hide show
  1. {log_foundry-0.10.2.dev10 → log_foundry-0.10.2.dev12}/PKG-INFO +1 -1
  2. {log_foundry-0.10.2.dev10 → log_foundry-0.10.2.dev12}/pyproject.toml +1 -1
  3. log_foundry-0.10.2.dev12/src/log_foundry/_diag.py +212 -0
  4. log_foundry-0.10.2.dev10/src/log_foundry/_diag.py +0 -59
  5. {log_foundry-0.10.2.dev10 → log_foundry-0.10.2.dev12}/LICENSE +0 -0
  6. {log_foundry-0.10.2.dev10 → log_foundry-0.10.2.dev12}/README.md +0 -0
  7. {log_foundry-0.10.2.dev10 → log_foundry-0.10.2.dev12}/src/log_foundry/__init__.py +0 -0
  8. {log_foundry-0.10.2.dev10 → log_foundry-0.10.2.dev12}/src/log_foundry/api.py +0 -0
  9. {log_foundry-0.10.2.dev10 → log_foundry-0.10.2.dev12}/src/log_foundry/config.py +0 -0
  10. {log_foundry-0.10.2.dev10 → log_foundry-0.10.2.dev12}/src/log_foundry/console.py +0 -0
  11. {log_foundry-0.10.2.dev10 → log_foundry-0.10.2.dev12}/src/log_foundry/context.py +0 -0
  12. {log_foundry-0.10.2.dev10 → log_foundry-0.10.2.dev12}/src/log_foundry/decorator.py +0 -0
  13. {log_foundry-0.10.2.dev10 → log_foundry-0.10.2.dev12}/src/log_foundry/ids.py +0 -0
  14. {log_foundry-0.10.2.dev10 → log_foundry-0.10.2.dev12}/src/log_foundry/model.py +0 -0
  15. {log_foundry-0.10.2.dev10 → log_foundry-0.10.2.dev12}/src/log_foundry/py.typed +0 -0
  16. {log_foundry-0.10.2.dev10 → log_foundry-0.10.2.dev12}/src/log_foundry/sanitize.py +0 -0
  17. {log_foundry-0.10.2.dev10 → log_foundry-0.10.2.dev12}/src/log_foundry/sinks/__init__.py +0 -0
  18. {log_foundry-0.10.2.dev10 → log_foundry-0.10.2.dev12}/src/log_foundry/sinks/_batch.py +0 -0
  19. {log_foundry-0.10.2.dev10 → log_foundry-0.10.2.dev12}/src/log_foundry/sinks/_chunk.py +0 -0
  20. {log_foundry-0.10.2.dev10 → log_foundry-0.10.2.dev12}/src/log_foundry/sinks/_socket.py +0 -0
  21. {log_foundry-0.10.2.dev10 → log_foundry-0.10.2.dev12}/src/log_foundry/sinks/_time.py +0 -0
  22. {log_foundry-0.10.2.dev10 → log_foundry-0.10.2.dev12}/src/log_foundry/sinks/base.py +0 -0
  23. {log_foundry-0.10.2.dev10 → log_foundry-0.10.2.dev12}/src/log_foundry/sinks/callback.py +0 -0
  24. {log_foundry-0.10.2.dev10 → log_foundry-0.10.2.dev12}/src/log_foundry/sinks/clickhouse.py +0 -0
  25. {log_foundry-0.10.2.dev10 → log_foundry-0.10.2.dev12}/src/log_foundry/sinks/datadog.py +0 -0
  26. {log_foundry-0.10.2.dev10 → log_foundry-0.10.2.dev12}/src/log_foundry/sinks/elasticsearch.py +0 -0
  27. {log_foundry-0.10.2.dev10 → log_foundry-0.10.2.dev12}/src/log_foundry/sinks/eventhubs.py +0 -0
  28. {log_foundry-0.10.2.dev10 → log_foundry-0.10.2.dev12}/src/log_foundry/sinks/file.py +0 -0
  29. {log_foundry-0.10.2.dev10 → log_foundry-0.10.2.dev12}/src/log_foundry/sinks/filtering.py +0 -0
  30. {log_foundry-0.10.2.dev10 → log_foundry-0.10.2.dev12}/src/log_foundry/sinks/firehose.py +0 -0
  31. {log_foundry-0.10.2.dev10 → log_foundry-0.10.2.dev12}/src/log_foundry/sinks/honeycomb.py +0 -0
  32. {log_foundry-0.10.2.dev10 → log_foundry-0.10.2.dev12}/src/log_foundry/sinks/http.py +0 -0
  33. {log_foundry-0.10.2.dev10 → log_foundry-0.10.2.dev12}/src/log_foundry/sinks/kafka.py +0 -0
  34. {log_foundry-0.10.2.dev10 → log_foundry-0.10.2.dev12}/src/log_foundry/sinks/kinesis.py +0 -0
  35. {log_foundry-0.10.2.dev10 → log_foundry-0.10.2.dev12}/src/log_foundry/sinks/logging_sink.py +0 -0
  36. {log_foundry-0.10.2.dev10 → log_foundry-0.10.2.dev12}/src/log_foundry/sinks/logstash.py +0 -0
  37. {log_foundry-0.10.2.dev10 → log_foundry-0.10.2.dev12}/src/log_foundry/sinks/loki.py +0 -0
  38. {log_foundry-0.10.2.dev10 → log_foundry-0.10.2.dev12}/src/log_foundry/sinks/mongodb.py +0 -0
  39. {log_foundry-0.10.2.dev10 → log_foundry-0.10.2.dev12}/src/log_foundry/sinks/multi.py +0 -0
  40. {log_foundry-0.10.2.dev10 → log_foundry-0.10.2.dev12}/src/log_foundry/sinks/nats.py +0 -0
  41. {log_foundry-0.10.2.dev10 → log_foundry-0.10.2.dev12}/src/log_foundry/sinks/newrelic.py +0 -0
  42. {log_foundry-0.10.2.dev10 → log_foundry-0.10.2.dev12}/src/log_foundry/sinks/postgres.py +0 -0
  43. {log_foundry-0.10.2.dev10 → log_foundry-0.10.2.dev12}/src/log_foundry/sinks/pubsub.py +0 -0
  44. {log_foundry-0.10.2.dev10 → log_foundry-0.10.2.dev12}/src/log_foundry/sinks/rabbitmq.py +0 -0
  45. {log_foundry-0.10.2.dev10 → log_foundry-0.10.2.dev12}/src/log_foundry/sinks/redis.py +0 -0
  46. {log_foundry-0.10.2.dev10 → log_foundry-0.10.2.dev12}/src/log_foundry/sinks/sentry.py +0 -0
  47. {log_foundry-0.10.2.dev10 → log_foundry-0.10.2.dev12}/src/log_foundry/sinks/sns.py +0 -0
  48. {log_foundry-0.10.2.dev10 → log_foundry-0.10.2.dev12}/src/log_foundry/sinks/splunk.py +0 -0
  49. {log_foundry-0.10.2.dev10 → log_foundry-0.10.2.dev12}/src/log_foundry/sinks/sqlite.py +0 -0
  50. {log_foundry-0.10.2.dev10 → log_foundry-0.10.2.dev12}/src/log_foundry/sinks/sqs.py +0 -0
  51. {log_foundry-0.10.2.dev10 → log_foundry-0.10.2.dev12}/src/log_foundry/sinks/stdout.py +0 -0
  52. {log_foundry-0.10.2.dev10 → log_foundry-0.10.2.dev12}/src/log_foundry/sinks/syslog.py +0 -0
  53. {log_foundry-0.10.2.dev10 → log_foundry-0.10.2.dev12}/src/log_foundry/sinks/transform.py +0 -0
  54. {log_foundry-0.10.2.dev10 → log_foundry-0.10.2.dev12}/src/log_foundry/sinks/util.py +0 -0
  55. {log_foundry-0.10.2.dev10 → log_foundry-0.10.2.dev12}/src/log_foundry/worker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: log-foundry
3
- Version: 0.10.2.dev10
3
+ Version: 0.10.2.dev12
4
4
  Summary: Generate logs for your console and JSON events for downstream consumption.
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -20,7 +20,7 @@ dependencies = [
20
20
  ]
21
21
 
22
22
  # Optional features. Install with: pip install log-foundry[aws]
23
- version = "0.10.2.dev10"
23
+ version = "0.10.2.dev12"
24
24
 
25
25
  [project.optional-dependencies]
26
26
  aws = ["boto3>=1.43.61"] # SQSSink, SNSSink, KinesisSink, FirehoseSink
@@ -0,0 +1,212 @@
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 were lost. The caller's counter has already moved by this much.
98
+ detail: Optional circumstances — the sink's class name, the attempt count, an ``errno``,
99
+ an exception *type*. Escaped and truncated to ``_MAX_DETAIL``.
100
+ """
101
+ try:
102
+ sys.stderr.write(f"log-foundry: lost {count} {_escape(what)}(s){_suffix(detail)}\n")
103
+ except Exception:
104
+ pass # best-effort, as above; the counter this line describes is already recorded.
105
+
106
+
107
+ def rejected(reason: str, value: object) -> None:
108
+ """Report an inbound trace context the library refused to adopt (SPEC-014).
109
+
110
+ The offending value is echoed as a **bounded ``repr``** — the one place in the library where a
111
+ ``repr`` is correct, because the input is an inbound *header* rather than an exception, and its
112
+ exact shape is what makes a rejection diagnosable. Unbounded it would be a log-injection
113
+ surface: the value is attacker-controllable.
114
+
115
+ The ``repr`` is escaped afterwards even though ``repr`` of a ``str`` is already printable
116
+ throughout, which makes this a no-op for every value the call sites actually pass. It is not a
117
+ no-op in general: ``repr`` runs ``__repr__``, which is user code free to *return* a raw newline
118
+ no matter what the built-in reprs would have done. Escaping the result is the difference
119
+ between "the built-ins happen to escape" and "this cannot forge a line".
120
+
121
+ Never raises on a stream fault; a ``BaseException`` from the write still propagates.
122
+
123
+ Args:
124
+ reason: Why it was refused — ``"unparseable traceparent"``, ``"invalid trace_id"``. A
125
+ literal, never a runtime value: unlike the echo, it is not bounded.
126
+ value: The refused value, echoed as a ``repr`` bounded to ``_MAX_REJECTED_ECHO``.
127
+ """
128
+ try:
129
+ shown = _escape(repr(value))
130
+ if len(shown) > _MAX_REJECTED_ECHO:
131
+ shown = shown[:_MAX_REJECTED_ECHO] + "…"
132
+ sys.stderr.write(
133
+ f"log-foundry: ignoring inbound trace context ({_escape(reason)}): {shown}\n"
134
+ )
135
+ except Exception:
136
+ pass # best-effort, as above.
137
+
138
+
139
+ def errno_of(exc: BaseException) -> str:
140
+ """Return ``"errno=N"`` for an exception carrying one, else ``""``.
141
+
142
+ The library-controlled detail that makes an ``OSError`` line actionable: "connection refused"
143
+ and "host unknown" are the same type name and are told apart only by the code. An integer from
144
+ the OS is not caller data, which is what makes it safe to write where the message is not.
145
+
146
+ ``urllib``'s ``URLError`` carries the underlying socket error on ``.reason`` rather than
147
+ setting its own ``errno``, so that is consulted too. Total: attribute access on an arbitrary
148
+ exception can run a property that raises, and a helper for a diagnostic must not become one.
149
+
150
+ The value is rendered through ``int()`` rather than interpolated as found. ``isinstance(x,
151
+ int)`` admits any *subclass*, and a driver's error code is routinely one — an ``IntEnum`` or a
152
+ bespoke class whose ``__str__`` returns whatever its author chose, which the f-string would
153
+ then have written verbatim. That is the leak this helper exists to be the alternative to.
154
+ """
155
+ try:
156
+ code = getattr(exc, "errno", None)
157
+ if code is None:
158
+ code = getattr(getattr(exc, "reason", None), "errno", None)
159
+ return f"errno={int(code)}" if isinstance(code, int) else ""
160
+ except Exception:
161
+ return ""
162
+
163
+
164
+ def _suffix(detail: str) -> str:
165
+ """Render a detail as a bounded, escaped ``"; ..."`` suffix, or ``""`` when there is none.
166
+
167
+ Escaping precedes truncation so the bound applies to what is written, and so truncation can
168
+ only ever remove characters from an already-safe string rather than split an escape into
169
+ something that isn't one.
170
+
171
+ Guarded in its own right, and returning ``""`` rather than propagating: this runs inside the
172
+ caller's f-string, so a detail that cannot be rendered would otherwise take the *whole* line
173
+ with it — including the count, which is the part an operator cannot reconstruct.
174
+ """
175
+ if not detail:
176
+ return ""
177
+ try:
178
+ shown = _escape(detail)
179
+ if len(shown) > _MAX_DETAIL:
180
+ shown = shown[:_MAX_DETAIL] + "…"
181
+ return f"; {shown}"
182
+ except Exception:
183
+ return ""
184
+
185
+
186
+ def _escape(text: str) -> str:
187
+ """Render every non-printable character visibly, so nothing in ``text`` can forge a line.
188
+
189
+ ``str.isprintable()`` is the test rather than a table over ``range(0x20)``: it is ``False`` for
190
+ the C0 block *and* for DEL, the C1 block (U+009B is CSI, which a terminal reads as the start of
191
+ an escape sequence), U+0085, U+2028, U+2029, and the bidi format characters. Python's own
192
+ ``splitlines()`` breaks on three of those a C0 table misses, so a log shipper or a reader doing
193
+ the obvious thing would see a forged ``log-foundry:`` line that a newline count says is not
194
+ there. Space is printable, so ordinary text is untouched.
195
+
196
+ The whole-string fast path keeps the overwhelmingly common case — a sink name, an attempt
197
+ count, an exception type — to a single C-level call.
198
+ """
199
+ if text.isprintable():
200
+ return text
201
+ return "".join(char if char.isprintable() else _escaped(char) for char in text)
202
+
203
+
204
+ def _escaped(char: str) -> str:
205
+ """Escape one non-printable character, following Python's own ``repr`` conventions."""
206
+ escape = _ESCAPES.get(char)
207
+ if escape is not None:
208
+ return escape
209
+ code = ord(char)
210
+ if code <= 0xFF:
211
+ return f"\\x{code:02x}"
212
+ return f"\\u{code:04x}" if code <= 0xFFFF else f"\\U{code:08x}"
@@ -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