log-foundry 0.10.2.dev22__tar.gz → 0.10.2.dev23__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.dev22 → log_foundry-0.10.2.dev23}/PKG-INFO +1 -1
- {log_foundry-0.10.2.dev22 → log_foundry-0.10.2.dev23}/pyproject.toml +1 -1
- {log_foundry-0.10.2.dev22 → log_foundry-0.10.2.dev23}/src/log_foundry/sinks/_socket.py +25 -10
- {log_foundry-0.10.2.dev22 → log_foundry-0.10.2.dev23}/src/log_foundry/sinks/base.py +19 -0
- {log_foundry-0.10.2.dev22 → log_foundry-0.10.2.dev23}/src/log_foundry/sinks/file.py +56 -23
- {log_foundry-0.10.2.dev22 → log_foundry-0.10.2.dev23}/src/log_foundry/sinks/sqlite.py +26 -13
- {log_foundry-0.10.2.dev22 → log_foundry-0.10.2.dev23}/LICENSE +0 -0
- {log_foundry-0.10.2.dev22 → log_foundry-0.10.2.dev23}/README.md +0 -0
- {log_foundry-0.10.2.dev22 → log_foundry-0.10.2.dev23}/src/log_foundry/__init__.py +0 -0
- {log_foundry-0.10.2.dev22 → log_foundry-0.10.2.dev23}/src/log_foundry/_diag.py +0 -0
- {log_foundry-0.10.2.dev22 → log_foundry-0.10.2.dev23}/src/log_foundry/api.py +0 -0
- {log_foundry-0.10.2.dev22 → log_foundry-0.10.2.dev23}/src/log_foundry/config.py +0 -0
- {log_foundry-0.10.2.dev22 → log_foundry-0.10.2.dev23}/src/log_foundry/console.py +0 -0
- {log_foundry-0.10.2.dev22 → log_foundry-0.10.2.dev23}/src/log_foundry/context.py +0 -0
- {log_foundry-0.10.2.dev22 → log_foundry-0.10.2.dev23}/src/log_foundry/decorator.py +0 -0
- {log_foundry-0.10.2.dev22 → log_foundry-0.10.2.dev23}/src/log_foundry/ids.py +0 -0
- {log_foundry-0.10.2.dev22 → log_foundry-0.10.2.dev23}/src/log_foundry/model.py +0 -0
- {log_foundry-0.10.2.dev22 → log_foundry-0.10.2.dev23}/src/log_foundry/py.typed +0 -0
- {log_foundry-0.10.2.dev22 → log_foundry-0.10.2.dev23}/src/log_foundry/sanitize.py +0 -0
- {log_foundry-0.10.2.dev22 → log_foundry-0.10.2.dev23}/src/log_foundry/sinks/__init__.py +0 -0
- {log_foundry-0.10.2.dev22 → log_foundry-0.10.2.dev23}/src/log_foundry/sinks/_batch.py +0 -0
- {log_foundry-0.10.2.dev22 → log_foundry-0.10.2.dev23}/src/log_foundry/sinks/_chunk.py +0 -0
- {log_foundry-0.10.2.dev22 → log_foundry-0.10.2.dev23}/src/log_foundry/sinks/_retry.py +0 -0
- {log_foundry-0.10.2.dev22 → log_foundry-0.10.2.dev23}/src/log_foundry/sinks/_time.py +0 -0
- {log_foundry-0.10.2.dev22 → log_foundry-0.10.2.dev23}/src/log_foundry/sinks/callback.py +0 -0
- {log_foundry-0.10.2.dev22 → log_foundry-0.10.2.dev23}/src/log_foundry/sinks/clickhouse.py +0 -0
- {log_foundry-0.10.2.dev22 → log_foundry-0.10.2.dev23}/src/log_foundry/sinks/datadog.py +0 -0
- {log_foundry-0.10.2.dev22 → log_foundry-0.10.2.dev23}/src/log_foundry/sinks/elasticsearch.py +0 -0
- {log_foundry-0.10.2.dev22 → log_foundry-0.10.2.dev23}/src/log_foundry/sinks/eventhubs.py +0 -0
- {log_foundry-0.10.2.dev22 → log_foundry-0.10.2.dev23}/src/log_foundry/sinks/filtering.py +0 -0
- {log_foundry-0.10.2.dev22 → log_foundry-0.10.2.dev23}/src/log_foundry/sinks/firehose.py +0 -0
- {log_foundry-0.10.2.dev22 → log_foundry-0.10.2.dev23}/src/log_foundry/sinks/honeycomb.py +0 -0
- {log_foundry-0.10.2.dev22 → log_foundry-0.10.2.dev23}/src/log_foundry/sinks/http.py +0 -0
- {log_foundry-0.10.2.dev22 → log_foundry-0.10.2.dev23}/src/log_foundry/sinks/kafka.py +0 -0
- {log_foundry-0.10.2.dev22 → log_foundry-0.10.2.dev23}/src/log_foundry/sinks/kinesis.py +0 -0
- {log_foundry-0.10.2.dev22 → log_foundry-0.10.2.dev23}/src/log_foundry/sinks/logging_sink.py +0 -0
- {log_foundry-0.10.2.dev22 → log_foundry-0.10.2.dev23}/src/log_foundry/sinks/logstash.py +0 -0
- {log_foundry-0.10.2.dev22 → log_foundry-0.10.2.dev23}/src/log_foundry/sinks/loki.py +0 -0
- {log_foundry-0.10.2.dev22 → log_foundry-0.10.2.dev23}/src/log_foundry/sinks/mongodb.py +0 -0
- {log_foundry-0.10.2.dev22 → log_foundry-0.10.2.dev23}/src/log_foundry/sinks/multi.py +0 -0
- {log_foundry-0.10.2.dev22 → log_foundry-0.10.2.dev23}/src/log_foundry/sinks/nats.py +0 -0
- {log_foundry-0.10.2.dev22 → log_foundry-0.10.2.dev23}/src/log_foundry/sinks/newrelic.py +0 -0
- {log_foundry-0.10.2.dev22 → log_foundry-0.10.2.dev23}/src/log_foundry/sinks/postgres.py +0 -0
- {log_foundry-0.10.2.dev22 → log_foundry-0.10.2.dev23}/src/log_foundry/sinks/pubsub.py +0 -0
- {log_foundry-0.10.2.dev22 → log_foundry-0.10.2.dev23}/src/log_foundry/sinks/rabbitmq.py +0 -0
- {log_foundry-0.10.2.dev22 → log_foundry-0.10.2.dev23}/src/log_foundry/sinks/redis.py +0 -0
- {log_foundry-0.10.2.dev22 → log_foundry-0.10.2.dev23}/src/log_foundry/sinks/sentry.py +0 -0
- {log_foundry-0.10.2.dev22 → log_foundry-0.10.2.dev23}/src/log_foundry/sinks/sns.py +0 -0
- {log_foundry-0.10.2.dev22 → log_foundry-0.10.2.dev23}/src/log_foundry/sinks/splunk.py +0 -0
- {log_foundry-0.10.2.dev22 → log_foundry-0.10.2.dev23}/src/log_foundry/sinks/sqs.py +0 -0
- {log_foundry-0.10.2.dev22 → log_foundry-0.10.2.dev23}/src/log_foundry/sinks/stdout.py +0 -0
- {log_foundry-0.10.2.dev22 → log_foundry-0.10.2.dev23}/src/log_foundry/sinks/syslog.py +0 -0
- {log_foundry-0.10.2.dev22 → log_foundry-0.10.2.dev23}/src/log_foundry/sinks/transform.py +0 -0
- {log_foundry-0.10.2.dev22 → log_foundry-0.10.2.dev23}/src/log_foundry/sinks/util.py +0 -0
- {log_foundry-0.10.2.dev22 → log_foundry-0.10.2.dev23}/src/log_foundry/worker.py +0 -0
|
@@ -3,10 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import socket
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
if TYPE_CHECKING:
|
|
9
|
-
import threading
|
|
6
|
+
import threading
|
|
10
7
|
|
|
11
8
|
from log_foundry import _diag
|
|
12
9
|
from log_foundry.sinks._retry import wait
|
|
@@ -64,6 +61,14 @@ class SocketTransport:
|
|
|
64
61
|
100-message batch against a dead destination is roughly 70 s of backoff on the single drain
|
|
65
62
|
thread at the defaults. The wait is interruptible, so ``shutdown()`` cuts it short.
|
|
66
63
|
|
|
64
|
+
Sends are serialized on a lock (SPEC-028 FR-002). One TCP connection is shared by every
|
|
65
|
+
caller, and ``sendall`` gives no atomicity against a concurrent one: two interleaved calls
|
|
66
|
+
splice their bytes into the stream, which turns octet-counted syslog framing into a sequence
|
|
67
|
+
the receiver cannot resynchronize — it reads the next frame's length from the middle of the
|
|
68
|
+
previous frame's payload and is lost for the life of the connection. That the lock is held
|
|
69
|
+
across the backoff waits is deliberate; they are interruptible, so a ``shutdown()`` releases
|
|
70
|
+
it promptly.
|
|
71
|
+
|
|
67
72
|
Attributes:
|
|
68
73
|
failed: Messages abandoned past the reconnect-retry bound.
|
|
69
74
|
"""
|
|
@@ -104,10 +109,15 @@ class SocketTransport:
|
|
|
104
109
|
self._sock: socket.socket | None = None
|
|
105
110
|
self.failed = 0
|
|
106
111
|
self.stop_signal: threading.Event | None = None
|
|
112
|
+
self._lock = threading.Lock()
|
|
107
113
|
|
|
108
114
|
def send_all(self, messages: list[bytes]) -> None:
|
|
109
115
|
"""Sends each pre-framed message, reconnecting on error (FR-005, FR-006).
|
|
110
116
|
|
|
117
|
+
The lock spans the whole call, not each message: it also guards ``_sock``, which a
|
|
118
|
+
reconnect rebinds, so releasing between messages would let another thread send on a
|
|
119
|
+
socket this one is about to reset (SPEC-028 FR-002).
|
|
120
|
+
|
|
111
121
|
Args:
|
|
112
122
|
messages: The exact bytes to put on the wire, one call per message.
|
|
113
123
|
|
|
@@ -121,10 +131,11 @@ class SocketTransport:
|
|
|
121
131
|
would re-send the messages that already landed, and an empty call is a no-op rather
|
|
122
132
|
than a total failure.
|
|
123
133
|
"""
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
134
|
+
with self._lock:
|
|
135
|
+
delivered = 0
|
|
136
|
+
for message in messages:
|
|
137
|
+
if self._send_one(message):
|
|
138
|
+
delivered += 1
|
|
128
139
|
if messages and delivered == 0:
|
|
129
140
|
raise SinkDeliveryError(
|
|
130
141
|
f"SocketTransport delivered none of {len(messages)} message(s)"
|
|
@@ -147,7 +158,10 @@ class SocketTransport:
|
|
|
147
158
|
def close(self) -> None:
|
|
148
159
|
"""Closes the held socket, if any (FR-005, FR-012).
|
|
149
160
|
|
|
150
|
-
Idempotent
|
|
161
|
+
Idempotent, and takes the send lock so it never closes the socket out from under an
|
|
162
|
+
in-flight ``send_all`` (SPEC-028 FR-002). ``_reset`` does not take the lock itself,
|
|
163
|
+
because ``_send_one`` calls it while ``send_all`` already holds one — the lock is
|
|
164
|
+
deliberately not re-entrant, so that path must stay the only unlocked caller.
|
|
151
165
|
|
|
152
166
|
Args:
|
|
153
167
|
None.
|
|
@@ -158,7 +172,8 @@ class SocketTransport:
|
|
|
158
172
|
Raises:
|
|
159
173
|
None.
|
|
160
174
|
"""
|
|
161
|
-
self.
|
|
175
|
+
with self._lock:
|
|
176
|
+
self._reset()
|
|
162
177
|
|
|
163
178
|
def _send_one(self, message: bytes) -> bool:
|
|
164
179
|
"""Sends one message within the retry bound.
|
|
@@ -62,6 +62,18 @@ class Sink(Protocol):
|
|
|
62
62
|
def emit(self, batch: list[dict[str, object]]) -> None:
|
|
63
63
|
"""Ships a batch of serialized event dicts.
|
|
64
64
|
|
|
65
|
+
**May be called concurrently from more than one thread, and must tolerate it**
|
|
66
|
+
(SPEC-028 FR-001). The background worker drains on its own thread, and a level call
|
|
67
|
+
made with no active span emits synchronously on the *caller's* thread (arch §12), which
|
|
68
|
+
may be any of the application's — an audit observed one sink object entered by two
|
|
69
|
+
application threads and the worker at once. An implementation holding mutable transport
|
|
70
|
+
state, such as a stream it rebinds, a reused socket or a connection with transaction
|
|
71
|
+
scope, must serialize access to it for the whole span of the operation that assumes
|
|
72
|
+
exclusivity. One that holds no such state need do nothing.
|
|
73
|
+
|
|
74
|
+
This is a requirement on implementations, not a promise the library serializes on their
|
|
75
|
+
behalf. It cannot: the orphan path runs on a thread the library does not own.
|
|
76
|
+
|
|
65
77
|
Raise when the batch delivered nothing and it was non-empty — the worker's bounded
|
|
66
78
|
retry and ``health().failed_batches`` depend on that signal, and a retry there cannot
|
|
67
79
|
duplicate anything (SPEC-026 FR-001). Raise after the sink's own retries are spent, so
|
|
@@ -88,6 +100,13 @@ class Sink(Protocol):
|
|
|
88
100
|
def close(self) -> None:
|
|
89
101
|
"""Flushes and releases any resources.
|
|
90
102
|
|
|
103
|
+
**May be called while an ``emit`` is in flight on another thread** (SPEC-028 FR-001).
|
|
104
|
+
It must therefore either wait for that emit or become a no-op, and must never release a
|
|
105
|
+
resource a concurrent ``emit`` is about to use — a half-released transport is worse than
|
|
106
|
+
an unreleased one, because the emit then fails against a closed handle instead of
|
|
107
|
+
succeeding. Taking the same lock ``emit`` takes satisfies this; so does an idempotent
|
|
108
|
+
guard checked under that lock.
|
|
109
|
+
|
|
91
110
|
Args:
|
|
92
111
|
None.
|
|
93
112
|
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
6
|
import os
|
|
7
|
+
import threading
|
|
7
8
|
import time
|
|
8
9
|
from typing import TextIO
|
|
9
10
|
|
|
@@ -22,8 +23,12 @@ class FileSink:
|
|
|
22
23
|
|
|
23
24
|
Not every deployment ships to a cloud queue; local dev, debugging, air-gapped hosts and
|
|
24
25
|
simple archival just want events on the local disk. Writes are synchronous stdlib calls
|
|
25
|
-
only
|
|
26
|
-
|
|
26
|
+
only.
|
|
27
|
+
|
|
28
|
+
Writers within the process are serialized on a lock, because ``emit`` may be called
|
|
29
|
+
concurrently (SPEC-028 FR-002) — this module claimed a single worker thread until that spec
|
|
30
|
+
measured the orphan path emitting on application threads at the same time. Cross-*process*
|
|
31
|
+
coordination remains out of scope: two processes appending to one path are on their own.
|
|
27
32
|
"""
|
|
28
33
|
|
|
29
34
|
def __init__(self, path: str, *, encoding: str = "utf-8") -> None:
|
|
@@ -47,10 +52,16 @@ class FileSink:
|
|
|
47
52
|
self._encoding = encoding
|
|
48
53
|
self._stream: TextIO = open(path, "a", encoding=encoding)
|
|
49
54
|
self._closed = False
|
|
55
|
+
self._lock = threading.Lock()
|
|
50
56
|
|
|
51
57
|
def emit(self, batch: list[dict[str, object]]) -> None:
|
|
52
58
|
"""Writes every event as one newline-terminated ``json.dumps`` line, then flushes.
|
|
53
59
|
|
|
60
|
+
The lock covers the whole batch rather than each line (SPEC-028 FR-002). A text stream
|
|
61
|
+
does not promise that one ``write`` is atomic against another, so per-line locking could
|
|
62
|
+
still interleave two events' bytes; batch-wide locking also keeps a batch contiguous in
|
|
63
|
+
the file, which is what makes the output readable.
|
|
64
|
+
|
|
54
65
|
Args:
|
|
55
66
|
batch: The events to write.
|
|
56
67
|
|
|
@@ -60,13 +71,17 @@ class FileSink:
|
|
|
60
71
|
Raises:
|
|
61
72
|
OSError: If the write or flush fails (FR-001).
|
|
62
73
|
"""
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
74
|
+
with self._lock:
|
|
75
|
+
for event in batch:
|
|
76
|
+
self._stream.write(json.dumps(event) + "\n")
|
|
77
|
+
self._stream.flush()
|
|
66
78
|
|
|
67
79
|
def close(self) -> None:
|
|
68
80
|
"""Flushes and closes the file handle, with a second call a no-op (FR-001).
|
|
69
81
|
|
|
82
|
+
Taking the same lock ``emit`` takes means a close waits for an in-flight write rather
|
|
83
|
+
than pulling the stream out from under it (SPEC-028 FR-002).
|
|
84
|
+
|
|
70
85
|
Args:
|
|
71
86
|
None.
|
|
72
87
|
|
|
@@ -76,11 +91,12 @@ class FileSink:
|
|
|
76
91
|
Raises:
|
|
77
92
|
OSError: If the flush or close fails.
|
|
78
93
|
"""
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
94
|
+
with self._lock:
|
|
95
|
+
if self._closed:
|
|
96
|
+
return
|
|
97
|
+
self._stream.flush()
|
|
98
|
+
self._stream.close()
|
|
99
|
+
self._closed = True
|
|
84
100
|
|
|
85
101
|
|
|
86
102
|
class RotatingFileSink:
|
|
@@ -95,6 +111,11 @@ class RotatingFileSink:
|
|
|
95
111
|
count, and opens a fresh active file — a backup count of zero keeps none, simply replacing
|
|
96
112
|
the active file. No event is lost across a rotation, because the rotate happens before the
|
|
97
113
|
pending event is written and the event lands in the fresh file.
|
|
114
|
+
|
|
115
|
+
A rotation rebinds the active stream, so it is the sink where concurrent writers did real
|
|
116
|
+
damage: a second thread mid-``emit`` could write to the handle rotation had just closed, or
|
|
117
|
+
to the pre-rotation file it had already renamed away. Both are serialized on a lock
|
|
118
|
+
(SPEC-028 FR-002).
|
|
98
119
|
"""
|
|
99
120
|
|
|
100
121
|
def __init__(
|
|
@@ -136,10 +157,16 @@ class RotatingFileSink:
|
|
|
136
157
|
self._size = os.path.getsize(path) if os.path.exists(path) else 0
|
|
137
158
|
self._next_rollover = self._schedule_next()
|
|
138
159
|
self._closed = False
|
|
160
|
+
self._lock = threading.Lock()
|
|
139
161
|
|
|
140
162
|
def emit(self, batch: list[dict[str, object]]) -> None:
|
|
141
163
|
"""Appends each event, rotating first whenever a size or time trigger fires (FR-002).
|
|
142
164
|
|
|
165
|
+
The lock spans the whole batch, so the decide-rotate-write-account sequence is
|
|
166
|
+
indivisible (SPEC-028 FR-002). Guarding only ``_rotate`` would not be enough: the
|
|
167
|
+
``_should_rotate`` check and the write that follows it must see the same stream, or a
|
|
168
|
+
rotation between them sends the line to a closed handle.
|
|
169
|
+
|
|
143
170
|
Args:
|
|
144
171
|
batch: The events to write.
|
|
145
172
|
|
|
@@ -149,18 +176,23 @@ class RotatingFileSink:
|
|
|
149
176
|
Raises:
|
|
150
177
|
OSError: If a write, flush or rotation fails.
|
|
151
178
|
"""
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
self.
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
179
|
+
with self._lock:
|
|
180
|
+
for event in batch:
|
|
181
|
+
line = json.dumps(event) + "\n"
|
|
182
|
+
data = len(line.encode(self._encoding))
|
|
183
|
+
if self._should_rotate(data):
|
|
184
|
+
self._rotate()
|
|
185
|
+
self._stream.write(line)
|
|
186
|
+
self._size += data
|
|
187
|
+
self._stream.flush()
|
|
160
188
|
|
|
161
189
|
def close(self) -> None:
|
|
162
190
|
"""Flushes and closes the active handle, with a second call a no-op (FR-002).
|
|
163
191
|
|
|
192
|
+
Taking the same lock ``emit`` takes means a close waits for an in-flight write, and in
|
|
193
|
+
particular never lands between a rotation and the write it was making room for
|
|
194
|
+
(SPEC-028 FR-002).
|
|
195
|
+
|
|
164
196
|
Args:
|
|
165
197
|
None.
|
|
166
198
|
|
|
@@ -170,11 +202,12 @@ class RotatingFileSink:
|
|
|
170
202
|
Raises:
|
|
171
203
|
OSError: If the flush or close fails.
|
|
172
204
|
"""
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
205
|
+
with self._lock:
|
|
206
|
+
if self._closed:
|
|
207
|
+
return
|
|
208
|
+
self._stream.flush()
|
|
209
|
+
self._stream.close()
|
|
210
|
+
self._closed = True
|
|
178
211
|
|
|
179
212
|
@staticmethod
|
|
180
213
|
def _rollover_seconds(when: str | None, interval: int) -> float | None:
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
6
|
import sqlite3
|
|
7
|
+
import threading
|
|
7
8
|
|
|
8
9
|
from log_foundry.sinks._chunk import valid_identifier
|
|
9
10
|
|
|
@@ -18,8 +19,14 @@ class SQLiteSink:
|
|
|
18
19
|
For local dev, debugging, air-gapped hosts or simple archival, an embedded SQLite file is a
|
|
19
20
|
durable sink you can open later and query with plain SQL. Each event is stored as its full
|
|
20
21
|
JSON — the source of truth — plus a few columns projected out for cheap filtering, which are
|
|
21
|
-
``NULL`` when absent. Standard library only
|
|
22
|
-
|
|
22
|
+
``NULL`` when absent. Standard library only.
|
|
23
|
+
|
|
24
|
+
The driver requirement satisfied (SPEC-028 FR-002): ``check_same_thread=False`` switches off
|
|
25
|
+
``sqlite3``'s own same-thread guard, and ``with connection`` is a transaction scope on the
|
|
26
|
+
shared connection rather than a per-caller one — two threads inside it at once share a single
|
|
27
|
+
implicit transaction, so one thread's rollback discards rows the other had already inserted.
|
|
28
|
+
A lock restores the one-writer-at-a-time the guard used to enforce. Cross-*process* writers
|
|
29
|
+
to one database file remain out of scope.
|
|
23
30
|
"""
|
|
24
31
|
|
|
25
32
|
def __init__(
|
|
@@ -33,8 +40,9 @@ class SQLiteSink:
|
|
|
33
40
|
"""Connects to the database and, by default, provisions the schema.
|
|
34
41
|
|
|
35
42
|
The connection is opened with ``check_same_thread=False``: the background worker is a
|
|
36
|
-
different thread from the one that ran ``configure()
|
|
37
|
-
|
|
43
|
+
different thread from the one that ran ``configure()``, so SQLite's same-thread guard
|
|
44
|
+
would reject every insert. It is not the sole writer — the orphan path emits on the
|
|
45
|
+
caller's thread (SPEC-028) — which is why the lock below replaces what the guard gave up.
|
|
38
46
|
|
|
39
47
|
Args:
|
|
40
48
|
database: The database file to open, ignored when a connection is injected.
|
|
@@ -61,6 +69,7 @@ class SQLiteSink:
|
|
|
61
69
|
else sqlite3.connect(database, check_same_thread=False)
|
|
62
70
|
)
|
|
63
71
|
self._closed = False
|
|
72
|
+
self._lock = threading.Lock()
|
|
64
73
|
if create_table:
|
|
65
74
|
self._ensure_schema()
|
|
66
75
|
|
|
@@ -68,7 +77,9 @@ class SQLiteSink:
|
|
|
68
77
|
"""Inserts every event in one transaction (FR-003).
|
|
69
78
|
|
|
70
79
|
``with connection`` opens a transaction and commits on success or rolls back on error,
|
|
71
|
-
so the whole batch lands atomically.
|
|
80
|
+
so the whole batch lands atomically. The lock holds that transaction to one thread
|
|
81
|
+
(SPEC-028 FR-002); the row-building above it needs no protection, but sits inside for
|
|
82
|
+
simplicity and costs nothing an uncontended lock does not.
|
|
72
83
|
|
|
73
84
|
Args:
|
|
74
85
|
batch: The events to insert.
|
|
@@ -84,7 +95,7 @@ class SQLiteSink:
|
|
|
84
95
|
]
|
|
85
96
|
placeholders = ", ".join("?" * (len(_COLUMNS) + 1))
|
|
86
97
|
columns = ", ".join((*_COLUMNS, "event"))
|
|
87
|
-
with self._conn:
|
|
98
|
+
with self._lock, self._conn:
|
|
88
99
|
self._conn.executemany(
|
|
89
100
|
f'INSERT INTO "{self._table}" ({columns}) VALUES ({placeholders})', rows
|
|
90
101
|
)
|
|
@@ -92,7 +103,8 @@ class SQLiteSink:
|
|
|
92
103
|
def close(self) -> None:
|
|
93
104
|
"""Commits pending work and closes only a connection the sink owns (FR-003).
|
|
94
105
|
|
|
95
|
-
Idempotent
|
|
106
|
+
Idempotent, and takes the emit lock so it never closes the connection out from under an
|
|
107
|
+
in-flight transaction (SPEC-028 FR-002).
|
|
96
108
|
|
|
97
109
|
Args:
|
|
98
110
|
None.
|
|
@@ -103,12 +115,13 @@ class SQLiteSink:
|
|
|
103
115
|
Raises:
|
|
104
116
|
sqlite3.Error: If the commit or close fails.
|
|
105
117
|
"""
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
self.
|
|
111
|
-
|
|
118
|
+
with self._lock:
|
|
119
|
+
if self._closed:
|
|
120
|
+
return
|
|
121
|
+
self._conn.commit()
|
|
122
|
+
if self._owns_connection:
|
|
123
|
+
self._conn.close()
|
|
124
|
+
self._closed = True
|
|
112
125
|
|
|
113
126
|
def _ensure_schema(self) -> None:
|
|
114
127
|
"""Idempotently creates the target table.
|
|
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.dev22 → log_foundry-0.10.2.dev23}/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
|