log-foundry 0.10.2.dev45__tar.gz → 0.10.2.dev47__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 (57) hide show
  1. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/PKG-INFO +10 -2
  2. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/README.md +9 -1
  3. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/pyproject.toml +1 -1
  4. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/src/log_foundry/api.py +20 -8
  5. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/src/log_foundry/sanitize.py +58 -2
  6. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/LICENSE +0 -0
  7. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/src/log_foundry/__init__.py +0 -0
  8. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/src/log_foundry/_diag.py +0 -0
  9. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/src/log_foundry/_lifecycle.py +0 -0
  10. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/src/log_foundry/config.py +0 -0
  11. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/src/log_foundry/console.py +0 -0
  12. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/src/log_foundry/context.py +0 -0
  13. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/src/log_foundry/decorator.py +0 -0
  14. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/src/log_foundry/ids.py +0 -0
  15. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/src/log_foundry/model.py +0 -0
  16. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/src/log_foundry/py.typed +0 -0
  17. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/src/log_foundry/results.py +0 -0
  18. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/src/log_foundry/sinks/__init__.py +0 -0
  19. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/src/log_foundry/sinks/_batch.py +0 -0
  20. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/src/log_foundry/sinks/_chunk.py +0 -0
  21. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/src/log_foundry/sinks/_retry.py +0 -0
  22. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/src/log_foundry/sinks/_socket.py +0 -0
  23. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/src/log_foundry/sinks/_time.py +0 -0
  24. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/src/log_foundry/sinks/base.py +0 -0
  25. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/src/log_foundry/sinks/callback.py +0 -0
  26. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/src/log_foundry/sinks/clickhouse.py +0 -0
  27. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/src/log_foundry/sinks/datadog.py +0 -0
  28. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/src/log_foundry/sinks/elasticsearch.py +0 -0
  29. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/src/log_foundry/sinks/eventhubs.py +0 -0
  30. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/src/log_foundry/sinks/file.py +0 -0
  31. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/src/log_foundry/sinks/filtering.py +0 -0
  32. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/src/log_foundry/sinks/firehose.py +0 -0
  33. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/src/log_foundry/sinks/honeycomb.py +0 -0
  34. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/src/log_foundry/sinks/http.py +0 -0
  35. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/src/log_foundry/sinks/kafka.py +0 -0
  36. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/src/log_foundry/sinks/kinesis.py +0 -0
  37. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/src/log_foundry/sinks/logging_sink.py +0 -0
  38. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/src/log_foundry/sinks/logstash.py +0 -0
  39. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/src/log_foundry/sinks/loki.py +0 -0
  40. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/src/log_foundry/sinks/mongodb.py +0 -0
  41. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/src/log_foundry/sinks/multi.py +0 -0
  42. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/src/log_foundry/sinks/nats.py +0 -0
  43. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/src/log_foundry/sinks/newrelic.py +0 -0
  44. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/src/log_foundry/sinks/postgres.py +0 -0
  45. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/src/log_foundry/sinks/pubsub.py +0 -0
  46. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/src/log_foundry/sinks/rabbitmq.py +0 -0
  47. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/src/log_foundry/sinks/redis.py +0 -0
  48. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/src/log_foundry/sinks/sentry.py +0 -0
  49. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/src/log_foundry/sinks/sns.py +0 -0
  50. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/src/log_foundry/sinks/splunk.py +0 -0
  51. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/src/log_foundry/sinks/sqlite.py +0 -0
  52. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/src/log_foundry/sinks/sqs.py +0 -0
  53. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/src/log_foundry/sinks/stdout.py +0 -0
  54. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/src/log_foundry/sinks/syslog.py +0 -0
  55. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/src/log_foundry/sinks/transform.py +0 -0
  56. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/src/log_foundry/sinks/util.py +0 -0
  57. {log_foundry-0.10.2.dev45 → log_foundry-0.10.2.dev47}/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.dev45
