log-foundry 0.10.2.dev23__tar.gz → 0.10.2.dev24__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.dev23 → log_foundry-0.10.2.dev24}/PKG-INFO +28 -10
- {log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/README.md +27 -9
- {log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/pyproject.toml +1 -1
- {log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/src/log_foundry/sinks/_socket.py +5 -2
- {log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/src/log_foundry/sinks/base.py +9 -0
- {log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/src/log_foundry/sinks/clickhouse.py +29 -15
- {log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/src/log_foundry/sinks/elasticsearch.py +9 -6
- {log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/src/log_foundry/sinks/eventhubs.py +9 -7
- {log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/src/log_foundry/sinks/firehose.py +14 -11
- {log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/src/log_foundry/sinks/http.py +6 -3
- {log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/src/log_foundry/sinks/kafka.py +8 -3
- {log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/src/log_foundry/sinks/kinesis.py +14 -11
- {log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/src/log_foundry/sinks/mongodb.py +21 -8
- {log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/src/log_foundry/sinks/multi.py +6 -4
- {log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/src/log_foundry/sinks/nats.py +6 -2
- {log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/src/log_foundry/sinks/postgres.py +45 -13
- {log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/src/log_foundry/sinks/pubsub.py +8 -3
- {log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/src/log_foundry/sinks/rabbitmq.py +7 -6
- {log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/src/log_foundry/sinks/redis.py +7 -6
- {log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/src/log_foundry/sinks/sentry.py +11 -8
- {log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/src/log_foundry/sinks/sns.py +9 -7
- {log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/src/log_foundry/sinks/sqs.py +11 -8
- {log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/src/log_foundry/sinks/util.py +4 -1
- {log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/LICENSE +0 -0
- {log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/src/log_foundry/__init__.py +0 -0
- {log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/src/log_foundry/_diag.py +0 -0
- {log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/src/log_foundry/api.py +0 -0
- {log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/src/log_foundry/config.py +0 -0
- {log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/src/log_foundry/console.py +0 -0
- {log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/src/log_foundry/context.py +0 -0
- {log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/src/log_foundry/decorator.py +0 -0
- {log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/src/log_foundry/ids.py +0 -0
- {log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/src/log_foundry/model.py +0 -0
- {log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/src/log_foundry/py.typed +0 -0
- {log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/src/log_foundry/sanitize.py +0 -0
- {log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/src/log_foundry/sinks/__init__.py +0 -0
- {log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/src/log_foundry/sinks/_batch.py +0 -0
- {log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/src/log_foundry/sinks/_chunk.py +0 -0
- {log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/src/log_foundry/sinks/_retry.py +0 -0
- {log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/src/log_foundry/sinks/_time.py +0 -0
- {log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/src/log_foundry/sinks/callback.py +0 -0
- {log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/src/log_foundry/sinks/datadog.py +0 -0
- {log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/src/log_foundry/sinks/file.py +0 -0
- {log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/src/log_foundry/sinks/filtering.py +0 -0
- {log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/src/log_foundry/sinks/honeycomb.py +0 -0
- {log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/src/log_foundry/sinks/logging_sink.py +0 -0
- {log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/src/log_foundry/sinks/logstash.py +0 -0
- {log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/src/log_foundry/sinks/loki.py +0 -0
- {log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/src/log_foundry/sinks/newrelic.py +0 -0
- {log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/src/log_foundry/sinks/splunk.py +0 -0
- {log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/src/log_foundry/sinks/sqlite.py +0 -0
- {log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/src/log_foundry/sinks/stdout.py +0 -0
- {log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/src/log_foundry/sinks/syslog.py +0 -0
- {log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/src/log_foundry/sinks/transform.py +0 -0
- {log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/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.dev24
|
|
4
4
|
Summary: Generate logs for your console and JSON events for downstream consumption.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -669,7 +669,16 @@ in `CallbackSink`.
|
|
|
669
669
|
#### Writing your own sink
|
|
670
670
|
|
|
671
671
|
`Sink` is two required methods, `emit(batch)` and `close()`, plus two rules about *how* `emit`
|
|
672
|
-
fails. They are not stylistic — the library's whole
|
|
672
|
+
fails and one about *when* it is called. They are not stylistic — the library's whole
|
|
673
|
+
loss-reporting apparatus is built on them:
|
|
674
|
+
|
|
675
|
+
- **Tolerate concurrent calls.** `emit` may run on more than one thread at once, and `close` may
|
|
676
|
+
be called while an `emit` is in flight. The worker drains on its own thread, but a level call
|
|
677
|
+
made with no active span emits synchronously on the *caller's* thread — which is any thread of
|
|
678
|
+
your application. If your sink holds mutable transport state (a stream it rebinds, a socket it
|
|
679
|
+
reuses, a connection with transaction scope), guard it with a `threading.Lock` held for the
|
|
680
|
+
whole operation that assumes exclusivity. If it holds none, you need do nothing. The library
|
|
681
|
+
cannot serialize this for you: it does not own the calling thread.
|
|
673
682
|
|
|
674
683
|
- **Raise when you delivered none of the batch**, after your own retries are spent. That is the
|
|
675
684
|
signal the worker's bounded retry and `health().failed_batches` depend on, and the one case where
|
|
@@ -685,29 +694,38 @@ never engages, `failed_batches` stays at zero, and `flush()` returns `True` whil
|
|
|
685
694
|
lost.
|
|
686
695
|
|
|
687
696
|
Optionally add `losses()` to report what you absorbed. It must never raise and must be safe to call
|
|
688
|
-
while `emit` is running (`health()` is a poll)
|
|
697
|
+
while `emit` is running (`health()` is a poll) — which means the counters need their own lock, kept
|
|
698
|
+
separate from the transport one so a poll never waits on an in-flight send:
|
|
689
699
|
|
|
690
700
|
```python
|
|
701
|
+
import threading
|
|
691
702
|
from log_foundry.sinks.base import SinkDeliveryError, SinkLosses
|
|
692
703
|
|
|
693
704
|
class MySink:
|
|
694
705
|
def __init__(self) -> None:
|
|
695
706
|
self._dropped = self._failed = 0
|
|
707
|
+
self._lock = threading.Lock() # transport state
|
|
708
|
+
self._counter_lock = threading.Lock() # counters only, never held across I/O
|
|
696
709
|
|
|
697
710
|
def emit(self, batch: list[dict[str, object]]) -> None:
|
|
698
711
|
delivered = 0
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
712
|
+
with self._lock: # your connection, socket or stream
|
|
713
|
+
for chunk in self._chunks(batch):
|
|
714
|
+
if self._send(chunk): # your own bounded retry
|
|
715
|
+
delivered += len(chunk)
|
|
716
|
+
else:
|
|
717
|
+
with self._counter_lock: # transport -> counter, never the reverse
|
|
718
|
+
self._failed += len(chunk)
|
|
704
719
|
if batch and not delivered:
|
|
705
720
|
raise SinkDeliveryError(f"MySink delivered none of {len(batch)} event(s)")
|
|
706
721
|
|
|
707
722
|
def losses(self) -> SinkLosses:
|
|
708
|
-
|
|
723
|
+
with self._counter_lock: # both fields from one instant
|
|
724
|
+
return SinkLosses(dropped=self._dropped, failed=self._failed)
|
|
709
725
|
|
|
710
|
-
def close(self) -> None:
|
|
726
|
+
def close(self) -> None:
|
|
727
|
+
with self._lock: # never release under an active writer
|
|
728
|
+
...
|
|
711
729
|
```
|
|
712
730
|
|
|
713
731
|
`losses()` is optional and probed by name, so a sink written before it existed keeps working and
|
|
@@ -633,7 +633,16 @@ in `CallbackSink`.
|
|
|
633
633
|
#### Writing your own sink
|
|
634
634
|
|
|
635
635
|
`Sink` is two required methods, `emit(batch)` and `close()`, plus two rules about *how* `emit`
|
|
636
|
-
fails. They are not stylistic — the library's whole
|
|
636
|
+
fails and one about *when* it is called. They are not stylistic — the library's whole
|
|
637
|
+
loss-reporting apparatus is built on them:
|
|
638
|
+
|
|
639
|
+
- **Tolerate concurrent calls.** `emit` may run on more than one thread at once, and `close` may
|
|
640
|
+
be called while an `emit` is in flight. The worker drains on its own thread, but a level call
|
|
641
|
+
made with no active span emits synchronously on the *caller's* thread — which is any thread of
|
|
642
|
+
your application. If your sink holds mutable transport state (a stream it rebinds, a socket it
|
|
643
|
+
reuses, a connection with transaction scope), guard it with a `threading.Lock` held for the
|
|
644
|
+
whole operation that assumes exclusivity. If it holds none, you need do nothing. The library
|
|
645
|
+
cannot serialize this for you: it does not own the calling thread.
|
|
637
646
|
|
|
638
647
|
- **Raise when you delivered none of the batch**, after your own retries are spent. That is the
|
|
639
648
|
signal the worker's bounded retry and `health().failed_batches` depend on, and the one case where
|
|
@@ -649,29 +658,38 @@ never engages, `failed_batches` stays at zero, and `flush()` returns `True` whil
|
|
|
649
658
|
lost.
|
|
650
659
|
|
|
651
660
|
Optionally add `losses()` to report what you absorbed. It must never raise and must be safe to call
|
|
652
|
-
while `emit` is running (`health()` is a poll)
|
|
661
|
+
while `emit` is running (`health()` is a poll) — which means the counters need their own lock, kept
|
|
662
|
+
separate from the transport one so a poll never waits on an in-flight send:
|
|
653
663
|
|
|
654
664
|
```python
|
|
665
|
+
import threading
|
|
655
666
|
from log_foundry.sinks.base import SinkDeliveryError, SinkLosses
|
|
656
667
|
|
|
657
668
|
class MySink:
|
|
658
669
|
def __init__(self) -> None:
|
|
659
670
|
self._dropped = self._failed = 0
|
|
671
|
+
self._lock = threading.Lock() # transport state
|
|
672
|
+
self._counter_lock = threading.Lock() # counters only, never held across I/O
|
|
660
673
|
|
|
661
674
|
def emit(self, batch: list[dict[str, object]]) -> None:
|
|
662
675
|
delivered = 0
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
676
|
+
with self._lock: # your connection, socket or stream
|
|
677
|
+
for chunk in self._chunks(batch):
|
|
678
|
+
if self._send(chunk): # your own bounded retry
|
|
679
|
+
delivered += len(chunk)
|
|
680
|
+
else:
|
|
681
|
+
with self._counter_lock: # transport -> counter, never the reverse
|
|
682
|
+
self._failed += len(chunk)
|
|
668
683
|
if batch and not delivered:
|
|
669
684
|
raise SinkDeliveryError(f"MySink delivered none of {len(batch)} event(s)")
|
|
670
685
|
|
|
671
686
|
def losses(self) -> SinkLosses:
|
|
672
|
-
|
|
687
|
+
with self._counter_lock: # both fields from one instant
|
|
688
|
+
return SinkLosses(dropped=self._dropped, failed=self._failed)
|
|
673
689
|
|
|
674
|
-
def close(self) -> None:
|
|
690
|
+
def close(self) -> None:
|
|
691
|
+
with self._lock: # never release under an active writer
|
|
692
|
+
...
|
|
675
693
|
```
|
|
676
694
|
|
|
677
695
|
`losses()` is optional and probed by name, so a sink written before it existed keeps working and
|
|
@@ -108,6 +108,7 @@ class SocketTransport:
|
|
|
108
108
|
self._max_retries = max(max_retries, 0)
|
|
109
109
|
self._sock: socket.socket | None = None
|
|
110
110
|
self.failed = 0
|
|
111
|
+
self._counter_lock = threading.Lock()
|
|
111
112
|
self.stop_signal: threading.Event | None = None
|
|
112
113
|
self._lock = threading.Lock()
|
|
113
114
|
|
|
@@ -153,7 +154,8 @@ class SocketTransport:
|
|
|
153
154
|
Raises:
|
|
154
155
|
None.
|
|
155
156
|
"""
|
|
156
|
-
|
|
157
|
+
with self._counter_lock:
|
|
158
|
+
return SinkLosses(dropped=0, failed=self.failed)
|
|
157
159
|
|
|
158
160
|
def close(self) -> None:
|
|
159
161
|
"""Closes the held socket, if any (FR-005, FR-012).
|
|
@@ -205,7 +207,8 @@ class SocketTransport:
|
|
|
205
207
|
if attempt < self._max_retries:
|
|
206
208
|
wait(_BACKOFF_BASE * (2**attempt), self.stop_signal)
|
|
207
209
|
continue
|
|
208
|
-
self.
|
|
210
|
+
with self._counter_lock:
|
|
211
|
+
self.failed += 1
|
|
209
212
|
_diag.lost(
|
|
210
213
|
"message",
|
|
211
214
|
1,
|
|
@@ -57,6 +57,15 @@ class Sink(Protocol):
|
|
|
57
57
|
``None`` is the same answer as having no method at all, which is what keeps a third-party
|
|
58
58
|
sink written against the pre-SPEC-026 interface satisfying this one. :func:`read_losses` is
|
|
59
59
|
the probe.
|
|
60
|
+
|
|
61
|
+
"Safe to call during an emit" is a concurrency requirement once :meth:`emit` is (SPEC-028
|
|
62
|
+
FR-003). The shipped sinks keep their loss counters under a **dedicated** lock, separate
|
|
63
|
+
from whatever guards their transport: an increment is a read-modify-write that Python does
|
|
64
|
+
not promise is atomic, and the pair read must come from one instant rather than straddling
|
|
65
|
+
another thread's increments. It is deliberately not the transport lock — ``health()`` is
|
|
66
|
+
the call an operator makes when a destination is already hanging, and sharing one lock
|
|
67
|
+
would make that poll wait for an in-flight emit and its retry backoff. Where a sink holds
|
|
68
|
+
both, the order is always transport then counter, never the reverse.
|
|
60
69
|
"""
|
|
61
70
|
|
|
62
71
|
def emit(self, batch: list[dict[str, object]]) -> None:
|
|
@@ -3,10 +3,8 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
if TYPE_CHECKING:
|
|
9
|
-
import threading
|
|
6
|
+
import threading
|
|
7
|
+
from typing import Any
|
|
10
8
|
|
|
11
9
|
from log_foundry import _diag
|
|
12
10
|
from log_foundry.sinks._chunk import chunk_list, valid_identifier
|
|
@@ -42,6 +40,12 @@ class ClickHouseSink:
|
|
|
42
40
|
columns plus the full event as a ``String`` column, inserted in a single call per chunk. The
|
|
43
41
|
sink is write-only, and the worst-case delay (SPEC-027 FR-005) is ``max_retries``
|
|
44
42
|
interruptible waits per chunk, 0.7 s at the defaults.
|
|
43
|
+
|
|
44
|
+
The driver requirement satisfied (SPEC-028 FR-002): a ``clickhouse-connect`` client holds
|
|
45
|
+
per-session state across an insert and the project does not publish it as safe to share
|
|
46
|
+
between threads, so this sink serializes its use rather than assuming otherwise. A lock is
|
|
47
|
+
the conservative reading — one client per thread would be the alternative, and that is the
|
|
48
|
+
connection-pool design FR-002 puts out of scope.
|
|
45
49
|
"""
|
|
46
50
|
|
|
47
51
|
def __init__(
|
|
@@ -81,6 +85,8 @@ class ClickHouseSink:
|
|
|
81
85
|
self.stop_signal: threading.Event | None = None
|
|
82
86
|
self.failed = 0
|
|
83
87
|
self._closed = False
|
|
88
|
+
self._lock = threading.Lock()
|
|
89
|
+
self._counter_lock = threading.Lock()
|
|
84
90
|
self._owns_client = client is None
|
|
85
91
|
if client is None:
|
|
86
92
|
import clickhouse_connect # type: ignore[import-not-found]
|
|
@@ -93,6 +99,9 @@ class ClickHouseSink:
|
|
|
93
99
|
def losses(self) -> SinkLosses:
|
|
94
100
|
"""Reports rows in a chunk abandoned past the retry bound (SPEC-026 FR-002).
|
|
95
101
|
|
|
102
|
+
Reads under the counter lock rather than the emit lock (SPEC-028 FR-003), so a poll
|
|
103
|
+
never waits on an in-flight insert and its backoff.
|
|
104
|
+
|
|
96
105
|
Args:
|
|
97
106
|
None.
|
|
98
107
|
|
|
@@ -102,7 +111,8 @@ class ClickHouseSink:
|
|
|
102
111
|
Raises:
|
|
103
112
|
None.
|
|
104
113
|
"""
|
|
105
|
-
|
|
114
|
+
with self._counter_lock:
|
|
115
|
+
return SinkLosses(dropped=0, failed=self.failed)
|
|
106
116
|
|
|
107
117
|
def emit(self, batch: list[dict[str, object]]) -> None:
|
|
108
118
|
"""Inserts each chunk as one columnar call, retrying on failure (FR-002).
|
|
@@ -122,16 +132,18 @@ class ClickHouseSink:
|
|
|
122
132
|
if not batch:
|
|
123
133
|
return
|
|
124
134
|
chunks = inserted = 0
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
135
|
+
with self._lock:
|
|
136
|
+
for chunk in chunk_list(batch, self._chunk_size):
|
|
137
|
+
chunks += 1
|
|
138
|
+
inserted += self._insert([self._row(event) for event in chunk])
|
|
128
139
|
if chunks and not inserted:
|
|
129
140
|
raise SinkDeliveryError(f"ClickHouseSink inserted none of {chunks} chunk(s)")
|
|
130
141
|
|
|
131
142
|
def close(self) -> None:
|
|
132
143
|
"""Closes the client only if the sink owns it (FR-005).
|
|
133
144
|
|
|
134
|
-
Idempotent
|
|
145
|
+
Idempotent, and takes the emit lock so the client is never closed mid-insert
|
|
146
|
+
(SPEC-028 FR-002).
|
|
135
147
|
|
|
136
148
|
Args:
|
|
137
149
|
None.
|
|
@@ -142,11 +154,12 @@ class ClickHouseSink:
|
|
|
142
154
|
Raises:
|
|
143
155
|
Exception: Whatever the client raises on close.
|
|
144
156
|
"""
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
self.
|
|
149
|
-
|
|
157
|
+
with self._lock:
|
|
158
|
+
if self._closed:
|
|
159
|
+
return
|
|
160
|
+
if self._owns_client:
|
|
161
|
+
self.client.close()
|
|
162
|
+
self._closed = True
|
|
150
163
|
|
|
151
164
|
def _row(self, event: dict[str, object]) -> list[object]:
|
|
152
165
|
"""Builds one row: the extracted columns, then the whole event as JSON.
|
|
@@ -182,7 +195,8 @@ class ClickHouseSink:
|
|
|
182
195
|
if attempt < self.max_retries:
|
|
183
196
|
wait(_BACKOFF_BASE * (2**attempt), self.stop_signal)
|
|
184
197
|
continue
|
|
185
|
-
self.
|
|
198
|
+
with self._counter_lock:
|
|
199
|
+
self.failed += len(rows)
|
|
186
200
|
_diag.lost(
|
|
187
201
|
"row",
|
|
188
202
|
len(rows),
|
{log_foundry-0.10.2.dev23 → log_foundry-0.10.2.dev24}/src/log_foundry/sinks/elasticsearch.py
RENAMED
|
@@ -98,10 +98,11 @@ class ElasticsearchSink(HTTPSink):
|
|
|
98
98
|
Raises:
|
|
99
99
|
None.
|
|
100
100
|
"""
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
101
|
+
with self._counter_lock:
|
|
102
|
+
return SinkLosses(
|
|
103
|
+
dropped=self.dropped_oversized,
|
|
104
|
+
failed=self.failed + self.item_errors + self.dropped_unadjudicated,
|
|
105
|
+
)
|
|
105
106
|
|
|
106
107
|
def _parse_bulk_response(self, payload: bytes, sent: int) -> bool:
|
|
107
108
|
"""Counts rejected items and reports whether the response proves nothing was indexed.
|
|
@@ -139,7 +140,8 @@ class ElasticsearchSink(HTTPSink):
|
|
|
139
140
|
items = usable_results(data.get("items"))
|
|
140
141
|
errors = sum(1 for item in items if _has_error(item))
|
|
141
142
|
if len(items) != sent or not errors:
|
|
142
|
-
self.
|
|
143
|
+
with self._counter_lock:
|
|
144
|
+
self.dropped_unadjudicated += sent
|
|
143
145
|
_diag.lost(
|
|
144
146
|
"event",
|
|
145
147
|
sent,
|
|
@@ -148,7 +150,8 @@ class ElasticsearchSink(HTTPSink):
|
|
|
148
150
|
f"not retried",
|
|
149
151
|
)
|
|
150
152
|
return False
|
|
151
|
-
self.
|
|
153
|
+
with self._counter_lock:
|
|
154
|
+
self.item_errors += errors
|
|
152
155
|
_diag.lost("bulk item", errors, f"{type(self).__name__}, rejected by the server")
|
|
153
156
|
return errors == sent
|
|
154
157
|
|
|
@@ -3,10 +3,8 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
if TYPE_CHECKING:
|
|
9
|
-
import threading
|
|
6
|
+
import threading
|
|
7
|
+
from typing import Any
|
|
10
8
|
|
|
11
9
|
from log_foundry import _diag
|
|
12
10
|
from log_foundry.sinks._retry import wait
|
|
@@ -69,6 +67,7 @@ class AzureEventHubsSink:
|
|
|
69
67
|
self.stop_signal: threading.Event | None = None
|
|
70
68
|
self.failed = 0
|
|
71
69
|
self.dropped_oversized = 0
|
|
70
|
+
self._counter_lock = threading.Lock()
|
|
72
71
|
|
|
73
72
|
def losses(self) -> SinkLosses:
|
|
74
73
|
"""Reports oversized drops and events in a batch abandoned past the bound (FR-002).
|
|
@@ -82,7 +81,8 @@ class AzureEventHubsSink:
|
|
|
82
81
|
Raises:
|
|
83
82
|
None.
|
|
84
83
|
"""
|
|
85
|
-
|
|
84
|
+
with self._counter_lock:
|
|
85
|
+
return SinkLosses(dropped=self.dropped_oversized, failed=self.failed)
|
|
86
86
|
|
|
87
87
|
def emit(self, batch: list[dict[str, object]]) -> None:
|
|
88
88
|
"""Packs events into batches within the size limit and sends each (FR-009).
|
|
@@ -118,7 +118,8 @@ class AzureEventHubsSink:
|
|
|
118
118
|
delivered += self._send(current)
|
|
119
119
|
current = self.producer.create_batch()
|
|
120
120
|
if not _try_add(current, data):
|
|
121
|
-
self.
|
|
121
|
+
with self._counter_lock:
|
|
122
|
+
self.dropped_oversized += 1
|
|
122
123
|
_diag.lost("event", 1, "AzureEventHubsSink, too large for an empty 1 MB batch")
|
|
123
124
|
if len(current) > 0:
|
|
124
125
|
attempted += 1
|
|
@@ -167,7 +168,8 @@ class AzureEventHubsSink:
|
|
|
167
168
|
if attempt < self.max_retries:
|
|
168
169
|
wait(_BACKOFF_BASE * (2**attempt), self.stop_signal)
|
|
169
170
|
continue
|
|
170
|
-
self.
|
|
171
|
+
with self._counter_lock:
|
|
172
|
+
self.failed += len(event_batch)
|
|
171
173
|
_diag.lost(
|
|
172
174
|
"event",
|
|
173
175
|
len(event_batch),
|
|
@@ -3,10 +3,8 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
if TYPE_CHECKING:
|
|
9
|
-
import threading
|
|
6
|
+
import threading
|
|
7
|
+
from typing import Any
|
|
10
8
|
|
|
11
9
|
from log_foundry import _diag
|
|
12
10
|
from log_foundry.sinks._batch import adjudicate_positional, usable_results
|
|
@@ -67,6 +65,7 @@ class FirehoseSink:
|
|
|
67
65
|
self.failed = 0
|
|
68
66
|
self.dropped_oversized = 0
|
|
69
67
|
self.dropped_unadjudicated = 0
|
|
68
|
+
self._counter_lock = threading.Lock()
|
|
70
69
|
|
|
71
70
|
def emit(self, batch: list[dict[str, object]]) -> None:
|
|
72
71
|
"""Re-chunks to the request limits and sends each chunk, retrying failures (FR-004).
|
|
@@ -126,10 +125,11 @@ class FirehoseSink:
|
|
|
126
125
|
Raises:
|
|
127
126
|
None.
|
|
128
127
|
"""
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
128
|
+
with self._counter_lock:
|
|
129
|
+
return SinkLosses(
|
|
130
|
+
dropped=self.dropped_oversized,
|
|
131
|
+
failed=self.failed + self.dropped_unadjudicated,
|
|
132
|
+
)
|
|
133
133
|
|
|
134
134
|
def close(self) -> None:
|
|
135
135
|
"""Does nothing, since the sink buffers nothing internally (FR-001).
|
|
@@ -160,7 +160,8 @@ class FirehoseSink:
|
|
|
160
160
|
for event in batch:
|
|
161
161
|
data = json.dumps(event).encode("utf-8")
|
|
162
162
|
if len(data) > self.MAX_RECORD_BYTES:
|
|
163
|
-
self.
|
|
163
|
+
with self._counter_lock:
|
|
164
|
+
self.dropped_oversized += 1
|
|
164
165
|
_diag.lost(
|
|
165
166
|
"event",
|
|
166
167
|
1,
|
|
@@ -201,7 +202,8 @@ class FirehoseSink:
|
|
|
201
202
|
results = usable_results(response.get("RequestResponses"))
|
|
202
203
|
verdict = adjudicate_positional(records, results)
|
|
203
204
|
if verdict.unadjudicated:
|
|
204
|
-
self.
|
|
205
|
+
with self._counter_lock:
|
|
206
|
+
self.dropped_unadjudicated += verdict.unadjudicated
|
|
205
207
|
_diag.lost(
|
|
206
208
|
"record",
|
|
207
209
|
verdict.unadjudicated,
|
|
@@ -216,7 +218,8 @@ class FirehoseSink:
|
|
|
216
218
|
if attempt < self.max_retries:
|
|
217
219
|
wait(_BACKOFF_BASE * (2**attempt), self.stop_signal)
|
|
218
220
|
if attempt >= self.max_retries:
|
|
219
|
-
self.
|
|
221
|
+
with self._counter_lock:
|
|
222
|
+
self.failed += len(records)
|
|
220
223
|
_diag.lost(
|
|
221
224
|
"record",
|
|
222
225
|
len(records),
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import gzip as _gzip
|
|
6
6
|
import json
|
|
7
|
+
import threading
|
|
7
8
|
import urllib.error
|
|
8
9
|
import urllib.request
|
|
9
10
|
from base64 import b64encode
|
|
@@ -14,7 +15,6 @@ from log_foundry.sinks._retry import clamp_server_delay, wait
|
|
|
14
15
|
from log_foundry.sinks.base import SinkDeliveryError, SinkLosses
|
|
15
16
|
|
|
16
17
|
if TYPE_CHECKING:
|
|
17
|
-
import threading
|
|
18
18
|
from collections.abc import Callable
|
|
19
19
|
|
|
20
20
|
__all__ = ["HTTPSink", "merge_headers"]
|
|
@@ -129,6 +129,7 @@ class HTTPSink:
|
|
|
129
129
|
self._opener = opener if opener is not None else urllib.request.urlopen
|
|
130
130
|
self.failed = 0
|
|
131
131
|
self.dropped_oversized = 0
|
|
132
|
+
self._counter_lock = threading.Lock()
|
|
132
133
|
|
|
133
134
|
def emit(self, batch: list[dict[str, object]]) -> None:
|
|
134
135
|
"""Serializes the batch per the configured body format and POSTs it (FR-001).
|
|
@@ -161,7 +162,8 @@ class HTTPSink:
|
|
|
161
162
|
Raises:
|
|
162
163
|
None.
|
|
163
164
|
"""
|
|
164
|
-
|
|
165
|
+
with self._counter_lock:
|
|
166
|
+
return SinkLosses(dropped=self.dropped_oversized, failed=self.failed)
|
|
165
167
|
|
|
166
168
|
def close(self) -> None:
|
|
167
169
|
"""Does nothing, since ``urllib`` opens a fresh connection per request (FR-012).
|
|
@@ -377,7 +379,8 @@ class HTTPSink:
|
|
|
377
379
|
Raises:
|
|
378
380
|
SinkDeliveryError: Always.
|
|
379
381
|
"""
|
|
380
|
-
self.
|
|
382
|
+
with self._counter_lock:
|
|
383
|
+
self.failed += 1
|
|
381
384
|
detail = f"{type(self).__name__}, {self.max_retries + 1} attempt(s), {reason}"
|
|
382
385
|
_diag.lost("request", 1, detail)
|
|
383
386
|
raise SinkDeliveryError(detail)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
+
import threading
|
|
6
7
|
from typing import Any
|
|
7
8
|
|
|
8
9
|
from log_foundry import _diag
|
|
@@ -61,6 +62,7 @@ class KafkaSink:
|
|
|
61
62
|
self.key_field = key_field
|
|
62
63
|
self.failed = 0
|
|
63
64
|
self.rejected = 0
|
|
65
|
+
self._counter_lock = threading.Lock()
|
|
64
66
|
|
|
65
67
|
def losses(self) -> SinkLosses:
|
|
66
68
|
"""Reports refused and undelivered messages (SPEC-026 FR-002).
|
|
@@ -82,7 +84,8 @@ class KafkaSink:
|
|
|
82
84
|
Raises:
|
|
83
85
|
None.
|
|
84
86
|
"""
|
|
85
|
-
|
|
87
|
+
with self._counter_lock:
|
|
88
|
+
return SinkLosses(dropped=self.rejected, failed=self.failed)
|
|
86
89
|
|
|
87
90
|
def emit(self, batch: list[dict[str, object]]) -> None:
|
|
88
91
|
"""Produces one message per event and serves delivery callbacks without blocking.
|
|
@@ -108,7 +111,8 @@ class KafkaSink:
|
|
|
108
111
|
try:
|
|
109
112
|
self.producer.produce(self.topic, value=body, key=key, callback=self._on_delivery)
|
|
110
113
|
except Exception as err:
|
|
111
|
-
self.
|
|
114
|
+
with self._counter_lock:
|
|
115
|
+
self.rejected += 1
|
|
112
116
|
_diag.lost("message", 1, f"KafkaSink produce, {type(err).__name__}")
|
|
113
117
|
continue
|
|
114
118
|
accepted += 1
|
|
@@ -167,7 +171,8 @@ class KafkaSink:
|
|
|
167
171
|
None.
|
|
168
172
|
"""
|
|
169
173
|
if err is not None:
|
|
170
|
-
self.
|
|
174
|
+
with self._counter_lock:
|
|
175
|
+
self.failed += 1
|
|
171
176
|
_diag.lost("message", 1, f"KafkaSink delivery, {type(err).__name__}{_code(err)}")
|
|
172
177
|
|
|
173
178
|
|
|
@@ -3,10 +3,8 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
if TYPE_CHECKING:
|
|
9
|
-
import threading
|
|
6
|
+
import threading
|
|
7
|
+
from typing import Any
|
|
10
8
|
|
|
11
9
|
from log_foundry import _diag
|
|
12
10
|
from log_foundry.sinks._batch import adjudicate_positional, usable_results
|
|
@@ -76,6 +74,7 @@ class KinesisSink:
|
|
|
76
74
|
self.failed = 0
|
|
77
75
|
self.dropped_oversized = 0
|
|
78
76
|
self.dropped_unadjudicated = 0
|
|
77
|
+
self._counter_lock = threading.Lock()
|
|
79
78
|
|
|
80
79
|
def emit(self, batch: list[dict[str, object]]) -> None:
|
|
81
80
|
"""Re-chunks to the request limits and sends each chunk, retrying failures (FR-003).
|
|
@@ -135,10 +134,11 @@ class KinesisSink:
|
|
|
135
134
|
Raises:
|
|
136
135
|
None.
|
|
137
136
|
"""
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
137
|
+
with self._counter_lock:
|
|
138
|
+
return SinkLosses(
|
|
139
|
+
dropped=self.dropped_oversized,
|
|
140
|
+
failed=self.failed + self.dropped_unadjudicated,
|
|
141
|
+
)
|
|
142
142
|
|
|
143
143
|
def close(self) -> None:
|
|
144
144
|
"""Does nothing, since the sink buffers nothing internally (FR-001).
|
|
@@ -169,7 +169,8 @@ class KinesisSink:
|
|
|
169
169
|
for event in batch:
|
|
170
170
|
data = json.dumps(event).encode("utf-8")
|
|
171
171
|
if len(data) > self.MAX_RECORD_BYTES:
|
|
172
|
-
self.
|
|
172
|
+
with self._counter_lock:
|
|
173
|
+
self.dropped_oversized += 1
|
|
173
174
|
_diag.lost(
|
|
174
175
|
"event",
|
|
175
176
|
1,
|
|
@@ -209,7 +210,8 @@ class KinesisSink:
|
|
|
209
210
|
results = usable_results(response.get("Records"))
|
|
210
211
|
verdict = adjudicate_positional(records, results)
|
|
211
212
|
if verdict.unadjudicated:
|
|
212
|
-
self.
|
|
213
|
+
with self._counter_lock:
|
|
214
|
+
self.dropped_unadjudicated += verdict.unadjudicated
|
|
213
215
|
_diag.lost(
|
|
214
216
|
"record",
|
|
215
217
|
verdict.unadjudicated,
|
|
@@ -223,7 +225,8 @@ class KinesisSink:
|
|
|
223
225
|
if attempt < self.max_retries:
|
|
224
226
|
wait(_BACKOFF_BASE * (2**attempt), self.stop_signal)
|
|
225
227
|
if attempt >= self.max_retries:
|
|
226
|
-
self.
|
|
228
|
+
with self._counter_lock:
|
|
229
|
+
self.failed += len(records)
|
|
227
230
|
_diag.lost(
|
|
228
231
|
"record",
|
|
229
232
|
len(records),
|