log-foundry 0.10.2.dev42__tar.gz → 0.10.2.dev43__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.dev43}/PKG-INFO +1 -1
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/pyproject.toml +1 -1
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/src/log_foundry/config.py +129 -32
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/src/log_foundry/model.py +4 -4
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/LICENSE +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/README.md +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/src/log_foundry/__init__.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/src/log_foundry/_diag.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/src/log_foundry/_lifecycle.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/src/log_foundry/api.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/src/log_foundry/console.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/src/log_foundry/context.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/src/log_foundry/decorator.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/src/log_foundry/ids.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/src/log_foundry/py.typed +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/src/log_foundry/sanitize.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/src/log_foundry/sinks/__init__.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/src/log_foundry/sinks/_batch.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/src/log_foundry/sinks/_chunk.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/src/log_foundry/sinks/_retry.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/src/log_foundry/sinks/_socket.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/src/log_foundry/sinks/_time.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/src/log_foundry/sinks/base.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/src/log_foundry/sinks/callback.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/src/log_foundry/sinks/clickhouse.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/src/log_foundry/sinks/datadog.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/src/log_foundry/sinks/elasticsearch.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/src/log_foundry/sinks/eventhubs.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/src/log_foundry/sinks/file.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/src/log_foundry/sinks/filtering.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/src/log_foundry/sinks/firehose.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/src/log_foundry/sinks/honeycomb.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/src/log_foundry/sinks/http.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/src/log_foundry/sinks/kafka.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/src/log_foundry/sinks/kinesis.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/src/log_foundry/sinks/logging_sink.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/src/log_foundry/sinks/logstash.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/src/log_foundry/sinks/loki.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/src/log_foundry/sinks/mongodb.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/src/log_foundry/sinks/multi.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/src/log_foundry/sinks/nats.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/src/log_foundry/sinks/newrelic.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/src/log_foundry/sinks/postgres.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/src/log_foundry/sinks/pubsub.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/src/log_foundry/sinks/rabbitmq.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/src/log_foundry/sinks/redis.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/src/log_foundry/sinks/sentry.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/src/log_foundry/sinks/sns.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/src/log_foundry/sinks/splunk.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/src/log_foundry/sinks/sqlite.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/src/log_foundry/sinks/sqs.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/src/log_foundry/sinks/stdout.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/src/log_foundry/sinks/syslog.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/src/log_foundry/sinks/transform.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/src/log_foundry/sinks/util.py +0 -0
- {log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/src/log_foundry/worker.py +0 -0
|
@@ -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
|
|
File without changes
|
|
File without changes
|
{log_foundry-0.10.2.dev42 → log_foundry-0.10.2.dev43}/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
|