3
+ Version: 0.10.2.dev47
4
4
  Summary: Generate logs for your console and JSON events for downstream consumption.
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -1127,7 +1127,15 @@ entries and `max_depth` levels. `datetime`, `UUID`, `Decimal`, `bytes` and frien
1127
1127
  strings; anything with no JSON form becomes `<unserializable: TypeName>` — the type name only,
1128
1128
  never a `repr`, so coercion can never leak a value the library was careful not to capture.
1129
1129
 
1130
- Integers are the one case worth knowing about. They are passed through unchanged an ID or an
1130
+ **Floats that are not finite are replaced.** `NaN`, `Infinity` and `-Infinity` are values Python
1131
+ produces readily — a division that underflowed, a ratio over an empty window — and `json.dumps`
1132
+ writes all three happily. RFC 8259 defines none of them, so a strict consumer (Fluent Bit, a
1133
+ Logstash `json` codec, Jackson behind Elasticsearch) rejects the **whole record**, with nothing on
1134
+ this side to see. Each becomes `<float: nan>`, `<float: inf>` or `<float: -inf>` — which one it
1135
+ was is kept, because that is the only information the field still carried. Ordinary floats,
1136
+ including `-0.0` and subnormals, pass through untouched.
1137
+
1138
+ Integers are the other case worth knowing about. They are passed through unchanged — an ID or an
1131
1139
  amount stays a number, at full precision — but an integer too long to *render* is replaced by
1132
1140
  `<int: ~N digits>`. CPython refuses to convert an integer past `sys.get_int_max_str_digits()`
1133
1141
  decimal digits (**4300** by default) and raises, and `json.dumps` inherits that refusal, so with
@@ -1091,7 +1091,15 @@ entries and `max_depth` levels. `datetime`, `UUID`, `Decimal`, `bytes` and frien
1091
1091
  strings; anything with no JSON form becomes `<unserializable: TypeName>` — the type name only,
1092
1092
  never a `repr`, so coercion can never leak a value the library was careful not to capture.
1093
1093
 
1094
- Integers are the one case worth knowing about. They are passed through unchanged an ID or an
1094
+ **Floats that are not finite are replaced.** `NaN`, `Infinity` and `-Infinity` are values Python
1095
+ produces readily — a division that underflowed, a ratio over an empty window — and `json.dumps`
1096
+ writes all three happily. RFC 8259 defines none of them, so a strict consumer (Fluent Bit, a
1097
+ Logstash `json` codec, Jackson behind Elasticsearch) rejects the **whole record**, with nothing on
1098
+ this side to see. Each becomes `<float: nan>`, `<float: inf>` or `<float: -inf>` — which one it
1099
+ was is kept, because that is the only information the field still carried. Ordinary floats,
1100
+ including `-0.0` and subnormals, pass through untouched.
1101
+
1102
+ Integers are the other case worth knowing about. They are passed through unchanged — an ID or an
1095
1103
  amount stays a number, at full precision — but an integer too long to *render* is replaced by
1096
1104
  `<int: ~N digits>`. CPython refuses to convert an integer past `sys.get_int_max_str_digits()`
1097
1105
  decimal digits (**4300** by default) and raises, and `json.dumps` inherits that refusal, so with
@@ -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.dev45"
23
+ version = "0.10.2.dev47"
24
24
 
25
25
  [project.optional-dependencies]
26
26
  aws = ["boto3>=1.43.61"] # SQSSink, SNSSink, KinesisSink, FirehoseSink
@@ -57,12 +57,21 @@ def _log(level: str, message: str, echo: bool, fields: dict[str, object]) -> Non
57
57
  sink that was written to. ``_ensure_sink`` is resolved first, so a sink that fails to
58
58
  *construct* arms nothing: there is nothing to close.
59
59
 
