log-foundry 0.10.2.dev43__tar.gz → 0.10.2.dev44__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 (56) hide show
  1. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/PKG-INFO +17 -4
  2. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/README.md +16 -3
  3. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/pyproject.toml +1 -1
  4. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/src/log_foundry/api.py +113 -15
  5. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/LICENSE +0 -0
  6. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/src/log_foundry/__init__.py +0 -0
  7. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/src/log_foundry/_diag.py +0 -0
  8. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/src/log_foundry/_lifecycle.py +0 -0
  9. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/src/log_foundry/config.py +0 -0
  10. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/src/log_foundry/console.py +0 -0
  11. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/src/log_foundry/context.py +0 -0
  12. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/src/log_foundry/decorator.py +0 -0
  13. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/src/log_foundry/ids.py +0 -0
  14. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/src/log_foundry/model.py +0 -0
  15. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/src/log_foundry/py.typed +0 -0
  16. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/src/log_foundry/sanitize.py +0 -0
  17. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/__init__.py +0 -0
  18. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/_batch.py +0 -0
  19. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/_chunk.py +0 -0
  20. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/_retry.py +0 -0
  21. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/_socket.py +0 -0
  22. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/_time.py +0 -0
  23. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/base.py +0 -0
  24. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/callback.py +0 -0
  25. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/clickhouse.py +0 -0
  26. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/datadog.py +0 -0
  27. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/elasticsearch.py +0 -0
  28. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/eventhubs.py +0 -0
  29. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/file.py +0 -0
  30. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/filtering.py +0 -0
  31. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/firehose.py +0 -0
  32. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/honeycomb.py +0 -0
  33. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/http.py +0 -0
  34. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/kafka.py +0 -0
  35. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/kinesis.py +0 -0
  36. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/logging_sink.py +0 -0
  37. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/logstash.py +0 -0
  38. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/loki.py +0 -0
  39. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/mongodb.py +0 -0
  40. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/multi.py +0 -0
  41. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/nats.py +0 -0
  42. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/newrelic.py +0 -0
  43. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/postgres.py +0 -0
  44. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/pubsub.py +0 -0
  45. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/rabbitmq.py +0 -0
  46. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/redis.py +0 -0
  47. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/sentry.py +0 -0
  48. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/sns.py +0 -0
  49. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/splunk.py +0 -0
  50. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/sqlite.py +0 -0
  51. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/sqs.py +0 -0
  52. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/stdout.py +0 -0
  53. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/syslog.py +0 -0
  54. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/transform.py +0 -0
  55. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/util.py +0 -0
  56. {log_foundry-0.10.2.dev43 → log_foundry-0.10.2.dev44}/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.dev43
3
+ Version: 0.10.2.dev44
4
4
  Summary: Generate logs for your console and JSON events for downstream consumption.
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -327,8 +327,8 @@ await fetch(4127) # one trace_id; load's parent_span_id == fetch's span_id
327
327
  Emit your own structured events from inside a decorated call with the level functions
328
328
  `debug` / `info` / `warning` / `error` / `critical`. Each appends one event to the current
329
329
  span, so the whole call's logs flush together and share its `trace_id` / `span_id`. Keyword
330
- arguments land in the event's `fields`; the function name is captured, but arguments and
331
- return values never are.
330
+ arguments land in the event's `fields` — except the three reserved names below; the function
331
+ name is captured, but arguments and return values never are.
332
332
 
