log-foundry 0.7.1.dev1__tar.gz → 0.7.1.dev2__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.7.1.dev1 → log_foundry-0.7.1.dev2}/PKG-INFO +19 -1
- {log_foundry-0.7.1.dev1 → log_foundry-0.7.1.dev2}/README.md +18 -0
- {log_foundry-0.7.1.dev1 → log_foundry-0.7.1.dev2}/pyproject.toml +1 -1
- {log_foundry-0.7.1.dev1 → log_foundry-0.7.1.dev2}/src/log_foundry/sanitize.py +66 -5
- {log_foundry-0.7.1.dev1 → log_foundry-0.7.1.dev2}/LICENSE +0 -0
- {log_foundry-0.7.1.dev1 → log_foundry-0.7.1.dev2}/src/log_foundry/__init__.py +0 -0
- {log_foundry-0.7.1.dev1 → log_foundry-0.7.1.dev2}/src/log_foundry/api.py +0 -0
- {log_foundry-0.7.1.dev1 → log_foundry-0.7.1.dev2}/src/log_foundry/config.py +0 -0
- {log_foundry-0.7.1.dev1 → log_foundry-0.7.1.dev2}/src/log_foundry/console.py +0 -0
- {log_foundry-0.7.1.dev1 → log_foundry-0.7.1.dev2}/src/log_foundry/context.py +0 -0
- {log_foundry-0.7.1.dev1 → log_foundry-0.7.1.dev2}/src/log_foundry/decorator.py +0 -0
- {log_foundry-0.7.1.dev1 → log_foundry-0.7.1.dev2}/src/log_foundry/ids.py +0 -0
- {log_foundry-0.7.1.dev1 → log_foundry-0.7.1.dev2}/src/log_foundry/model.py +0 -0
- {log_foundry-0.7.1.dev1 → log_foundry-0.7.1.dev2}/src/log_foundry/py.typed +0 -0
- {log_foundry-0.7.1.dev1 → log_foundry-0.7.1.dev2}/src/log_foundry/sinks/__init__.py +0 -0
- {log_foundry-0.7.1.dev1 → log_foundry-0.7.1.dev2}/src/log_foundry/sinks/_batch.py +0 -0
- {log_foundry-0.7.1.dev1 → log_foundry-0.7.1.dev2}/src/log_foundry/sinks/_chunk.py +0 -0
- {log_foundry-0.7.1.dev1 → log_foundry-0.7.1.dev2}/src/log_foundry/sinks/_socket.py +0 -0
- {log_foundry-0.7.1.dev1 → log_foundry-0.7.1.dev2}/src/log_foundry/sinks/_time.py +0 -0
- {log_foundry-0.7.1.dev1 → log_foundry-0.7.1.dev2}/src/log_foundry/sinks/base.py +0 -0
- {log_foundry-0.7.1.dev1 → log_foundry-0.7.1.dev2}/src/log_foundry/sinks/callback.py +0 -0
- {log_foundry-0.7.1.dev1 → log_foundry-0.7.1.dev2}/src/log_foundry/sinks/clickhouse.py +0 -0
- {log_foundry-0.7.1.dev1 → log_foundry-0.7.1.dev2}/src/log_foundry/sinks/datadog.py +0 -0
- {log_foundry-0.7.1.dev1 → log_foundry-0.7.1.dev2}/src/log_foundry/sinks/elasticsearch.py +0 -0
- {log_foundry-0.7.1.dev1 → log_foundry-0.7.1.dev2}/src/log_foundry/sinks/eventhubs.py +0 -0
- {log_foundry-0.7.1.dev1 → log_foundry-0.7.1.dev2}/src/log_foundry/sinks/file.py +0 -0
- {log_foundry-0.7.1.dev1 → log_foundry-0.7.1.dev2}/src/log_foundry/sinks/filtering.py +0 -0
- {log_foundry-0.7.1.dev1 → log_foundry-0.7.1.dev2}/src/log_foundry/sinks/firehose.py +0 -0
- {log_foundry-0.7.1.dev1 → log_foundry-0.7.1.dev2}/src/log_foundry/sinks/honeycomb.py +0 -0
- {log_foundry-0.7.1.dev1 → log_foundry-0.7.1.dev2}/src/log_foundry/sinks/http.py +0 -0
- {log_foundry-0.7.1.dev1 → log_foundry-0.7.1.dev2}/src/log_foundry/sinks/kafka.py +0 -0
- {log_foundry-0.7.1.dev1 → log_foundry-0.7.1.dev2}/src/log_foundry/sinks/kinesis.py +0 -0
- {log_foundry-0.7.1.dev1 → log_foundry-0.7.1.dev2}/src/log_foundry/sinks/logging_sink.py +0 -0
- {log_foundry-0.7.1.dev1 → log_foundry-0.7.1.dev2}/src/log_foundry/sinks/logstash.py +0 -0
- {log_foundry-0.7.1.dev1 → log_foundry-0.7.1.dev2}/src/log_foundry/sinks/loki.py +0 -0
- {log_foundry-0.7.1.dev1 → log_foundry-0.7.1.dev2}/src/log_foundry/sinks/mongodb.py +0 -0
- {log_foundry-0.7.1.dev1 → log_foundry-0.7.1.dev2}/src/log_foundry/sinks/multi.py +0 -0
- {log_foundry-0.7.1.dev1 → log_foundry-0.7.1.dev2}/src/log_foundry/sinks/nats.py +0 -0
- {log_foundry-0.7.1.dev1 → log_foundry-0.7.1.dev2}/src/log_foundry/sinks/newrelic.py +0 -0
- {log_foundry-0.7.1.dev1 → log_foundry-0.7.1.dev2}/src/log_foundry/sinks/postgres.py +0 -0
- {log_foundry-0.7.1.dev1 → log_foundry-0.7.1.dev2}/src/log_foundry/sinks/pubsub.py +0 -0
- {log_foundry-0.7.1.dev1 → log_foundry-0.7.1.dev2}/src/log_foundry/sinks/rabbitmq.py +0 -0
- {log_foundry-0.7.1.dev1 → log_foundry-0.7.1.dev2}/src/log_foundry/sinks/redis.py +0 -0
- {log_foundry-0.7.1.dev1 → log_foundry-0.7.1.dev2}/src/log_foundry/sinks/sentry.py +0 -0
- {log_foundry-0.7.1.dev1 → log_foundry-0.7.1.dev2}/src/log_foundry/sinks/sns.py +0 -0
- {log_foundry-0.7.1.dev1 → log_foundry-0.7.1.dev2}/src/log_foundry/sinks/splunk.py +0 -0
- {log_foundry-0.7.1.dev1 → log_foundry-0.7.1.dev2}/src/log_foundry/sinks/sqlite.py +0 -0
- {log_foundry-0.7.1.dev1 → log_foundry-0.7.1.dev2}/src/log_foundry/sinks/sqs.py +0 -0
- {log_foundry-0.7.1.dev1 → log_foundry-0.7.1.dev2}/src/log_foundry/sinks/stdout.py +0 -0
- {log_foundry-0.7.1.dev1 → log_foundry-0.7.1.dev2}/src/log_foundry/sinks/syslog.py +0 -0
- {log_foundry-0.7.1.dev1 → log_foundry-0.7.1.dev2}/src/log_foundry/sinks/transform.py +0 -0
- {log_foundry-0.7.1.dev1 → log_foundry-0.7.1.dev2}/src/log_foundry/sinks/util.py +0 -0
- {log_foundry-0.7.1.dev1 → log_foundry-0.7.1.dev2}/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.7.1.
|
|
3
|
+
Version: 0.7.1.dev2
|
|
4
4
|
Summary: Generate logs for your console and JSON events for downstream consumption.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -743,10 +743,28 @@ Every event is the same shape (arch §6). Boundary events add a few fields:
|
|
|
743
743
|
| `duration_ms` | span.end | wall time from a monotonic delta |
|
|
744
744
|
| `status` | span.end | `"ok"` or `"error"` |
|
|
745
745
|
| `error` | on failure | `{"type": ..., "stack": ...}` |
|
|
746
|
+
| `truncated` | when a ceiling fired | `true`; absent otherwise, never `false` |
|
|
746
747
|
|
|
747
748
|
IDs are [W3C Trace Context](https://www.w3.org/TR/trace-context/)-compatible by design, so the
|
|
748
749
|
logs can later correlate with distributed traces cheaply.
|
|
749
750
|
|
|
751
|
+
### Field values are coerced and bounded
|
|
752
|
+
|
|
753
|
+
Every value you pass is made JSON-safe and given a size ceiling once, when the event is assembled
|
|
754
|
+
— so no sink can be handed a payload JSON refuses, and no single field can grow without limit.
|
|
755
|
+
Strings are clipped to `max_value_bytes` (8192 by default), mappings and sequences to `max_keys`
|
|
756
|
+
entries and `max_depth` levels. `datetime`, `UUID`, `Decimal`, `bytes` and friends render as
|
|
757
|
+
strings; anything with no JSON form becomes `<unserializable: TypeName>` — the type name only,
|
|
758
|
+
never a `repr`, so coercion can never leak a value the library was careful not to capture.
|
|
759
|
+
|
|
760
|
+
Integers are the one case worth knowing about. They are passed through unchanged — an ID or an
|
|
761
|
+
amount stays a number, at full precision — but an integer too long to *render* is replaced by
|
|
762
|
+
`<int: ~N digits>`. CPython refuses to convert an integer past `sys.get_int_max_str_digits()`
|
|
763
|
+
decimal digits (**4300** by default) and raises, and `json.dumps` inherits that refusal, so with
|
|
764
|
+
the default configuration the interpreter's limit — not `max_value_bytes` — is what binds. You are
|
|
765
|
+
unlikely to meet it deliberately; `int.from_bytes(blob, "big")` over a couple of kilobytes gets
|
|
766
|
+
there. Any ceiling firing sets `truncated: true` on the event.
|
|
767
|
+
|
|
750
768
|
## Development
|
|
751
769
|
|
|
752
770
|
```bash
|
|
@@ -707,10 +707,28 @@ Every event is the same shape (arch §6). Boundary events add a few fields:
|
|
|
707
707
|
| `duration_ms` | span.end | wall time from a monotonic delta |
|
|
708
708
|
| `status` | span.end | `"ok"` or `"error"` |
|
|
709
709
|
| `error` | on failure | `{"type": ..., "stack": ...}` |
|
|
710
|
+
| `truncated` | when a ceiling fired | `true`; absent otherwise, never `false` |
|
|
710
711
|
|
|
711
712
|
IDs are [W3C Trace Context](https://www.w3.org/TR/trace-context/)-compatible by design, so the
|
|
712
713
|
logs can later correlate with distributed traces cheaply.
|
|
713
714
|
|
|
715
|
+
### Field values are coerced and bounded
|
|
716
|
+
|
|
717
|
+
Every value you pass is made JSON-safe and given a size ceiling once, when the event is assembled
|
|
718
|
+
— so no sink can be handed a payload JSON refuses, and no single field can grow without limit.
|
|
719
|
+
Strings are clipped to `max_value_bytes` (8192 by default), mappings and sequences to `max_keys`
|
|
720
|
+
entries and `max_depth` levels. `datetime`, `UUID`, `Decimal`, `bytes` and friends render as
|
|
721
|
+
strings; anything with no JSON form becomes `<unserializable: TypeName>` — the type name only,
|
|
722
|
+
never a `repr`, so coercion can never leak a value the library was careful not to capture.
|
|
723
|
+
|
|
724
|
+
Integers are the one case worth knowing about. They are passed through unchanged — an ID or an
|
|
725
|
+
amount stays a number, at full precision — but an integer too long to *render* is replaced by
|
|
726
|
+
`<int: ~N digits>`. CPython refuses to convert an integer past `sys.get_int_max_str_digits()`
|
|
727
|
+
decimal digits (**4300** by default) and raises, and `json.dumps` inherits that refusal, so with
|
|
728
|
+
the default configuration the interpreter's limit — not `max_value_bytes` — is what binds. You are
|
|
729
|
+
unlikely to meet it deliberately; `int.from_bytes(blob, "big")` over a couple of kilobytes gets
|
|
730
|
+
there. Any ceiling firing sets `truncated: true` on the event.
|
|
731
|
+
|
|
714
732
|
## Development
|
|
715
733
|
|
|
716
734
|
```bash
|
|
@@ -27,6 +27,7 @@ dropped without disclosing it.
|
|
|
27
27
|
|
|
28
28
|
from __future__ import annotations
|
|
29
29
|
|
|
30
|
+
import sys
|
|
30
31
|
from collections.abc import Iterable, Mapping, Sequence
|
|
31
32
|
from datetime import date, datetime, time
|
|
32
33
|
from decimal import Decimal
|
|
@@ -47,6 +48,13 @@ _MARKER_BYTES = len(TRUNCATION_MARKER.encode("utf-8"))
|
|
|
47
48
|
_CIRCULAR = "<circular>"
|
|
48
49
|
_DEPTH_LIMIT = "<depth limit>"
|
|
49
50
|
|
|
51
|
+
# ``log10(2)`` as an integer ratio, rounded *up*: ``|n| < 2**b``, so ``n`` has at most
|
|
52
|
+
# ``b * _LOG10_2_NUM // _LOG10_2_DEN + 1`` decimal digits. Rounding the ratio up makes that an
|
|
53
|
+
# over-estimate, which is the safe direction — it can replace an integer marginally short of the
|
|
54
|
+
# ceiling, but never admit one past it (SPEC-020 FR-002).
|
|
55
|
+
_LOG10_2_NUM = 30103
|
|
56
|
+
_LOG10_2_DEN = 100000
|
|
57
|
+
|
|
50
58
|
# Exact-type membership, deliberately not ``isinstance``. ``IntEnum``/``StrEnum`` members *are*
|
|
51
59
|
# ``int``/``str`` instances, so an isinstance check would pass the enum member itself through and
|
|
52
60
|
# hand a sink an ``Enum`` where a plain value was promised. Exact typing lets them fall to the
|
|
@@ -59,6 +67,18 @@ _PLAIN_SCALARS: frozenset[type] = frozenset({int, float, bool})
|
|
|
59
67
|
_TEXTLIKE: tuple[type, ...] = (str, bytes, bytearray, memoryview)
|
|
60
68
|
|
|
61
69
|
|
|
70
|
+
def _int_digit_ceiling(max_value_bytes: int) -> int:
|
|
71
|
+
"""The largest decimal length an integer may have and still be rendered (SPEC-020 FR-001).
|
|
72
|
+
|
|
73
|
+
CPython 3.11+ refuses to convert an integer past ``sys.get_int_max_str_digits()`` digits and
|
|
74
|
+
raises ``ValueError``, which ``json.dumps`` inherits. A configured ceiling above that cannot be
|
|
75
|
+
honoured — rendering such an integer is the very thing that raises — so the interpreter's limit
|
|
76
|
+
wins whenever it is lower. A limit of ``0`` means the interpreter imposes none.
|
|
77
|
+
"""
|
|
78
|
+
limit = sys.get_int_max_str_digits()
|
|
79
|
+
return max_value_bytes if limit <= 0 else min(max_value_bytes, limit)
|
|
80
|
+
|
|
81
|
+
|
|
62
82
|
def _measured(value: str) -> bytes:
|
|
63
83
|
"""UTF-8 bytes of ``value``, tolerating lone surrogates.
|
|
64
84
|
|
|
@@ -152,7 +172,9 @@ class _Coercer:
|
|
|
152
172
|
kind = type(value)
|
|
153
173
|
if kind is str:
|
|
154
174
|
return self.text(value) # type: ignore[arg-type]
|
|
155
|
-
if kind
|
|
175
|
+
if kind is int: # before _PLAIN_SCALARS: `int` is the one scalar with no natural ceiling.
|
|
176
|
+
return self.integer(value) # type: ignore[arg-type]
|
|
177
|
+
if kind in _PLAIN_SCALARS: # float (IEEE-754-bounded) and bool
|
|
156
178
|
return value
|
|
157
179
|
if kind is dict:
|
|
158
180
|
return self.mapping(value, depth) # type: ignore[arg-type]
|
|
@@ -161,6 +183,8 @@ class _Coercer:
|
|
|
161
183
|
|
|
162
184
|
if isinstance(value, Enum):
|
|
163
185
|
member = value.value
|
|
186
|
+
if type(member) is int: # an IntEnum member is as unbounded as a bare int.
|
|
187
|
+
return self.integer(member)
|
|
164
188
|
if type(member) in _PLAIN_SCALARS or member is None:
|
|
165
189
|
return member
|
|
166
190
|
if isinstance(member, str):
|
|
@@ -170,7 +194,9 @@ class _Coercer:
|
|
|
170
194
|
return self.text(str(value))
|
|
171
195
|
if isinstance(value, bool): # bool before int: it is an int subclass.
|
|
172
196
|
return value
|
|
173
|
-
if isinstance(value,
|
|
197
|
+
if isinstance(value, int): # int *subclasses* — bounded like the exact type above.
|
|
198
|
+
return self.integer(value)
|
|
199
|
+
if isinstance(value, float): # float *subclasses*.
|
|
174
200
|
return value
|
|
175
201
|
if isinstance(value, (datetime, date, time)):
|
|
176
202
|
return self.text(value.isoformat())
|
|
@@ -231,9 +257,44 @@ class _Coercer:
|
|
|
231
257
|
self._parents.pop()
|
|
232
258
|
|
|
233
259
|
def key(self, key: object) -> str:
|
|
234
|
-
"""Coerce a mapping key to a bounded ``str`` — JSON object keys are always strings.
|
|
235
|
-
|
|
236
|
-
|
|
260
|
+
"""Coerce a mapping key to a bounded ``str`` — JSON object keys are always strings.
|
|
261
|
+
|
|
262
|
+
An integer key goes through :meth:`integer` first. A bare ``str()`` here would raise on an
|
|
263
|
+
over-long one — the very ``ValueError`` this module exists to keep away from a sink — and
|
|
264
|
+
the failure would be caught up in :meth:`value`, replacing the *whole mapping* with a
|
|
265
|
+
placeholder. One hostile key would take every sibling key with it, unmarked. ``bool`` is
|
|
266
|
+
excluded because ``True`` must render as the key ``"True"``, not ``"1"``.
|
|
267
|
+
"""
|
|
268
|
+
if isinstance(key, str):
|
|
269
|
+
return self.text(key)
|
|
270
|
+
if isinstance(key, int) and not isinstance(key, bool):
|
|
271
|
+
rendered = self.integer(key)
|
|
272
|
+
return self.text(rendered if isinstance(rendered, str) else str(rendered))
|
|
273
|
+
return self.text(str(key))
|
|
274
|
+
|
|
275
|
+
def integer(self, value: int) -> object:
|
|
276
|
+
"""Return ``value`` unchanged, or a placeholder when it is too long to render (FR-001).
|
|
277
|
+
|
|
278
|
+
The size test is ``bit_length()``, never ``len(str(value))``: converting an over-long
|
|
279
|
+
integer to a string raises the very ``ValueError`` this bound exists to prevent, so the
|
|
280
|
+
obvious check would move the crash rather than remove it (FR-002). ``bit_length()`` is
|
|
281
|
+
O(1), total, and ignores the sign, so ``n`` and ``-n`` are bounded identically.
|
|
282
|
+
|
|
283
|
+
An over-long integer is *replaced*, not clipped. Dropping digits would silently change the
|
|
284
|
+
value, and a wrong number is worse than a visibly elided one — so this reuses the
|
|
285
|
+
type-naming shape of :meth:`_placeholder` rather than inventing a second elision style.
|
|
286
|
+
|
|
287
|
+
The bound trusts ``bit_length()``. An ``int`` subclass that overrides it to understate its
|
|
288
|
+
own magnitude defeats this, and nothing here can tell — the call does not raise, so the
|
|
289
|
+
totality guard in :meth:`value` never engages either. That is the same trust every coercion
|
|
290
|
+
rule extends to a subclass's dunders, and narrowing it would cost the common path a
|
|
291
|
+
``type()`` check to catch only a value engineered to lie about itself.
|
|
292
|
+
"""
|
|
293
|
+
digits = value.bit_length() * _LOG10_2_NUM // _LOG10_2_DEN + 1
|
|
294
|
+
if digits <= _int_digit_ceiling(self._cfg.max_value_bytes):
|
|
295
|
+
return value
|
|
296
|
+
self.truncated = True
|
|
297
|
+
return f"<int: ~{digits} digits>"
|
|
237
298
|
|
|
238
299
|
def text(self, value: str) -> str:
|
|
239
300
|
"""Apply ``max_value_bytes``, recording whether it fired."""
|
|
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
|
|
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
|