60
- The orphan branch is the one that reaches the sink on the caller's own thread, with no
61
- worker between them to absorb a failure, so the whole branch is guarded (SPEC-025
62
- FR-003) — ``_ensure_sink`` constructs the sink on first use, so a sink that fails to
63
- build raises here too. The in-span branch is deliberately left untouched, since it only
64
- appends to a list. The echo runs after the emit, so a closed or redirected stream never
65
- costs the event itself.
60
+ **Both branches are guarded.** The orphan branch reaches the sink on the caller's own
61
+ thread, with no worker between them to absorb a failure (SPEC-025 FR-003) — ``_ensure_sink``
62
+ constructs the sink on first use, so a sink that fails to build raises here too.
63
+
64
+ ~~The in-span branch is deliberately left untouched, since it only appends to a list.~~
65
+ struck (SPEC-021), and it is why that branch shipped unguarded: it does not *only* append.
66
+ It calls ``build_event``, which calls ``truncate_str``, which calls ``value.encode`` — so
67
+ ``info(some_exception)``, a slip ``mypy`` catches only at typed call sites, returned normally
68
+ on the orphan path and killed the caller's function inside a span. Worse than a crash: the
69
+ decorator's own handler then recorded the span ``status=error`` with an ``error.type`` of
70
+ ``AttributeError`` that the caller's code never raised — wrong data *and* a broken caller.
71
+ A guard that reads "this cannot fail" is a guard that stops being true when the code under
72
+ it changes, and this one had already stopped.
73
+
74
+ The echo runs after the emit, so a closed or redirected stream never costs the event itself.
66
75
 
67
76
  Args:
68
77
  level: The severity label to stamp on the event.
@@ -81,8 +90,11 @@ def _log(level: str, message: str, echo: bool, fields: dict[str, object]) -> Non
81
90
  span = context.current_span()
82
91
  event: dict[str, object] | None = None
83
92
  if span is not None:
84
- event = build_event(span, level, message, fields=fields, baggage=baggage)
85
- span.events.append(event)
93
+ try:
94
+ event = build_event(span, level, message, fields=fields, baggage=baggage)
95
+ span.events.append(event)
96
+ except Exception as exc:
97
+ _diag.absorbed("building an in-span log", exc, "the event was lost")
86
98
  else:
87
99
  try:
