log-foundry 0.10.2.dev42__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.
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/PKG-INFO +17 -4
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/README.md +16 -3
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/pyproject.toml +1 -1
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/src/log_foundry/api.py +113 -15
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/src/log_foundry/config.py +129 -32
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/src/log_foundry/model.py +4 -4
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/LICENSE +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/src/log_foundry/__init__.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/src/log_foundry/_diag.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/src/log_foundry/_lifecycle.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/src/log_foundry/console.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/src/log_foundry/context.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/src/log_foundry/decorator.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/src/log_foundry/ids.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/src/log_foundry/py.typed +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/src/log_foundry/sanitize.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/__init__.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/_batch.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/_chunk.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/_retry.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/_socket.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/_time.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/base.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/callback.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/clickhouse.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/datadog.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/elasticsearch.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/eventhubs.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/file.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/filtering.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/firehose.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/honeycomb.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/http.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/kafka.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/kinesis.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/logging_sink.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/logstash.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/loki.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/mongodb.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/multi.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/nats.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/newrelic.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/postgres.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/pubsub.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/rabbitmq.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/redis.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/sentry.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/sns.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/splunk.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/sqlite.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/sqs.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/stdout.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/syslog.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/transform.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/util.py +0 -0
- {log_foundry-0.10.2.dev42 → 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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
|
@@ -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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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))
|
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
import threading
|
|
6
|
+
from dataclasses import dataclass, field, replace
|
|
6
7
|
from typing import TYPE_CHECKING
|
|
7
8
|
|
|
8
9
|
if TYPE_CHECKING:
|
|
9
10
|
from log_foundry.sinks.base import Sink
|
|
10
11
|
|
|
11
12
|
|
|
12
|
-
@dataclass
|
|
13
|
+
@dataclass(frozen=True)
|
|
13
14
|
class Config:
|
|
14
15
|
"""Process-wide settings stamped onto every event and consulted by the pipeline.
|
|
15
16
|
|
|
@@ -22,6 +23,13 @@ class Config:
|
|
|
22
23
|
string is measured in UTF-8 bytes, an integer in the decimal length it renders as, sign
|
|
23
24
|
included. An integer is also bounded by ``sys.get_int_max_str_digits()`` whenever that
|
|
24
25
|
is lower, since a longer one cannot be rendered at all.
|
|
26
|
+
|
|
27
|
+
It is **frozen** (SPEC-034 FR-003). :func:`get_config` handed out this object, so assigning
|
|
28
|
+
to it retargeted what the config *reported* while every event continued to the sink the
|
|
29
|
+
worker had already captured — SPEC-030's defect reachable with no underscore in sight — and
|
|
30
|
+
assigning a ceiling bypassed :func:`_require_positive`, so ``max_value_bytes = 0`` was
|
|
31
|
+
accepted and emptied every event it touched. Both measured. :func:`configure` is the only
|
|
32
|
+
supported route to a change, and it rebinds the module global rather than mutating.
|
|
25
33
|
"""
|
|
26
34
|
|
|
27
35
|
service: str = "unknown"
|
|
@@ -37,6 +45,25 @@ class Config:
|
|
|
37
45
|
|
|
38
46
|
_config = Config()
|
|
39
47
|
|
|
48
|
+
_config_lock = threading.Lock()
|
|
49
|
+
"""Serializes the read-modify-write that replacing a frozen config now is.
|
|
50
|
+
|
|
51
|
+
Freezing :class:`Config` turned each field assignment into a whole-object
|
|
52
|
+
``replace()`` — a read of every field followed by a write of every field — and one of the two
|
|
53
|
+
call sites, :func:`_ensure_sink`, runs on the **orphan logging path**, on whatever application
|
|
54
|
+
thread called ``info()``. A stale snapshot there puts back the pre-``configure()`` ``service``,
|
|
55
|
+
``version``, ``env``, ``defaults`` *and* ``sink``, permanently. Measured on the unlocked version:
|
|
56
|
+
268 of 2000 trials shipped every later event with ``service="unknown"`` after one concurrent
|
|
57
|
+
``info()``, against 0 before the freeze — a regression, and the SPEC-024 category of wrong data
|
|
58
|
+
rather than lost data.
|
|
59
|
+
|
|
60
|
+
It does **not** cover reads. :func:`_live_config` is one atomic global read and stays lock-free,
|
|
61
|
+
so the per-event path pays nothing (SPEC-034 FR-003 AC-6). Lock ordering is one-way and stays
|
|
62
|
+
that way: ``_ensure_sink`` is called with ``decorator._worker_lock`` held, and ``configure()``
|
|
63
|
+
releases this lock before ``_swap_live_sink`` takes that one, so nothing acquires
|
|
64
|
+
``_worker_lock`` underneath this.
|
|
65
|
+
"""
|
|
66
|
+
|
|
40
67
|
|
|
41
68
|
def _require_positive(name: str, value: int | None) -> None:
|
|
42
69
|
"""Rejects a non-positive ceiling.
|
|
@@ -127,24 +154,22 @@ def configure(
|
|
|
127
154
|
_require_positive("max_keys", max_keys)
|
|
128
155
|
_require_positive("max_depth", max_depth)
|
|
129
156
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
if max_depth is not None:
|
|
147
|
-
_config.max_depth = max_depth
|
|
157
|
+
changed: dict[str, object] = {
|
|
158
|
+
name: value
|
|
159
|
+
for name, value in (
|
|
160
|
+
("service", service),
|
|
161
|
+
("version", version),
|
|
162
|
+
("env", env),
|
|
163
|
+
("sink", sink),
|
|
164
|
+
("defaults", None if defaults is None else dict(defaults)),
|
|
165
|
+
("max_value_bytes", max_value_bytes),
|
|
166
|
+
("max_stack_bytes", max_stack_bytes),
|
|
167
|
+
("max_keys", max_keys),
|
|
168
|
+
("max_depth", max_depth),
|
|
169
|
+
)
|
|
170
|
+
if value is not None
|
|
171
|
+
}
|
|
172
|
+
_rebind(**changed)
|
|
148
173
|
|
|
149
174
|
_ensure_sink()
|
|
150
175
|
|
|
@@ -183,18 +208,73 @@ def _swap_live_sink(sink: Sink) -> None:
|
|
|
183
208
|
def get_config() -> Config:
|
|
184
209
|
"""Returns the current global config, for reading.
|
|
185
210
|
|
|
186
|
-
**Mutating what this returns
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
211
|
+
**Mutating what this returns raises** (SPEC-034 FR-003). It used to hand back the live
|
|
212
|
+
singleton, so assigning to it retargeted what the config *reported* while every event
|
|
213
|
+
continued to the sink the worker had already captured, and assigning a ceiling bypassed the
|
|
214
|
+
validation :func:`configure` performs — ``max_value_bytes = 0`` was accepted and emptied
|
|
215
|
+
every event it touched. Both measured. :func:`configure` is the only route to a change.
|
|
216
|
+
|
|
217
|
+
It is a **copy**, not the frozen original, and ``defaults`` is copied with it. A caller who
|
|
218
|
+
defeats the freeze — ``object.__setattr__`` reaches through any frozen dataclass — then
|
|
219
|
+
edits an object the library does not read, rather than the live config; and ``defaults`` is
|
|
220
|
+
a plain mutable ``dict``, so sharing it would leave the freeze cosmetic at the one field
|
|
221
|
+
that is not a scalar. ``dataclasses.replace`` alone does **not** do this: it shares the
|
|
222
|
+
dict, which was measured while building this.
|
|
192
223
|
|
|
193
224
|
Args:
|
|
194
225
|
None.
|
|
195
226
|
|
|
196
227
|
Returns:
|
|
197
|
-
|
|
228
|
+
A copy of the process-wide :class:`Config`, with its own ``defaults``.
|
|
229
|
+
|
|
230
|
+
Raises:
|
|
231
|
+
None.
|
|
232
|
+
"""
|
|
233
|
+
return replace(_config, defaults=dict(_config.defaults))
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _rebind(**changed: object) -> None:
|
|
237
|
+
"""Replaces the module-global config with a copy carrying the changed fields.
|
|
238
|
+
|
|
239
|
+
:class:`Config` is frozen (SPEC-034 FR-003), so a change is a new object and a rebinding of
|
|
240
|
+
the global rather than an assignment to a field. It is **one** replacement for the whole
|
|
241
|
+
call, not one per field: nine rebindings would allocate nine configs and, worse, would leave
|
|
242
|
+
a window in which another thread reads a half-applied config — a `service` from the new call
|
|
243
|
+
beside a `sink` from the old one, stamped onto real events.
|
|
244
|
+
|
|
245
|
+
Rebinding is safe only because no module imports ``_config`` by value; a
|
|
246
|
+
``from log_foundry.config import _config`` anywhere would hold the pre-rebind object forever,
|
|
247
|
+
which is why a test asserts the absence rather than a comment claiming it.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
**changed: Field names and their new values. Fields not named keep their current value.
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
None.
|
|
254
|
+
|
|
255
|
+
Raises:
|
|
256
|
+
None.
|
|
257
|
+
"""
|
|
258
|
+
global _config
|
|
259
|
+
with _config_lock:
|
|
260
|
+
_config = replace(_config, **changed) # type: ignore[arg-type]
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _live_config() -> Config:
|
|
264
|
+
"""Returns the config object itself, for callers inside the package.
|
|
265
|
+
|
|
266
|
+
:func:`get_config` copies, because it is public and a caller must not be able to reach the
|
|
267
|
+
live object through it. ``model.build_event`` reads the config **one to three times per
|
|
268
|
+
event**, so routing that through the public accessor would allocate a ``Config`` and a
|
|
269
|
+
``defaults`` dict per event. The freeze is a guarantee to the library's *users*, not one the
|
|
270
|
+
library needs against itself, so internal callers read the live object and treat it as
|
|
271
|
+
read-only — the same split :func:`context._live_baggage` makes for baggage.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
None.
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
The live process-wide :class:`Config`.
|
|
198
278
|
|
|
199
279
|
Raises:
|
|
200
280
|
None.
|
|
@@ -210,6 +290,14 @@ def _ensure_sink() -> Sink:
|
|
|
210
290
|
user has not called ``configure()`` yet. The local import defers the ``sinks``
|
|
211
291
|
dependency and avoids a top-level import cycle (arch §7).
|
|
212
292
|
|
|
293
|
+
It is called on the **orphan logging path**, on arbitrary application threads, so the
|
|
294
|
+
default is resolved under :data:`_config_lock` with a double check and the global is re-read
|
|
295
|
+
inside it (SPEC-034 FR-003). Returning a freshly built local instead handed two racing
|
|
296
|
+
threads two different ``StdoutSink`` objects — measured 996 of 3000 trials — one of which
|
|
297
|
+
then received events and was referenced by nothing, so nothing closed it (SPEC-031 FR-006).
|
|
298
|
+
The unlocked read above it is the fast path: once a sink is configured this is one atomic
|
|
299
|
+
global read and no lock at all.
|
|
300
|
+
|
|
213
301
|
Args:
|
|
214
302
|
None.
|
|
215
303
|
|
|
@@ -219,8 +307,17 @@ def _ensure_sink() -> Sink:
|
|
|
219
307
|
Raises:
|
|
220
308
|
None.
|
|
221
309
|
"""
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
310
|
+
global _config
|
|
311
|
+
sink_now = _config.sink
|
|
312
|
+
if sink_now is not None:
|
|
313
|
+
return sink_now
|
|
314
|
+
|
|
315
|
+
from log_foundry.sinks.stdout import StdoutSink
|
|
316
|
+
|
|
317
|
+
with _config_lock:
|
|
318
|
+
if _config.sink is None:
|
|
319
|
+
_config = replace(_config, sink=StdoutSink())
|
|
320
|
+
resolved = _config.sink
|
|
321
|
+
if resolved is None: # pragma: no cover - unreachable; the branch above just set it
|
|
322
|
+
raise RuntimeError("the default sink could not be resolved")
|
|
323
|
+
return resolved
|
|
@@ -8,7 +8,7 @@ from dataclasses import dataclass, field
|
|
|
8
8
|
from datetime import UTC, datetime
|
|
9
9
|
from typing import TYPE_CHECKING
|
|
10
10
|
|
|
11
|
-
from log_foundry.config import
|
|
11
|
+
from log_foundry.config import _live_config
|
|
12
12
|
from log_foundry.ids import new_log_id
|
|
13
13
|
from log_foundry.sanitize import sanitize_fields, truncate_str, truncate_tail
|
|
14
14
|
|
|
@@ -93,7 +93,7 @@ def build_event(
|
|
|
93
93
|
Raises:
|
|
94
94
|
None.
|
|
95
95
|
"""
|
|
96
|
-
cfg =
|
|
96
|
+
cfg = _live_config()
|
|
97
97
|
merged: dict[str, object] = {**cfg.defaults, **span.defaults, **baggage, **fields}
|
|
98
98
|
safe, clipped = sanitize_fields(merged, cfg=cfg)
|
|
99
99
|
bounded_message, message_clipped = truncate_str(message, cfg.max_value_bytes)
|
|
@@ -236,7 +236,7 @@ def end_event(
|
|
|
236
236
|
event["duration_ms"] = (time.monotonic() - span.start_ts) * 1000.0
|
|
237
237
|
event["status"] = status
|
|
238
238
|
if exc is not None:
|
|
239
|
-
error, clipped = _error_fields(exc, cfg=
|
|
239
|
+
error, clipped = _error_fields(exc, cfg=_live_config())
|
|
240
240
|
event["error"] = error
|
|
241
241
|
if clipped:
|
|
242
242
|
event[_TRUNCATED] = True
|
|
@@ -269,7 +269,7 @@ def backfill_baggage(span: Span, baggage: dict[str, object]) -> None:
|
|
|
269
269
|
"""
|
|
270
270
|
if not baggage:
|
|
271
271
|
return
|
|
272
|
-
safe, clipped = sanitize_fields(baggage, cfg=
|
|
272
|
+
safe, clipped = sanitize_fields(baggage, cfg=_live_config())
|
|
273
273
|
for event in span.events:
|
|
274
274
|
if event.get("message") in (_START_MESSAGE, _END_MESSAGE):
|
|
275
275
|
fields = event.get("fields")
|
|
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
|
{log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev44}/src/log_foundry/sinks/elasticsearch.py
RENAMED
|
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
|