333
333
  ```python
334
334
  @lf.trace
@@ -341,7 +341,8 @@ def process_payment(user_id: int) -> str:
341
341
 
342
342
  - **`set_baggage(**kv)`** — attach trace-scoped context that is merged into the `fields` of
343
343
  every subsequent event in the same execution flow. Precedence, lowest to highest: config
344
- `defaults` → span `defaults` → baggage → per-call `fields`. **Trace-scoped means it ends
344
+ `defaults` → span `defaults` → baggage → per-call fields (`fields=` first, then `**kwargs`
345
+ over it). **Trace-scoped means it ends
345
346
  with the trace:** when the outermost `@trace` call returns *or raises*, the baggage in effect
346
347
  before it is restored, so one request's keys do not reach the next request's events. Nested
347
348
  calls do not reset — baggage set three calls deep stays visible to its parent and to the
@@ -352,6 +353,18 @@ def process_payment(user_id: int) -> str:
352
353
  - **`echo=True`** — *additionally* write a human-readable `LEVEL message` line to the console
353
354
  (`sys.stderr` by default), synchronously, without waiting for the async flush. The event
354
355
  still rides the normal pipeline to the sink — echo never redirects.
356
+ - **`fields={...}`** — the escape hatch. `message`, `echo` and `fields` are **reserved**: they
357
+ are parameters, so `info("x", echo="the payload we echoed back")` would switch on the console
358
+ line instead of recording a field. Pass them — and any key that is not a Python identifier —
359
+ through `fields=`:
360
+
361
+ ```python
362
+ lf.info("proxied", fields={"echo": "the payload we echoed back", "content-type": "text/json"})
363
+ ```
364
+
365
+ It reaches its own name too (`fields={"fields": ...}`), so every reserved word has exactly one
366
+ route through. A key given both ways takes the keyword's value, since `**kwargs` is what you
367
+ wrote at the call site and `fields=` is usually a mapping built elsewhere.
355
368
  - **Orphan logs** — a level call made with no active span is not dropped: it emits a standalone
356
369
  one-event span with a fresh `trace_id`, flushed straight to the sink.
357
370
 
@@ -291,8 +291,8 @@ await fetch(4127) # one trace_id; load's parent_span_id == fetch's span_id
291
291
  Emit your own structured events from inside a decorated call with the level functions
292
292
  `debug` / `info` / `warning` / `error` / `critical`. Each appends one event to the current
293
293
  span, so the whole call's logs flush together and share its `trace_id` / `span_id`. Keyword
294
- arguments land in the event's `fields`; the function name is captured, but arguments and
295
- return values never are.
294
+ arguments land in the event's `fields` — except the three reserved names below; the function
295
+ name is captured, but arguments and return values never are.
296
296
 
297
297
  ```python
298
298
  @lf.trace
@@ -305,7 +305,8 @@ def process_payment(user_id: int) -> str:
305
305
 
306
306
  - **`set_baggage(**kv)`** — attach trace-scoped context that is merged into the `fields` of
307
307
  every subsequent event in the same execution flow. Precedence, lowest to highest: config
308
- `defaults` → span `defaults` → baggage → per-call `fields`. **Trace-scoped means it ends
308
+ `defaults` → span `defaults` → baggage → per-call fields (`fields=` first, then `**kwargs`
309
+ over it). **Trace-scoped means it ends
309
310
  with the trace:** when the outermost `@trace` call returns *or raises*, the baggage in effect
310
311
  before it is restored, so one request's keys do not reach the next request's events. Nested
311
312
  calls do not reset — baggage set three calls deep stays visible to its parent and to the
@@ -316,6 +317,18 @@ def process_payment(user_id: int) -> str:
316
317
  - **`echo=True`** — *additionally* write a human-readable `LEVEL message` line to the console
317
318
  (`sys.stderr` by default), synchronously, without waiting for the async flush. The event
318
319
  still rides the normal pipeline to the sink — echo never redirects.
320
+ - **`fields={...}`** — the escape hatch. `message`, `echo` and `fields` are **reserved**: they
321
+ are parameters, so `info("x", echo="the payload we echoed back")` would switch on the console
322
+ line instead of recording a field. Pass them — and any key that is not a Python identifier —
323
+ through `fields=`:
324
+
325
+ ```python
326
+ lf.info("proxied", fields={"echo": "the payload we echoed back", "content-type": "text/json"})
327
+ ```
328
+
329
+ It reaches its own name too (`fields={"fields": ...}`), so every reserved word has exactly one
330
+ route through. A key given both ways takes the keyword's value, since `**kwargs` is what you
331
+ wrote at the call site and `fields=` is usually a mapping built elsewhere.
319
332
  - **Orphan logs** — a level call made with no active span is not dropped: it emits a standalone
320
333
  one-event span with a fresh `trace_id`, flushed straight to the sink.
321
334
 
@@ -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.dev43"
23
+ version = "0.10.2.dev44"
24
24
 
25
25
  [project.optional-dependencies]
26
26
  aws = ["boto3>=1.43.61"] # SQSSink, SNSSink, KinesisSink, FirehoseSink
@@ -2,6 +2,8 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from typing import TYPE_CHECKING
6
+
5
7
  from log_foundry import _diag, context
6
8
  from log_foundry.config import _ensure_sink
7
9
  from log_foundry.console import ConsoleWriter
@@ -10,6 +12,9 @@ from log_foundry.decorator import _note_orphan_emit
10
12
  from log_foundry.ids import new_span_id, new_trace_id
11
13
  from log_foundry.model import Span, build_event
12
14
 
