log-foundry 0.10.2.dev68__tar.gz → 0.10.2.dev69__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.dev68 → log_foundry-0.10.2.dev69}/PKG-INFO +10 -1
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/README.md +9 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/pyproject.toml +1 -1
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/src/log_foundry/context.py +21 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/src/log_foundry/decorator.py +158 -8
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/src/log_foundry/model.py +13 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/LICENSE +0 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/src/log_foundry/__init__.py +0 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/src/log_foundry/_diag.py +0 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/src/log_foundry/_fork.py +0 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/src/log_foundry/_lifecycle.py +0 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/src/log_foundry/api.py +0 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/src/log_foundry/config.py +0 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/src/log_foundry/console.py +0 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/src/log_foundry/ids.py +0 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/src/log_foundry/py.typed +0 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/src/log_foundry/results.py +0 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/src/log_foundry/sanitize.py +0 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/src/log_foundry/sinks/__init__.py +0 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/src/log_foundry/sinks/_batch.py +0 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/src/log_foundry/sinks/_chunk.py +0 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/src/log_foundry/sinks/_retry.py +0 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/src/log_foundry/sinks/_socket.py +0 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/src/log_foundry/sinks/_time.py +0 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/src/log_foundry/sinks/base.py +0 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/src/log_foundry/sinks/callback.py +0 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/src/log_foundry/sinks/clickhouse.py +0 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/src/log_foundry/sinks/datadog.py +0 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/src/log_foundry/sinks/elasticsearch.py +0 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/src/log_foundry/sinks/eventhubs.py +0 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/src/log_foundry/sinks/file.py +0 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/src/log_foundry/sinks/filtering.py +0 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/src/log_foundry/sinks/firehose.py +0 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/src/log_foundry/sinks/honeycomb.py +0 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/src/log_foundry/sinks/http.py +0 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/src/log_foundry/sinks/kafka.py +0 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/src/log_foundry/sinks/kinesis.py +0 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/src/log_foundry/sinks/logging_sink.py +0 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/src/log_foundry/sinks/logstash.py +0 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/src/log_foundry/sinks/loki.py +0 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/src/log_foundry/sinks/memory.py +0 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/src/log_foundry/sinks/mongodb.py +0 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/src/log_foundry/sinks/multi.py +0 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/src/log_foundry/sinks/nats.py +0 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/src/log_foundry/sinks/newrelic.py +0 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/src/log_foundry/sinks/null.py +0 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/src/log_foundry/sinks/postgres.py +0 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/src/log_foundry/sinks/pubsub.py +0 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/src/log_foundry/sinks/rabbitmq.py +0 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/src/log_foundry/sinks/redis.py +0 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/src/log_foundry/sinks/sentry.py +0 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/src/log_foundry/sinks/sns.py +0 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/src/log_foundry/sinks/splunk.py +0 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/src/log_foundry/sinks/sqlite.py +0 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/src/log_foundry/sinks/sqs.py +0 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/src/log_foundry/sinks/stdout.py +0 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/src/log_foundry/sinks/syslog.py +0 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/src/log_foundry/sinks/transform.py +0 -0
- {log_foundry-0.10.2.dev68 → log_foundry-0.10.2.dev69}/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.dev69
|
|
4
4
|
Summary: Generate logs for your console and JSON events for downstream consumption.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -233,6 +233,15 @@ is FIFO, everything submitted before the call is necessarily ahead of that marke
|
|
|
233
233
|
exactly why the guarantee is "events submitted before this call", and why concurrent
|
|
234
234
|
submissions from other threads may or may not be included.
|
|
235
235
|
|
|
236
|
+
**`flush()` also sweeps the spans that are still open**, so an in-span event does not have to wait
|
|
237
|
+
for its span to close to be delivered. The span stays open and usable afterwards: its events go
|
|
238
|
+
now and its `span.end` arrives later, in its own batch. Two consequences worth knowing. Boundary
|
|
239
|
+
events swept this way carry the baggage as of the **flush** rather than as of the close, since
|
|
240
|
+
that completion has to happen before they leave. And the sweep reaches only the **calling
|
|
241
|
+
context's** spans — `contextvars` offers no way to enumerate another thread's or task's context,
|
|
242
|
+
so a `flush()` in a handler that fanned out to tasks does not reach what those tasks have
|
|
243
|
+
buffered; their events arrive when their own spans close.
|
|
244
|
+
|
|
236
245
|
## Usage
|
|
237
246
|
|
|
238
247
|
### `configure(...)`
|
|
@@ -197,6 +197,15 @@ is FIFO, everything submitted before the call is necessarily ahead of that marke
|
|
|
197
197
|
exactly why the guarantee is "events submitted before this call", and why concurrent
|
|
198
198
|
submissions from other threads may or may not be included.
|
|
199
199
|
|
|
200
|
+
**`flush()` also sweeps the spans that are still open**, so an in-span event does not have to wait
|
|
201
|
+
for its span to close to be delivered. The span stays open and usable afterwards: its events go
|
|
202
|
+
now and its `span.end` arrives later, in its own batch. Two consequences worth knowing. Boundary
|
|
203
|
+
events swept this way carry the baggage as of the **flush** rather than as of the close, since
|
|
204
|
+
that completion has to happen before they leave. And the sweep reaches only the **calling
|
|
205
|
+
context's** spans — `contextvars` offers no way to enumerate another thread's or task's context,
|
|
206
|
+
so a `flush()` in a handler that fanned out to tasks does not reach what those tasks have
|
|
207
|
+
buffered; their events arrive when their own spans close.
|
|
208
|
+
|
|
200
209
|
## Usage
|
|
201
210
|
|
|
202
211
|
### `configure(...)`
|
|
@@ -55,6 +55,27 @@ def current_span() -> Span | None:
|
|
|
55
55
|
return stack[-1] if stack else None
|
|
56
56
|
|
|
57
57
|
|
|
58
|
+
def _live_span_stack() -> tuple[Span, ...]:
|
|
59
|
+
"""Returns every span open on this context, outermost first (SPEC-036 FR-001).
|
|
60
|
+
|
|
61
|
+
The library's non-copying read, named as :func:`_live_baggage` is and distinguished from the
|
|
62
|
+
public accessors for the same reason (SPEC-034 FR-003): a caller gets a copy, the library
|
|
63
|
+
reads the live object. There is nothing to copy here in any case — the stack is a tuple, so
|
|
64
|
+
handing it out cannot let anyone mutate it. :func:`current_span` returns only the innermost,
|
|
65
|
+
which is what an event needs and not what a sweep does.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
None.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Every span open on this context, outermost first. Empty when none is.
|
|
72
|
+
|
|
73
|
+
Raises:
|
|
74
|
+
None.
|
|
75
|
+
"""
|
|
76
|
+
return _span_stack.get()
|
|
77
|
+
|
|
78
|
+
|
|
58
79
|
def push_span(span: Span) -> contextvars.Token[tuple[Span, ...]]:
|
|
59
80
|
"""Pushes a span onto the stack.
|
|
60
81
|
|
|
@@ -60,6 +60,13 @@ shutdown's event has every backoff collapsed to zero.
|
|
|
60
60
|
"""
|
|
61
61
|
_orphan_retired = False
|
|
62
62
|
|
|
63
|
+
_sweep_lock = threading.Lock()
|
|
64
|
+
"""Serializes the span sweep, so two threads cannot deliver one span's buffer twice.
|
|
65
|
+
|
|
66
|
+
The detach is a load and a store, and ``contextvars`` copies the same ``Span`` object into every
|
|
67
|
+
task and into any ``copy_context()`` thread, so the two-reader case is ordinary rather than exotic
|
|
68
|
+
(SPEC-036 FR-001 AC-10). A flush is not a hot path; a sink lock it is not competing with.
|
|
69
|
+
"""
|
|
63
70
|
_loss_lock = threading.Lock()
|
|
64
71
|
"""Guards the two loss counters, and deliberately not ``_worker_lock`` (SPEC-036 FR-003 AC-5).
|
|
65
72
|
|
|
@@ -197,6 +204,14 @@ def continue_trace(
|
|
|
197
204
|
_diag.rejected("parent_span_id given with no trace_id to join", parent_span_id)
|
|
198
205
|
announced = True
|
|
199
206
|
|
|
207
|
+
if adopted is not None and _current_span_was_swept():
|
|
208
|
+
_diag.rejected(
|
|
209
|
+
"the current span has already been flushed; trace context refused",
|
|
210
|
+
traceparent if traceparent is not None else str(trace_id),
|
|
211
|
+
)
|
|
212
|
+
adopted = None
|
|
213
|
+
announced = True
|
|
214
|
+
|
|
200
215
|
if adopted is not None:
|
|
201
216
|
context.set_adopted_context(*adopted)
|
|
202
217
|
_reparent_current_span(*adopted)
|
|
@@ -214,6 +229,32 @@ def continue_trace(
|
|
|
214
229
|
return ContinueResult(ok=False, reason="rejected" if announced else "nothing-supplied")
|
|
215
230
|
|
|
216
231
|
|
|
232
|
+
def _current_span_was_swept() -> bool:
|
|
233
|
+
"""Reports whether an in-span ``flush()`` has already shipped this span's events.
|
|
234
|
+
|
|
235
|
+
Read from ``context.current_span()`` — what :func:`_reparent_current_span` itself reads —
|
|
236
|
+
**and only when that span is a root**, which is the other half of that function's own guard.
|
|
237
|
+
A swept *child* is not a reason to refuse: the re-parent would have returned early on it and
|
|
238
|
+
rewritten nothing, so a refusal there prevents no corruption. It would still be wrong to
|
|
239
|
+
refuse — the two guards must agree, or the refusal fires where the thing it guards does not
|
|
240
|
+
run — though the adoption it spares reaches less than it appears to: SPEC-024 clears the
|
|
241
|
+
adopted context at the **root** span's close, so one made inside a child does not survive to
|
|
242
|
+
the next root span either. ``continue_trace``'s documented placement on the entry
|
|
243
|
+
point's first line is untouched either way: nothing has been swept that early.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
None.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
Whether the current span is a root that has been swept.
|
|
250
|
+
|
|
251
|
+
Raises:
|
|
252
|
+
None.
|
|
253
|
+
"""
|
|
254
|
+
span = context.current_span()
|
|
255
|
+
return span is not None and span.parent_span_id is None and span.swept
|
|
256
|
+
|
|
257
|
+
|
|
217
258
|
def _reparent_current_span(trace_id: str, parent_span_id: str | None) -> None:
|
|
218
259
|
"""Moves an already-open root span into the adopted trace, events included.
|
|
219
260
|
|
|
@@ -712,33 +753,133 @@ def _adopt_declined_swap(new_sink: Sink) -> None:
|
|
|
712
753
|
_orphan_sink = new_sink
|
|
713
754
|
|
|
714
755
|
|
|
756
|
+
def _sweep_open_spans() -> None:
|
|
757
|
+
"""Hands the worker every event buffered on an open span in this context (SPEC-036 FR-001).
|
|
758
|
+
|
|
759
|
+
An in-span event lives on ``span.events`` until the span *closes*, and ``Worker.flush``
|
|
760
|
+
drains the *queue* — so a ``flush()`` called inside a ``@trace``d function, which is where
|
|
761
|
+
the README's serverless recipe put it, had by construction nothing to drain. Measured: zero
|
|
762
|
+
of two events delivered, every counter clean, and ``FlushResult`` reporting ``reason=None``.
|
|
763
|
+
|
|
764
|
+
The span stays **open**: its events go now and its ``span.end`` arrives later in its own
|
|
765
|
+
batch. Closing and reopening was rejected — it would emit a ``span.end`` the function never
|
|
766
|
+
reached, with a fabricated ``duration_ms`` and ``status``.
|
|
767
|
+
|
|
768
|
+
Two things must happen before the events leave, and both are why this is not a one-liner.
|
|
769
|
+
The boundary events are backfilled **first**, because SPEC-015 completes them at close by
|
|
770
|
+
iterating ``span.events`` and a swept buffer would ship ``span.start`` with ``fields={}`` —
|
|
771
|
+
the very defect that spec exists to fix, recreated by any in-span flush. They therefore carry
|
|
772
|
+
the baggage as of the flush rather than as of the close, which is a real semantic change and
|
|
773
|
+
the alternative is mutating an event the worker already owns (SPEC-028). And the buffer is
|
|
774
|
+
**detached by swap**, never cleared: ``clear()`` empties the same list object the worker was
|
|
775
|
+
handed.
|
|
776
|
+
|
|
777
|
+
The worker is created when there is something to submit, and **resolved before the buffer is
|
|
778
|
+
detached**. That ordering is the whole of the difference between a lost batch and a delivered
|
|
779
|
+
one: ``_get_worker`` can raise — it ends in ``Thread.start()`` — and a detach that has already
|
|
780
|
+
happened leaves the events in a discarded local while the span reads empty and ``flush()``
|
|
781
|
+
reports success. Measured with the failure injected: 3 of 4 events destroyed, every counter
|
|
782
|
+
zero, on a span that was still open and would have delivered them at its close.
|
|
783
|
+
``Worker.submit`` raises nothing, so once it is reached the batch is safe. Creating the worker
|
|
784
|
+
at all narrows SPEC-013's refusal rather than contradicting it — that exists so an *empty*
|
|
785
|
+
flush does not stand up a thread, and a sweep that found buffered events is not an empty
|
|
786
|
+
flush. A cold-start Lambda flushing before it returns is exactly this case: the worker is
|
|
787
|
+
built when the first span *closes*, so inside the first traced call there is none.
|
|
788
|
+
|
|
789
|
+
Concurrent sweeps are serialized on ``_sweep_lock``. The detach is a load and a store with a
|
|
790
|
+
real gap between them, and two threads sharing one ``Span`` — which ``contextvars`` makes
|
|
791
|
+
ordinary — can both read the same buffer and deliver it twice: measured, all 8 events
|
|
792
|
+
duplicated with the window held open, and 9 of 25 runs with only a GIL yield between them.
|
|
793
|
+
Rarely preempted on today's build is not a guarantee, and the floor is ``>=3.12`` where a
|
|
794
|
+
free-threading build removes even that. A flush is not a hot path, so a single lock is the
|
|
795
|
+
right cost.
|
|
796
|
+
|
|
797
|
+
**The detach stays one statement, and the two orderings above are not in tension.** A draft
|
|
798
|
+
hoisted the load to the top of the loop so a test could park on it — which put
|
|
799
|
+
``_get_worker()``, and therefore ``Thread.start()``, *inside* the load-to-store gap: measured,
|
|
800
|
+
a sweep racing a close then delivered the whole batch twice, two ``span.end`` events among
|
|
801
|
+
them, in 67 of 100 unforced trials against 0 before.
|
|
802
|
+
|
|
803
|
+
One statement makes that gap **narrow, not closed**, and the difference matters. It compiles
|
|
804
|
+
to ``LOAD_ATTR … STORE_ATTR`` with no ``CALL`` between, so CPython's eval breaker never runs
|
|
805
|
+
there and today's GIL cannot switch inside it — 0 of 500 unforced trials. Forced with an
|
|
806
|
+
opcode-level preemption it reproduces 10 of 10, and a free-threaded build removes the
|
|
807
|
+
accident entirely while ``requires-python`` has no upper bound. So :func:`_flush` takes this
|
|
808
|
+
same lock rather than relying on the width of a window: that is the *detach-vs-detach* race,
|
|
809
|
+
and a process-global lock is the right instrument for it. The **append** race
|
|
810
|
+
(``api._log`` versus a detach) is a different window needing a per-span lock, and
|
|
811
|
+
``architecture.md`` §13 declines it on cost.
|
|
812
|
+
|
|
813
|
+
It reaches only the calling context's spans. ``contextvars`` offers no way to enumerate
|
|
814
|
+
another thread's or task's context, so a ``flush()`` in a handler that fanned out does not
|
|
815
|
+
reach what those tasks buffered.
|
|
816
|
+
|
|
817
|
+
Args:
|
|
818
|
+
None.
|
|
819
|
+
|
|
820
|
+
Returns:
|
|
821
|
+
None.
|
|
822
|
+
|
|
823
|
+
Raises:
|
|
824
|
+
Exception: Whatever building the worker raises. :func:`_flush_worker` guards it, because a
|
|
825
|
+
flush is the call most likely to be made in a ``finally``.
|
|
826
|
+
"""
|
|
827
|
+
with _sweep_lock:
|
|
828
|
+
for span in context._live_span_stack():
|
|
829
|
+
if not span.events:
|
|
830
|
+
span.swept = True
|
|
831
|
+
continue
|
|
832
|
+
worker = _get_worker()
|
|
833
|
+
backfill_baggage(span, context._live_baggage())
|
|
834
|
+
span.swept = True
|
|
835
|
+
buffered, span.events = span.events, []
|
|
836
|
+
worker.submit(buffered)
|
|
837
|
+
|
|
838
|
+
|
|
715
839
|
def _flush_worker(timeout: float | None = 5.0) -> FlushResult:
|
|
716
840
|
"""Drains the process worker without retiring it, backing ``flush()`` (SPEC-013 FR-003).
|
|
717
841
|
|
|
718
|
-
This deliberately does not call :func:`_get_worker
|
|
719
|
-
|
|
720
|
-
|
|
842
|
+
~~This deliberately does not call :func:`_get_worker`~~ — narrowed by SPEC-036 FR-001. The
|
|
843
|
+
refusal still holds for an *empty* flush: a process that never logged has nothing to drain,
|
|
844
|
+
and building a worker — with the thread and ``atexit`` registration that brings — in order to
|
|
845
|
+
flush nothing would be pure cost. What changed is that :func:`_sweep_open_spans`, which runs
|
|
846
|
+
first, does build one when it finds buffered events on an open span, because submitting them
|
|
847
|
+
into a worker that does not exist delivers nothing and still reports success.
|
|
721
848
|
|
|
722
849
|
Args:
|
|
723
850
|
timeout: Seconds to wait for the drain, or ``None`` to wait indefinitely.
|
|
724
851
|
|
|
725
852
|
Returns:
|
|
726
853
|
A :class:`FlushResult`, truthy when everything outstanding was delivered and when no
|
|
727
|
-
worker exists — a process that never logged has nothing to drain, so it has lost
|
|
728
|
-
|
|
854
|
+
worker exists — a process that never logged has nothing to drain, so it has lost nothing.
|
|
855
|
+
A sweep that could not hand its buffers over reports ``"abandoned"``, the existing token
|
|
856
|
+
for "this call did not deliver them" (SPEC-036 FR-001): the events are still on their open
|
|
857
|
+
spans and their close may yet carry them, but the caller asked *now*, and on the
|
|
858
|
+
cold-start path this exists for there may be no close — reporting success there is the
|
|
859
|
+
exact shape the spec was written to remove. The drain still runs, so whatever was
|
|
860
|
+
submitted before the failure is not held back by it.
|
|
729
861
|
|
|
730
862
|
Raises:
|
|
731
863
|
None. A flush is the call most likely to be made in a ``finally``, so the library must
|
|
732
864
|
never be the reason a caller's function fails; a failure is reported by the return
|
|
733
865
|
value instead (FR-003).
|
|
734
866
|
"""
|
|
867
|
+
swept = True
|
|
868
|
+
try:
|
|
869
|
+
_sweep_open_spans()
|
|
870
|
+
except Exception as exc:
|
|
871
|
+
_diag.absorbed("sweeping open spans for a flush", exc, "buffered events were not swept")
|
|
872
|
+
swept = False
|
|
735
873
|
worker = _worker
|
|
736
874
|
if worker is None:
|
|
737
|
-
return FlushResult(ok=True)
|
|
875
|
+
return FlushResult(ok=True) if swept else FlushResult(ok=False, reason="abandoned")
|
|
738
876
|
try:
|
|
739
|
-
|
|
877
|
+
if swept:
|
|
878
|
+
return worker.flush(timeout)
|
|
879
|
+
worker.flush(timeout)
|
|
740
880
|
except Exception:
|
|
741
881
|
return FlushResult(ok=False, reason="thread-died")
|
|
882
|
+
return FlushResult(ok=False, reason="abandoned")
|
|
742
883
|
|
|
743
884
|
|
|
744
885
|
def _note_orphan_loss() -> None:
|
|
@@ -919,6 +1060,14 @@ def _flush(span: Span) -> None:
|
|
|
919
1060
|
The late append is now landing in a buffer nothing will emit, which is *also* loss — that
|
|
920
1061
|
half is ``api._log``'s, keyed on :attr:`Span.closed`.
|
|
921
1062
|
|
|
1063
|
+
It takes ``_sweep_lock`` for the detach, because :func:`_sweep_open_spans` performs the same
|
|
1064
|
+
detach on the same attribute and a span can be swept and closed concurrently — measured, the
|
|
1065
|
+
whole batch delivered twice with two ``span.end`` events among them. The hold covers one
|
|
1066
|
+
statement and not the submit; ``Worker.submit`` is a ``put_nowait`` that never blocks, and the
|
|
1067
|
+
cost of the lock on the traced path measured within noise (+0.4% single-threaded, +0.9% across
|
|
1068
|
+
eight threads, 20,000 spans each). The **append** window this does not close is a different
|
|
1069
|
+
one, needs a per-span lock, and is declined in ``architecture.md`` §13.
|
|
1070
|
+
|
|
922
1071
|
Args:
|
|
923
1072
|
span: The finished span whose buffered events are submitted.
|
|
924
1073
|
|
|
@@ -928,7 +1077,8 @@ def _flush(span: Span) -> None:
|
|
|
928
1077
|
Raises:
|
|
929
1078
|
Exception: Whatever creating the worker or submitting raises; :func:`_end` is the guard.
|
|
930
1079
|
"""
|
|
931
|
-
|
|
1080
|
+
with _sweep_lock:
|
|
1081
|
+
events, span.events = span.events, []
|
|
932
1082
|
_get_worker().submit(events)
|
|
933
1083
|
|
|
934
1084
|
|
|
@@ -36,6 +36,18 @@ class Span:
|
|
|
36
36
|
span, so a fire-and-forget ``create_task`` can outlive its parent and append to a buffer
|
|
37
37
|
nothing will emit again. It is read by ``api._log`` at append time, which is the only place
|
|
38
38
|
that can notice: nothing in the library looks at a span after ``_close_span`` returns.
|
|
39
|
+
|
|
40
|
+
``swept`` marks a span whose buffered events an in-span ``flush()`` has already handed to the
|
|
41
|
+
worker (SPEC-036 FR-001). Nothing in the delivery path needs it — the sweep is correct without
|
|
42
|
+
it — but ``continue_trace`` does: ``_reparent_current_span`` adopts a context by rewriting the
|
|
43
|
+
events still *buffered* on the open root span, and swept events have left that buffer, so an
|
|
44
|
+
adoption after a sweep would leave one span carrying two trace ids. That is the SPEC-024
|
|
45
|
+
category, wrong data rather than lost data, so the flag lets the adoption refuse instead.
|
|
46
|
+
|
|
47
|
+
The guarantee is **single-threaded**. The flag is not a synchronization primitive:
|
|
48
|
+
``continue_trace`` reads it and then re-parents across an ordinary function call, so a sweep
|
|
49
|
+
arriving in that gap still splits the span. It is set before the detach so a concurrent reader
|
|
50
|
+
errs toward refusing, and the residual is recorded in ``architecture.md`` §13.
|
|
39
51
|
"""
|
|
40
52
|
|
|
41
53
|
trace_id: str
|
|
@@ -46,6 +58,7 @@ class Span:
|
|
|
46
58
|
defaults: dict[str, object] = field(default_factory=dict)
|
|
47
59
|
events: list[dict[str, object]] = field(default_factory=list)
|
|
48
60
|
closed: bool = False
|
|
61
|
+
swept: bool = False
|
|
49
62
|
|
|
50
63
|
|
|
51
64
|
def _iso_now() -> str:
|
|
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.dev68 → log_foundry-0.10.2.dev69}/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
|
|
File without changes
|