log-foundry 0.5.1.dev6__tar.gz → 0.5.1.dev7__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 (52) hide show
  1. {log_foundry-0.5.1.dev6 → log_foundry-0.5.1.dev7}/PKG-INFO +1 -1
  2. {log_foundry-0.5.1.dev6 → log_foundry-0.5.1.dev7}/pyproject.toml +1 -1
  3. {log_foundry-0.5.1.dev6 → log_foundry-0.5.1.dev7}/src/log_foundry/model.py +95 -9
  4. {log_foundry-0.5.1.dev6 → log_foundry-0.5.1.dev7}/LICENSE +0 -0
  5. {log_foundry-0.5.1.dev6 → log_foundry-0.5.1.dev7}/README.md +0 -0
  6. {log_foundry-0.5.1.dev6 → log_foundry-0.5.1.dev7}/src/log_foundry/__init__.py +0 -0
  7. {log_foundry-0.5.1.dev6 → log_foundry-0.5.1.dev7}/src/log_foundry/api.py +0 -0
  8. {log_foundry-0.5.1.dev6 → log_foundry-0.5.1.dev7}/src/log_foundry/config.py +0 -0
  9. {log_foundry-0.5.1.dev6 → log_foundry-0.5.1.dev7}/src/log_foundry/console.py +0 -0
  10. {log_foundry-0.5.1.dev6 → log_foundry-0.5.1.dev7}/src/log_foundry/context.py +0 -0
  11. {log_foundry-0.5.1.dev6 → log_foundry-0.5.1.dev7}/src/log_foundry/decorator.py +0 -0
  12. {log_foundry-0.5.1.dev6 → log_foundry-0.5.1.dev7}/src/log_foundry/ids.py +0 -0
  13. {log_foundry-0.5.1.dev6 → log_foundry-0.5.1.dev7}/src/log_foundry/py.typed +0 -0
  14. {log_foundry-0.5.1.dev6 → log_foundry-0.5.1.dev7}/src/log_foundry/sanitize.py +0 -0
  15. {log_foundry-0.5.1.dev6 → log_foundry-0.5.1.dev7}/src/log_foundry/sinks/__init__.py +0 -0
  16. {log_foundry-0.5.1.dev6 → log_foundry-0.5.1.dev7}/src/log_foundry/sinks/_chunk.py +0 -0
  17. {log_foundry-0.5.1.dev6 → log_foundry-0.5.1.dev7}/src/log_foundry/sinks/_socket.py +0 -0
  18. {log_foundry-0.5.1.dev6 → log_foundry-0.5.1.dev7}/src/log_foundry/sinks/_time.py +0 -0
  19. {log_foundry-0.5.1.dev6 → log_foundry-0.5.1.dev7}/src/log_foundry/sinks/base.py +0 -0
  20. {log_foundry-0.5.1.dev6 → log_foundry-0.5.1.dev7}/src/log_foundry/sinks/callback.py +0 -0
  21. {log_foundry-0.5.1.dev6 → log_foundry-0.5.1.dev7}/src/log_foundry/sinks/clickhouse.py +0 -0
  22. {log_foundry-0.5.1.dev6 → log_foundry-0.5.1.dev7}/src/log_foundry/sinks/datadog.py +0 -0
  23. {log_foundry-0.5.1.dev6 → log_foundry-0.5.1.dev7}/src/log_foundry/sinks/elasticsearch.py +0 -0
  24. {log_foundry-0.5.1.dev6 → log_foundry-0.5.1.dev7}/src/log_foundry/sinks/eventhubs.py +0 -0
  25. {log_foundry-0.5.1.dev6 → log_foundry-0.5.1.dev7}/src/log_foundry/sinks/file.py +0 -0
  26. {log_foundry-0.5.1.dev6 → log_foundry-0.5.1.dev7}/src/log_foundry/sinks/filtering.py +0 -0
  27. {log_foundry-0.5.1.dev6 → log_foundry-0.5.1.dev7}/src/log_foundry/sinks/firehose.py +0 -0
  28. {log_foundry-0.5.1.dev6 → log_foundry-0.5.1.dev7}/src/log_foundry/sinks/honeycomb.py +0 -0
  29. {log_foundry-0.5.1.dev6 → log_foundry-0.5.1.dev7}/src/log_foundry/sinks/http.py +0 -0
  30. {log_foundry-0.5.1.dev6 → log_foundry-0.5.1.dev7}/src/log_foundry/sinks/kafka.py +0 -0
  31. {log_foundry-0.5.1.dev6 → log_foundry-0.5.1.dev7}/src/log_foundry/sinks/kinesis.py +0 -0
  32. {log_foundry-0.5.1.dev6 → log_foundry-0.5.1.dev7}/src/log_foundry/sinks/logging_sink.py +0 -0
  33. {log_foundry-0.5.1.dev6 → log_foundry-0.5.1.dev7}/src/log_foundry/sinks/logstash.py +0 -0
  34. {log_foundry-0.5.1.dev6 → log_foundry-0.5.1.dev7}/src/log_foundry/sinks/loki.py +0 -0
  35. {log_foundry-0.5.1.dev6 → log_foundry-0.5.1.dev7}/src/log_foundry/sinks/mongodb.py +0 -0
  36. {log_foundry-0.5.1.dev6 → log_foundry-0.5.1.dev7}/src/log_foundry/sinks/multi.py +0 -0
  37. {log_foundry-0.5.1.dev6 → log_foundry-0.5.1.dev7}/src/log_foundry/sinks/nats.py +0 -0
  38. {log_foundry-0.5.1.dev6 → log_foundry-0.5.1.dev7}/src/log_foundry/sinks/newrelic.py +0 -0
  39. {log_foundry-0.5.1.dev6 → log_foundry-0.5.1.dev7}/src/log_foundry/sinks/postgres.py +0 -0
  40. {log_foundry-0.5.1.dev6 → log_foundry-0.5.1.dev7}/src/log_foundry/sinks/pubsub.py +0 -0
  41. {log_foundry-0.5.1.dev6 → log_foundry-0.5.1.dev7}/src/log_foundry/sinks/rabbitmq.py +0 -0
  42. {log_foundry-0.5.1.dev6 → log_foundry-0.5.1.dev7}/src/log_foundry/sinks/redis.py +0 -0
  43. {log_foundry-0.5.1.dev6 → log_foundry-0.5.1.dev7}/src/log_foundry/sinks/sentry.py +0 -0
  44. {log_foundry-0.5.1.dev6 → log_foundry-0.5.1.dev7}/src/log_foundry/sinks/sns.py +0 -0
  45. {log_foundry-0.5.1.dev6 → log_foundry-0.5.1.dev7}/src/log_foundry/sinks/splunk.py +0 -0
  46. {log_foundry-0.5.1.dev6 → log_foundry-0.5.1.dev7}/src/log_foundry/sinks/sqlite.py +0 -0
  47. {log_foundry-0.5.1.dev6 → log_foundry-0.5.1.dev7}/src/log_foundry/sinks/sqs.py +0 -0
  48. {log_foundry-0.5.1.dev6 → log_foundry-0.5.1.dev7}/src/log_foundry/sinks/stdout.py +0 -0
  49. {log_foundry-0.5.1.dev6 → log_foundry-0.5.1.dev7}/src/log_foundry/sinks/syslog.py +0 -0
  50. {log_foundry-0.5.1.dev6 → log_foundry-0.5.1.dev7}/src/log_foundry/sinks/transform.py +0 -0
  51. {log_foundry-0.5.1.dev6 → log_foundry-0.5.1.dev7}/src/log_foundry/sinks/util.py +0 -0
  52. {log_foundry-0.5.1.dev6 → log_foundry-0.5.1.dev7}/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.5.1.dev6