15
+ if TYPE_CHECKING:
16
+ from collections.abc import Mapping
17
+
13
18
  __all__ = [
14
19
  "critical",
15
20
  "debug",
@@ -100,13 +105,66 @@ def _log(level: str, message: str, echo: bool, fields: dict[str, object]) -> Non
100
105
  _diag.absorbed("echoing to the console", exc)
101
106
 
102
107
 
103
- def debug(message: str, *, echo: bool = False, **fields: object) -> None:
108
+ def _merge(fields: Mapping[str, object] | None, kv: dict[str, object]) -> dict[str, object]:
109
+ """Combines the explicit ``fields=`` mapping with the keyword form.
110
+
111
+ ``echo`` and ``message`` were reserved words stolen from the caller's field namespace:
112
+ ``info("x", echo="incoming payload echoed back")`` dropped a real field *and* turned on an
113
+ unwanted console line, and ``message=`` raised ``TypeError`` (SPEC-034 FR-004). ``fields=``
114
+ is the way round all three — including its own name, which is why the escape hatch can
115
+ express every key including the ones this signature reserves — and the only way to pass a
116
+ key that is not a Python identifier.
117
+
118
+ The keyword form wins a collision. ``fields=`` is the bulk route, usually a mapping built
119
+ somewhere else; ``**kv`` is what the caller wrote at this call site, and a literal
120
+ overriding a base is what ``{**base, **overrides}`` already means in the language.
121
+
122
+ A ``fields=`` that is not a mapping is **absorbed, not raised**. This helper runs in the
123
+ emitter, before :func:`_log`, so it sits outside that function's orphan guard entirely: an
124
+ unguarded ``{**fields, **kv}`` propagated a ``TypeError`` into the application on all four
125
+ entry paths, including the orphan one where SPEC-025's promise holds today, and inside a span
126
+ the decorator then recorded ``status=error`` with an ``error.type`` the caller's code never
127
+ raised. It was also asymmetric in a way neither reading defends — ``fields=[]`` was silently
128
+ ignored while ``fields=["x"]`` crashed. The library coerces rather than validates (SPEC-017),
129
+ so the tolerant half is the one that matches, and the fault is announced by type.
130
+
131
+ Args:
132
+ fields: The explicit mapping, or ``None``.
133
+ kv: The keyword-collected fields.
134
+
135
+ Returns:
136
+ One mapping, with ``kv`` taking precedence. The caller's ``fields`` is never mutated, and a
137
+ ``fields=`` that could not be merged is dropped rather than costing the event.
138
+
139
+ Raises:
140
+ None.
141
+ """
142
+ if not fields:
143
+ return kv
144
+ try:
145
+ return {**fields, **kv}
146
+ except Exception as exc:
147
+ _diag.absorbed("merging the fields= argument", exc, "the fields= argument was ignored")
148
+ return kv
149
+
150
+
151
+ def debug(
152
+ message: str,
153
+ *,
154
+ echo: bool = False,
155
+ fields: Mapping[str, object] | None = None,
156
+ **kv: object,
157
+ ) -> None:
104
158
  """Emits a ``DEBUG`` event on the current span, or a standalone orphan span.
105
159
 
106
160
  Args:
107
161
  message: The message text.
108
162
  echo: Whether to also write a human-readable console line.
109
- **fields: Per-call structured fields.
163
+ fields: Per-call structured fields, for names ``**kv`` cannot express — the three
164
+ reserved words ``message``, ``echo`` and ``fields`` itself, and any key that is not a
165
+ Python identifier at all. Merged **under** ``**kv``, so a name given both ways takes the
166
+ keyword's value.
167
+ **kv: Per-call structured fields.
110
168
 
111
169
  Returns:
112
170
  None.
@@ -114,16 +172,26 @@ def debug(message: str, *, echo: bool = False, **fields: object) -> None:
114
172
  Raises:
115
173
  None.
116
174
  """
117
- _log("DEBUG", message, echo, fields)
175
+ _log("DEBUG", message, echo, _merge(fields, kv))
118
176
 
119
177
 
120
- def info(message: str, *, echo: bool = False, **fields: object) -> None:
178
+ def info(
179
+ message: str,
180
+ *,
181
+ echo: bool = False,
182
+ fields: Mapping[str, object] | None = None,
183
+ **kv: object,
184
+ ) -> None:
121
185
  """Emits an ``INFO`` event on the current span, or a standalone orphan span.
122
186
 
123
187
  Args:
124
188
  message: The message text.
125
189
  echo: Whether to also write a human-readable console line.
126
- **fields: Per-call structured fields.
190
+ fields: Per-call structured fields, for names ``**kv`` cannot express — the three
191
+ reserved words ``message``, ``echo`` and ``fields`` itself, and any key that is not a
192
+ Python identifier at all. Merged **under** ``**kv``, so a name given both ways takes the
193
+ keyword's value.
194
+ **kv: Per-call structured fields.
127
195
 
128
196
  Returns:
129
197
  None.
@@ -131,16 +199,26 @@ def info(message: str, *, echo: bool = False, **fields: object) -> None:
131
199
  Raises:
132
200
  None.
133
201
  """
134
- _log("INFO", message, echo, fields)
202
+ _log("INFO", message, echo, _merge(fields, kv))
135
203
 
136
204
 
137
- def warning(message: str, *, echo: bool = False, **fields: object) -> None:
205
+ def warning(
206
+ message: str,
207
+ *,
208
+ echo: bool = False,
209
+ fields: Mapping[str, object] | None = None,
210
+ **kv: object,
211
+ ) -> None:
138
212
  """Emits a ``WARNING`` event on the current span, or a standalone orphan span.
139
213
 
140
214
  Args:
141
215
  message: The message text.
142
216
  echo: Whether to also write a human-readable console line.
143
- **fields: Per-call structured fields.
217
+ fields: Per-call structured fields, for names ``**kv`` cannot express — the three
218
+ reserved words ``message``, ``echo`` and ``fields`` itself, and any key that is not a
219
+ Python identifier at all. Merged **under** ``**kv``, so a name given both ways takes the
220
+ keyword's value.
221
+ **kv: Per-call structured fields.
144
222
 
145
223
  Returns:
146
224
  None.
@@ -148,16 +226,26 @@ def warning(message: str, *, echo: bool = False, **fields: object) -> None:
148
226
  Raises:
149
227
  None.
150
228
  """
151
- _log("WARNING", message, echo, fields)
229
+ _log("WARNING", message, echo, _merge(fields, kv))
152
230
 
153
231
 
154
- def error(message: str, *, echo: bool = False, **fields: object) -> None:
232
+ def error(
233
+ message: str,
234
+ *,
235
+ echo: bool = False,
236
+ fields: Mapping[str, object] | None = None,
237
+ **kv: object,
238
+ ) -> None:
155
239
  """Emits an ``ERROR`` event on the current span, or a standalone orphan span.
156
240
 
157
241
  Args:
158
242
  message: The message text.
159
243
  echo: Whether to also write a human-readable console line.
160
- **fields: Per-call structured fields.
244
+ fields: Per-call structured fields, for names ``**kv`` cannot express — the three
245
+ reserved words ``message``, ``echo`` and ``fields`` itself, and any key that is not a
246
+ Python identifier at all. Merged **under** ``**kv``, so a name given both ways takes the
247
+ keyword's value.
248
+ **kv: Per-call structured fields.
161
249
 
162
250
  Returns:
163
251
  None.
@@ -165,16 +253,26 @@ def error(message: str, *, echo: bool = False, **fields: object) -> None:
165
253
  Raises:
166
254
  None.
167
255
  """
168
- _log("ERROR", message, echo, fields)
256
+ _log("ERROR", message, echo, _merge(fields, kv))
169
257
 
170
258
 
171
- def critical(message: str, *, echo: bool = False, **fields: object) -> None:
259
+ def critical(
260
+ message: str,
261
+ *,
262
+ echo: bool = False,
263
+ fields: Mapping[str, object] | None = None,
264
+ **kv: object,
265
+ ) -> None:
172
266
  """Emits a ``CRITICAL`` event on the current span, or a standalone orphan span.
173
267
 
174
268
  Args:
175
269
  message: The message text.
176
270
  echo: Whether to also write a human-readable console line.
177
- **fields: Per-call structured fields.
271
+ fields: Per-call structured fields, for names ``**kv`` cannot express — the three
272
+ reserved words ``message``, ``echo`` and ``fields`` itself, and any key that is not a
273
+ Python identifier at all. Merged **under** ``**kv``, so a name given both ways takes the
274
+ keyword's value.
275
+ **kv: Per-call structured fields.
178
276
 
179
277
  Returns:
180
278
  None.
@@ -182,4 +280,4 @@ def critical(message: str, *, echo: bool = False, **fields: object) -> None:
182
280
  Raises:
183
281
  None.
184
282
  """
185
- _log("CRITICAL", message, echo, fields)
283
+ _log("CRITICAL", message, echo, _merge(fields, kv))