88
100
  orphan = Span(
@@ -7,6 +7,7 @@ from collections.abc import Iterable, Mapping, Sequence
7
7
  from datetime import date, datetime, time
8
8
  from decimal import Decimal
9
9
  from enum import Enum
10
+ from math import isfinite
10
11
  from typing import TYPE_CHECKING, Any
11
12
  from uuid import UUID
12
13
 
@@ -25,8 +26,21 @@ _LOG10_2_NUM = 30103
25
26
  _LOG10_2_DEN = 100000
26
27
 
27
28
  _INT_LT = int.__lt__
29
+ _FLOAT_REPR = float.__repr__
30
+ """``float.__repr__``, unbound, so a subclass cannot divert the placeholder's text.
28
31
 
29
- _PLAIN_SCALARS: frozenset[type] = frozenset({int, float, bool})
32
+ The f-string form ``f"<float: {value}>"`` calls ``format(value, "")``, which is the value's own
33
+ ``__str__`` — so a ``float`` subclass chose what the library printed. Measured: a subclass whose
34
+ ``__str__`` names its source emitted ``<float: inf from probe-7 (token=sk-live-…)>`` into the event
35
+ stream, and one returning two megabytes emitted all of it against a configured
36
+ ``max_value_bytes`` of 8192. Both are the disclosure arch §6 forbids and the ceiling this module
37
+ exists to apply, defeated by the one new rule that handed the decision back to the value.
38
+
39
+ The same reasoning as :data:`_INT_LT` beside it, and as ``_placeholder``'s refusal to use
40
+ ``repr()``: what the library writes about a value must be computed by the library.
41
+ """
42
+
43
+ _PLAIN_SCALARS: frozenset[type] = frozenset({int, bool})
30
44
 
31
45
  _TEXTLIKE: tuple[type, ...] = (str, bytes, bytearray, memoryview)
32
46
 
@@ -222,6 +236,8 @@ class _Coercer:
222
236
  return self.text(value) # type: ignore[arg-type]
223
237
  if kind is int:
224
238
  return self.integer(value) # type: ignore[arg-type]
239
+ if kind is float:
240
+ return self.real(value) # type: ignore[arg-type]
225
241
  if kind in _PLAIN_SCALARS:
226
242
  return value
227
243
  if kind is dict:
@@ -233,6 +249,8 @@ class _Coercer:
233
249
  member = value.value
234
250
  if type(member) is int:
235
251
  return self.integer(member)
252
+ if isinstance(member, float) and not isinstance(member, bool):
253
+ return self.real(member)
236
254
  if type(member) in _PLAIN_SCALARS or member is None:
237
255
  return member
238
256
  if isinstance(member, str):
@@ -243,7 +261,7 @@ class _Coercer:
243
261
  if isinstance(value, int):
244
262
  return self.integer(value)
245
263
  if isinstance(value, float):
246
- return value
264
+ return self.real(value)
247
265
  if isinstance(value, (datetime, date, time)):
248
266
  return self.text(value.isoformat())
249
267
  if isinstance(value, UUID):
@@ -331,6 +349,12 @@ class _Coercer:
331
349
  a placeholder, so one hostile key would take every sibling with it, unmarked. ``bool``
332
350
  is excluded because ``True`` must render as the key ``"True"``, not ``"1"``.
333
351
 
352
+ A float key goes through :meth:`real` for the same reason, and its ``text()`` wrap is
353
+ defence in depth rather than load-bearing: ``real`` computes its placeholder through
354
+ :data:`_FLOAT_REPR`, so it cannot hand back a long string. No test distinguishes the wrap
355
+ from its absence, which is recorded here rather than pinned by an assertion that cannot
356
+ fail.
357
+
334
358
  Args:
335
359
  key: The mapping key, of any type.
336
360
 
@@ -345,6 +369,9 @@ class _Coercer:
345
369
  if isinstance(key, int) and not isinstance(key, bool):
346
370
  rendered = self.integer(key)
347
371
  return self.text(rendered if isinstance(rendered, str) else str(rendered))
372
+ if isinstance(key, float):
373
+ replaced = self.real(key)
374
+ return self.text(replaced if isinstance(replaced, str) else str(replaced))
348
375
  return self.text(str(key))
349
376
 
350
377
  def integer(self, value: int) -> object:
@@ -379,6 +406,35 @@ class _Coercer:
379
406
  self.truncated = True
380
407
  return f"<int: ~{digits} digits>"
381
408
 
409
+ def real(self, value: float) -> object:
410
+ """Replaces a non-finite float, since ``NaN`` and ``Infinity`` are not JSON.
411
+
412
+ ``json.dumps`` writes ``NaN``, ``Infinity`` and ``-Infinity`` happily, and RFC 8259
413
+ defines none of them: a strict consumer — Fluent Bit, a Logstash ``json`` codec, Jackson
414
+ behind Elasticsearch — rejects the whole record, with nothing on the library side to see.
415
+ That contradicts ``build_event``'s promise that an event is safe for any sink to
416
+ serialize (SPEC-017).
417
+
418
+ Replaced rather than coerced to ``None`` or ``0.0``, on SPEC-020's reasoning for the
419
+ over-long integer this mirrors: a wrong number is worse than a visibly elided one, and
420
+ the marker says which of the three it was. ``truncated`` is set for the same reason it is
421
+ set everywhere else — a substitution nobody can see is a silent change to the data.
422
+
423
+ Args:
424
+ value: The float to check.
425
+
426
+ Returns:
427
+ The float itself when finite, or a ``<float: nan>`` / ``<float: inf>`` /
428
+ ``<float: -inf>`` placeholder naming which it was.
429
+
430
+ Raises:
431
+ None.
432
+ """
433
+ if isfinite(value):
434
+ return value
435
+ self.truncated = True
436
+ return f"<float: {_FLOAT_REPR(value)}>"
437
+
382
438
  def text(self, value: str) -> str:
383
439
  """Applies ``max_value_bytes`` to a string, recording whether it fired.
384
440