3
+ Version: 0.5.1.dev7
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.5.1.dev6"
23
+ version = "0.5.1.dev7"
24
24
 
25
25
  [project.optional-dependencies]
26
26
  aws = ["boto3>=1.34"] # SQSSink, SNSSink, KinesisSink, FirehoseSink
@@ -12,6 +12,12 @@ import time
12
12
  import traceback
13
13
  from dataclasses import dataclass, field
14
14
  from datetime import datetime, timezone
15
+ from typing import TYPE_CHECKING
16
+
17
+ from log_foundry.sanitize import sanitize_fields, truncate_str, truncate_tail
18
+
19
+ if TYPE_CHECKING:
20
+ from log_foundry.config import Config
15
21
 
16
22
  __all__ = ["Span", "build_event", "start_event", "end_event", "backfill_baggage"]
17
23
 
@@ -20,6 +26,11 @@ __all__ = ["Span", "build_event", "start_event", "end_event", "backfill_baggage"
20
26
  _START_MESSAGE = "span.start"
21
27
  _END_MESSAGE = "span.end"
22
28
 
29
+ # Marks an event that a ceiling clipped (SPEC-017 FR-002). Set **only ever to True**, never to
30
+ # False — that single invariant is what makes "OR with whatever an earlier stage set" and
31
+ # "absent, not false, on a clean event" both fall out with no read-modify-write anywhere.
32
+ _TRUNCATED = "truncated"
33
+
23
34
 
24
35
  @dataclass
25
36
  class Span:
@@ -57,16 +68,22 @@ def build_event(
57
68
 
58
69
  Field precedence, lowest to highest: config ``defaults`` → ``span.defaults`` → ``baggage``
59
70
  → per-call ``fields`` (arch §5.1). Later sources win on a key conflict.
71
+
72
+ The merged mapping is coerced and size-bounded here (SPEC-017 FR-001/FR-002), which is what
73
+ makes every sink's bare ``json.dumps`` safe without any of them changing. ``message`` is
74
+ bounded too: it is a base field, but unlike the other eleven it is caller-supplied free text.
60
75
  """
61
76
  from log_foundry.config import get_config
62
77
  from log_foundry.ids import new_log_id
63
78
 
64
79
  cfg = get_config()
65
80
  merged: dict[str, object] = {**cfg.defaults, **span.defaults, **baggage, **fields}
66
- return {
81
+ safe, clipped = sanitize_fields(merged, cfg=cfg)
82
+ bounded_message, message_clipped = truncate_str(message, cfg.max_value_bytes)
83
+ event: dict[str, object] = {
67
84
  "timestamp": _iso_now(),
68
85
  "level": level,
69
- "message": message,
86
+ "message": bounded_message,
70
87
  "trace_id": span.trace_id,
71
88
  "span_id": span.span_id,
72
89
  "parent_span_id": span.parent_span_id,
@@ -75,8 +92,12 @@ def build_event(
75
92
  "service": cfg.service,
76
93
  "version": cfg.version,
77
94
  "env": cfg.env,
78
- "fields": merged,
79
95
  }
96
+ # Before ``fields`` so it reads ahead of the payload blob in a rendered log line.
97
+ if clipped or message_clipped:
98
+ event[_TRUNCATED] = True
99
+ event["fields"] = safe
100
+ return event
80
101
 
81
102
 
82
103
  def start_event(span: Span) -> dict[str, object]:
@@ -84,6 +105,57 @@ def start_event(span: Span) -> dict[str, object]:
84
105
  return build_event(span, "INFO", _START_MESSAGE, fields={}, baggage={})
85
106
 
86
107
 
108
+ def _exception_message(exc: BaseException) -> str:
109
+ """``str(exc)`` — ``""`` for an exception raised with no arguments (SPEC-017 FR-003).
110
+
111
+ Guarded because a user exception may define a ``__str__`` that itself raises. This runs
112
+ inside the decorator's ``except`` block, so an exception escaping here would *replace* the
113
+ one the user's code raised, demoting theirs to ``__context__`` — logging breaking the app,
114
+ which is the thing this spec exists to stop.
115
+ """
116
+ try:
117
+ return str(exc)
118
+ except Exception: # noqa: BLE001 — see above; a hostile __str__ must not escape.
119
+ return "<unprintable message>"
120
+
121
+
122
+ def _exception_stack(exc: BaseException) -> str:
123
+ """The formatted traceback.
124
+
125
+ ``traceback.format_exception`` already renders a failing ``__str__`` as
126
+ ``<exception str() failed>`` rather than propagating, so this guard is for the rarer case of
127
+ a frame that cannot be rendered at all.
128
+ """
129
+ try:
130
+ return "".join(traceback.format_exception(type(exc), exc, exc.__traceback__))
131
+ except Exception: # noqa: BLE001 — same reasoning as _exception_message.
132
+ return f"<unformattable traceback: {type(exc).__name__}>"
133
+
134
+
135
+ def _error_fields(exc: BaseException, *, cfg: Config) -> tuple[dict[str, object], bool]:
136
+ """Build the bounded ``error`` sub-document; report whether a ceiling fired.
137
+
138
+ ``type`` keeps the bare class name consumers already index; ``module`` is added alongside it
139
+ so two same-named exception classes from different packages stay distinguishable, rather
140
+ than qualifying ``type`` in place and breaking every existing query (SPEC-017 FR-003).
141
+
142
+ ``stack`` gets its own, larger ceiling: a traceback is legitimately long and is the most
143
+ valuable thing on the event. It is clipped from the *head*, because ``format_exception``
144
+ puts the exception and the innermost frames last.
145
+ """
146
+ type_name, t1 = truncate_str(type(exc).__name__, cfg.max_value_bytes)
147
+ module, t2 = truncate_str(str(getattr(type(exc), "__module__", "")), cfg.max_value_bytes)
148
+ message, t3 = truncate_str(_exception_message(exc), cfg.max_value_bytes)
149
+ stack, t4 = truncate_tail(_exception_stack(exc), cfg.max_stack_bytes)
150
+ error: dict[str, object] = {
151
+ "type": type_name,
152
+ "module": module,
153
+ "message": message,
154
+ "stack": stack,
155
+ }
156
+ return error, (t1 or t2 or t3 or t4)
157
+
158
+
87
159
  def end_event(
88
160
  span: Span,
89
161
  status: str,
@@ -92,17 +164,20 @@ def end_event(
92
164
  """Build the span-end boundary event.
93
165
 
94
166
  Adds ``duration_ms`` (from a monotonic delta), ``status`` (``"ok"``/``"error"``), and on
95
- failure a nested ``error`` with the exception type and formatted stack (arch §6).
167
+ failure a nested ``error`` with the exception type, module, message and formatted stack
168
+ (arch §6, SPEC-017 FR-003).
96
169
  """
170
+ from log_foundry.config import get_config
171
+
97
172
  level = "INFO" if status == "ok" else "ERROR"
98
173
  event = build_event(span, level, _END_MESSAGE, fields={}, baggage={})
99
174
  event["duration_ms"] = (time.monotonic() - span.start_ts) * 1000.0
100
175
  event["status"] = status
101
176
  if exc is not None:
102
- event["error"] = {
103
- "type": type(exc).__name__,
104
- "stack": "".join(traceback.format_exception(type(exc), exc, exc.__traceback__)),
105
- }
177
+ error, clipped = _error_fields(exc, cfg=get_config())
178
+ event["error"] = error
179
+ if clipped:
180
+ event[_TRUNCATED] = True # only ever True — this *is* the OR with build_event's
106
181
  return event
107
182
 
108
183
 
@@ -123,13 +198,24 @@ def backfill_baggage(span: Span, baggage: dict[str, object]) -> None:
123
198
 
124
199
  The baggage is a parameter rather than read from ``context``: this module does not know where
125
200
  the current span lives (arch §6).
201
+
202
+ These values bypass :func:`build_event`'s pass entirely — ``set_baggage`` accepts arbitrary
203
+ objects — so they are coerced here, **once** above the loop rather than per event: the same
204
+ mapping is merged into every boundary event, and ``fields`` is already sanitized. The merge
205
+ can push a mapping past ``max_keys``; that is deliberate, because re-capping here would drop
206
+ the correlation keys SPEC-015 shipped to add (SPEC-017 FR-001).
126
207
  """
127
208
  if not baggage:
128
209
  return
210
+ from log_foundry.config import get_config
211
+
212
+ safe, clipped = sanitize_fields(baggage, cfg=get_config())
129
213
  for event in span.events:
130
214
  # Matched on the message constants, not a position — that ``span.start`` is index 0 is an
131
215
  # implementation detail of when it happens to be appended.
132
216
  if event.get("message") in (_START_MESSAGE, _END_MESSAGE):
133
217
  fields = event.get("fields")
134
218
  if isinstance(fields, dict):
135
- event["fields"] = {**fields, **baggage}
219
+ event["fields"] = {**fields, **safe}
220
+ if clipped:
221
+ event[_TRUNCATED] = True