fluidattacks_core_logging 12.0.0__tar.gz → 12.2.0__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.
- fluidattacks_core_logging-12.2.0/.envrc +1 -0
- fluidattacks_core_logging-12.2.0/.gitignore +15 -0
- {fluidattacks_core_logging-12.0.0 → fluidattacks_core_logging-12.2.0}/PKG-INFO +1 -1
- {fluidattacks_core_logging-12.0.0 → fluidattacks_core_logging-12.2.0}/fluidattacks_core/logging/__init__.py +15 -0
- fluidattacks_core_logging-12.2.0/fluidattacks_core/logging/capture.py +57 -0
- {fluidattacks_core_logging-12.0.0 → fluidattacks_core_logging-12.2.0}/fluidattacks_core/logging/handlers.py +29 -5
- {fluidattacks_core_logging-12.0.0 → fluidattacks_core_logging-12.2.0}/import-linter.cfg +2 -10
- {fluidattacks_core_logging-12.0.0 → fluidattacks_core_logging-12.2.0}/pyproject.toml +1 -1
- fluidattacks_core_logging-12.2.0/tests/test_capture.py +69 -0
- {fluidattacks_core_logging-12.0.0 → fluidattacks_core_logging-12.2.0}/tests/test_logging.py +166 -0
- {fluidattacks_core_logging-12.0.0 → fluidattacks_core_logging-12.2.0}/uv.lock +1 -1
- fluidattacks_core_logging-12.0.0/.envrc +0 -1
- fluidattacks_core_logging-12.0.0/.gitignore +0 -5
- fluidattacks_core_logging-12.0.0/build/default.nix +0 -19
- fluidattacks_core_logging-12.0.0/build/filter.nix +0 -13
- fluidattacks_core_logging-12.0.0/flake.lock +0 -303
- fluidattacks_core_logging-12.0.0/flake.nix +0 -29
- {fluidattacks_core_logging-12.0.0 → fluidattacks_core_logging-12.2.0}/fluidattacks_core/logging/filters.py +0 -0
- {fluidattacks_core_logging-12.0.0 → fluidattacks_core_logging-12.2.0}/fluidattacks_core/logging/formatters.py +0 -0
- {fluidattacks_core_logging-12.0.0 → fluidattacks_core_logging-12.2.0}/fluidattacks_core/logging/presets.py +0 -0
- {fluidattacks_core_logging-12.0.0 → fluidattacks_core_logging-12.2.0}/fluidattacks_core/logging/py.typed +0 -0
- {fluidattacks_core_logging-12.0.0 → fluidattacks_core_logging-12.2.0}/fluidattacks_core/logging/sources/__init__.py +0 -0
- {fluidattacks_core_logging-12.0.0 → fluidattacks_core_logging-12.2.0}/fluidattacks_core/logging/sources/types.py +0 -0
- {fluidattacks_core_logging-12.0.0 → fluidattacks_core_logging-12.2.0}/fluidattacks_core/logging/sources/utils.py +0 -0
- {fluidattacks_core_logging-12.0.0 → fluidattacks_core_logging-12.2.0}/fluidattacks_core/logging/utils.py +0 -0
- {fluidattacks_core_logging-12.0.0 → fluidattacks_core_logging-12.2.0}/mypy.ini +0 -0
- {fluidattacks_core_logging-12.0.0 → fluidattacks_core_logging-12.2.0}/ruff.toml +0 -0
- {fluidattacks_core_logging-12.0.0 → fluidattacks_core_logging-12.2.0}/tests/__init__.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
use flake ../../..#fluidattacks-core-logging
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
dist/
|
|
2
|
+
dist-*/
|
|
3
|
+
*.zip
|
|
4
|
+
/utils/terraform-modules/**/.terraform.lock.hcl
|
|
5
|
+
/.cargo-cache
|
|
6
|
+
target/
|
|
7
|
+
.direnv/
|
|
8
|
+
.venv/
|
|
9
|
+
.vscode/settings.json
|
|
10
|
+
.zed/settings.json
|
|
11
|
+
# each package's shell writes its own, beside the package rather than here
|
|
12
|
+
**/.vscode/settings.json
|
|
13
|
+
*.so
|
|
14
|
+
# the publish check builds into its own tree per architecture
|
|
15
|
+
target-*/
|
|
@@ -5,6 +5,7 @@ import logging.config
|
|
|
5
5
|
import sys
|
|
6
6
|
from typing import TYPE_CHECKING
|
|
7
7
|
|
|
8
|
+
from fluidattacks_core.logging.capture import CapturedLog, CaptureHandler
|
|
8
9
|
from fluidattacks_core.logging.presets import DATE_FORMAT, PRODUCT_LOGGING
|
|
9
10
|
from fluidattacks_core.logging.sources.utils import (
|
|
10
11
|
set_commit_ref_name,
|
|
@@ -45,10 +46,24 @@ def init_logging(preset: _DictConfigArgs | None = None) -> None:
|
|
|
45
46
|
init_uncaught_exception_logging()
|
|
46
47
|
|
|
47
48
|
|
|
49
|
+
def init_logging_captured(preset: _DictConfigArgs | None = None) -> CapturedLog:
|
|
50
|
+
"""`init_logging`, that also keeps a copy of everything logged.
|
|
51
|
+
|
|
52
|
+
The console is unchanged; the copy is what a program reads back to hand its output to
|
|
53
|
+
someone else once the run is over. Call it once: each call adds another copy.
|
|
54
|
+
"""
|
|
55
|
+
init_logging(preset)
|
|
56
|
+
capture = CapturedLog()
|
|
57
|
+
logging.getLogger().addHandler(CaptureHandler(capture))
|
|
58
|
+
return capture
|
|
59
|
+
|
|
60
|
+
|
|
48
61
|
__all__ = [
|
|
49
62
|
"DATE_FORMAT",
|
|
50
63
|
"PRODUCT_LOGGING",
|
|
64
|
+
"CapturedLog",
|
|
51
65
|
"init_logging",
|
|
66
|
+
"init_logging_captured",
|
|
52
67
|
"init_uncaught_exception_logging",
|
|
53
68
|
"set_commit_ref_name",
|
|
54
69
|
"set_commit_sha",
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Opt-in copy of what the logger writes.
|
|
2
|
+
|
|
3
|
+
For a program that has to hand its output to someone else after the run.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import threading
|
|
10
|
+
|
|
11
|
+
from fluidattacks_core.logging.presets import DATE_FORMAT
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CapturedLog:
|
|
15
|
+
"""What the logger wrote since `init_logging_captured`, as plain text lines.
|
|
16
|
+
|
|
17
|
+
Each line reads `<date> [<level>] [<logger>] <message>`, followed by the traceback
|
|
18
|
+
when the record carries one. It is the same whatever the environment, so it holds no
|
|
19
|
+
ANSI colors and no JSON, unlike the console.
|
|
20
|
+
|
|
21
|
+
It keeps everything: it never trims, and whoever receives it decides how much to
|
|
22
|
+
accept. Safe to read while other threads log.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self) -> None:
|
|
26
|
+
self._lock = threading.Lock()
|
|
27
|
+
self._lines: list[str] = []
|
|
28
|
+
|
|
29
|
+
def _push(self, text: str) -> None:
|
|
30
|
+
with self._lock:
|
|
31
|
+
self._lines.append(text)
|
|
32
|
+
|
|
33
|
+
def contents(self) -> str:
|
|
34
|
+
"""Return the output so far."""
|
|
35
|
+
with self._lock:
|
|
36
|
+
return "".join(self._lines)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class CaptureHandler(logging.Handler):
|
|
40
|
+
"""Feeds every record it receives into a `CapturedLog`."""
|
|
41
|
+
|
|
42
|
+
def __init__(self, capture: CapturedLog) -> None:
|
|
43
|
+
super().__init__()
|
|
44
|
+
self.capture = capture
|
|
45
|
+
self.setFormatter(
|
|
46
|
+
logging.Formatter(
|
|
47
|
+
"{asctime} [{levelname}] [{name}] {message}",
|
|
48
|
+
datefmt=DATE_FORMAT,
|
|
49
|
+
style="{",
|
|
50
|
+
),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def emit(self, record: logging.LogRecord) -> None:
|
|
54
|
+
try:
|
|
55
|
+
self.capture._push(self.format(record) + "\n") # noqa: SLF001
|
|
56
|
+
except Exception: # noqa: BLE001
|
|
57
|
+
self.handleError(record)
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import copy
|
|
2
2
|
import logging
|
|
3
3
|
import sys
|
|
4
|
+
import threading
|
|
4
5
|
from logging.handlers import HTTPHandler, QueueHandler, QueueListener
|
|
5
|
-
from queue import Queue
|
|
6
|
-
from typing import TextIO
|
|
6
|
+
from queue import Full, Queue
|
|
7
|
+
from typing import TextIO, cast
|
|
7
8
|
|
|
8
9
|
import simplejson as json
|
|
9
10
|
|
|
@@ -81,23 +82,46 @@ class ProductionSyncHandler(logging.StreamHandler[TextIO]):
|
|
|
81
82
|
self.setFormatter(CustomJsonFormatter())
|
|
82
83
|
|
|
83
84
|
|
|
85
|
+
class _DrainingStopListener(QueueListener):
|
|
86
|
+
"""Listener that drains the queue before stopping, so a full one cannot reject it."""
|
|
87
|
+
|
|
88
|
+
def stop(self) -> None:
|
|
89
|
+
cast("Queue[object]", self.queue).join()
|
|
90
|
+
super().stop()
|
|
91
|
+
|
|
92
|
+
|
|
84
93
|
class ProductionAsyncHandler(CustomQueueHandler):
|
|
85
94
|
"""Logging handler for production environments implemented with `QueueHandler`.
|
|
86
95
|
|
|
87
96
|
Includes:
|
|
88
97
|
- Filters: `NoBatchFilter`, `ProductionOnlyFilter`
|
|
89
98
|
- Formatter: `CustomJsonFormatter`
|
|
99
|
+
|
|
100
|
+
A positive `max_queue_size` drops records once the queue is full.
|
|
90
101
|
"""
|
|
91
102
|
|
|
92
|
-
def __init__(self, stream: TextIO = sys.stderr) -> None:
|
|
103
|
+
def __init__(self, stream: TextIO = sys.stderr, max_queue_size: int = 0) -> None:
|
|
93
104
|
handler = logging.StreamHandler(stream)
|
|
94
105
|
handler.addFilter(ProductionOnlyFilter())
|
|
95
106
|
handler.setFormatter(CustomJsonFormatter())
|
|
96
107
|
|
|
97
|
-
super().__init__(Queue())
|
|
108
|
+
super().__init__(Queue(maxsize=max_queue_size))
|
|
98
109
|
self.shutting_down = False
|
|
110
|
+
self.dropped = 0
|
|
111
|
+
self._dropped_lock = threading.Lock()
|
|
99
112
|
|
|
100
|
-
self._attach_listener(
|
|
113
|
+
self._attach_listener(_DrainingStopListener(self.queue, handler))
|
|
114
|
+
|
|
115
|
+
def on_drop(self) -> None:
|
|
116
|
+
"""Report a dropped record; it must neither log nor raise."""
|
|
117
|
+
|
|
118
|
+
def enqueue(self, record: logging.LogRecord) -> None:
|
|
119
|
+
try:
|
|
120
|
+
super().enqueue(record)
|
|
121
|
+
except Full:
|
|
122
|
+
with self._dropped_lock:
|
|
123
|
+
self.dropped += 1
|
|
124
|
+
self.on_drop()
|
|
101
125
|
|
|
102
126
|
def emit(self, record: logging.LogRecord) -> None:
|
|
103
127
|
if self.shutting_down:
|
|
@@ -1,16 +1,7 @@
|
|
|
1
1
|
[importlinter]
|
|
2
|
-
root_package = fluidattacks_core
|
|
2
|
+
root_package = fluidattacks_core.logging
|
|
3
3
|
include_external_packages = True
|
|
4
4
|
|
|
5
|
-
[importlinter:contract:portion]
|
|
6
|
-
name = Single Portion
|
|
7
|
-
type = layers
|
|
8
|
-
containers =
|
|
9
|
-
fluidattacks_core
|
|
10
|
-
exhaustive = true
|
|
11
|
-
layers =
|
|
12
|
-
logging
|
|
13
|
-
|
|
14
5
|
[importlinter:contract:dag]
|
|
15
6
|
name = Direct Acyclic Graph
|
|
16
7
|
type = layers
|
|
@@ -18,6 +9,7 @@
|
|
|
18
9
|
fluidattacks_core.logging
|
|
19
10
|
exhaustive = true
|
|
20
11
|
layers =
|
|
12
|
+
capture
|
|
21
13
|
presets
|
|
22
14
|
handlers
|
|
23
15
|
formatters
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from collections.abc import Iterator
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from fluidattacks_core.logging import CapturedLog, init_logging_captured
|
|
7
|
+
from fluidattacks_core.logging.capture import CaptureHandler
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.fixture
|
|
11
|
+
def root_logger() -> Iterator[logging.Logger]:
|
|
12
|
+
root = logging.getLogger()
|
|
13
|
+
handlers, level = list(root.handlers), root.level
|
|
14
|
+
yield root
|
|
15
|
+
root.handlers[:] = handlers
|
|
16
|
+
root.setLevel(level)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _written(capture: CapturedLog, text: str) -> None:
|
|
20
|
+
capture._push(text) # noqa: SLF001
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_a_capture_keeps_what_the_logger_wrote(root_logger: logging.Logger) -> None:
|
|
24
|
+
capture = init_logging_captured()
|
|
25
|
+
logging.getLogger("scan").info("first")
|
|
26
|
+
logging.getLogger("scan").warning("second")
|
|
27
|
+
lines = capture.contents().splitlines()
|
|
28
|
+
assert len(lines) == 2
|
|
29
|
+
assert lines[0].endswith("[INFO] [scan] first")
|
|
30
|
+
assert lines[1].endswith("[WARNING] [scan] second")
|
|
31
|
+
assert root_logger.handlers
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_the_copy_has_no_color_codes(root_logger: logging.Logger) -> None:
|
|
35
|
+
capture = init_logging_captured()
|
|
36
|
+
logging.getLogger("scan").error("boom")
|
|
37
|
+
assert "\x1b" not in capture.contents()
|
|
38
|
+
assert root_logger.handlers
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_the_copy_is_the_same_in_production(
|
|
42
|
+
monkeypatch: pytest.MonkeyPatch, root_logger: logging.Logger
|
|
43
|
+
) -> None:
|
|
44
|
+
monkeypatch.setenv("CI_COMMIT_REF_NAME", "trunk")
|
|
45
|
+
capture = init_logging_captured()
|
|
46
|
+
logging.getLogger("scan").info("plain")
|
|
47
|
+
assert capture.contents().endswith("[INFO] [scan] plain\n")
|
|
48
|
+
assert root_logger.handlers
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_a_traceback_is_kept(root_logger: logging.Logger) -> None:
|
|
52
|
+
capture = CapturedLog()
|
|
53
|
+
root_logger.addHandler(CaptureHandler(capture))
|
|
54
|
+
root_logger.setLevel(logging.INFO)
|
|
55
|
+
try:
|
|
56
|
+
message = "bad value"
|
|
57
|
+
raise ValueError(message) # noqa: TRY301
|
|
58
|
+
except ValueError:
|
|
59
|
+
logging.getLogger("scan").exception("failed")
|
|
60
|
+
assert "Traceback" in capture.contents()
|
|
61
|
+
assert "ValueError: bad value" in capture.contents()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_a_long_log_is_kept_whole() -> None:
|
|
65
|
+
capture = CapturedLog()
|
|
66
|
+
lines = [f"line {number}\n" for number in range(100_000)]
|
|
67
|
+
for line in lines:
|
|
68
|
+
_written(capture, line)
|
|
69
|
+
assert capture.contents() == "".join(lines)
|
|
@@ -1,10 +1,20 @@
|
|
|
1
|
+
import io
|
|
1
2
|
import logging
|
|
2
3
|
import sys
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
from queue import Queue
|
|
7
|
+
from typing import cast
|
|
3
8
|
|
|
4
9
|
import pytest
|
|
5
10
|
import simplejson as json
|
|
6
11
|
|
|
7
12
|
from fluidattacks_core.logging import init_logging
|
|
13
|
+
from fluidattacks_core.logging.handlers import (
|
|
14
|
+
CustomQueueHandler,
|
|
15
|
+
ProductionAsyncHandler,
|
|
16
|
+
TelemetryAsyncHandler,
|
|
17
|
+
)
|
|
8
18
|
from fluidattacks_core.logging.utils import set_telemetry_metadata
|
|
9
19
|
|
|
10
20
|
|
|
@@ -321,3 +331,159 @@ def test_colorful_formatter_uses_colors_for_warning(
|
|
|
321
331
|
assert "\x1b[0m" in log_message
|
|
322
332
|
assert "[WARNING] [product]" in log_message
|
|
323
333
|
assert "A warning message" in log_message
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _listener_consumes_new_records(handler: ProductionAsyncHandler | TelemetryAsyncHandler) -> bool:
|
|
337
|
+
queue = handler.queue
|
|
338
|
+
assert isinstance(queue, Queue)
|
|
339
|
+
|
|
340
|
+
record = logging.LogRecord("test", logging.INFO, __file__, 1, "Test message", None, None)
|
|
341
|
+
queue.put_nowait(record)
|
|
342
|
+
time.sleep(0.2)
|
|
343
|
+
|
|
344
|
+
return queue.empty()
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def test_custom_queue_handler_defaults_leave_no_listener() -> None:
|
|
348
|
+
# A fresh handler carries no listener on any interpreter: the class default
|
|
349
|
+
# leaves it empty, and `QueueHandler.__init__` clears it on Python 3.12+.
|
|
350
|
+
# Attaching is therefore the only thing that can install one, which the
|
|
351
|
+
# subclass tests below observe.
|
|
352
|
+
queue: Queue[logging.LogRecord] = Queue()
|
|
353
|
+
|
|
354
|
+
handler = CustomQueueHandler(queue)
|
|
355
|
+
|
|
356
|
+
assert handler.listener is None
|
|
357
|
+
assert handler.shutting_down is False
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def test_production_async_handler_ends_init_with_a_running_listener() -> None:
|
|
361
|
+
handler = ProductionAsyncHandler(io.StringIO())
|
|
362
|
+
listener = handler.listener
|
|
363
|
+
|
|
364
|
+
handler.close()
|
|
365
|
+
|
|
366
|
+
assert listener is not None
|
|
367
|
+
assert isinstance(handler.queue, Queue)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def test_telemetry_async_handler_ends_init_with_a_running_listener() -> None:
|
|
371
|
+
handler = TelemetryAsyncHandler("test-service", "test-source", "test-token")
|
|
372
|
+
listener = handler.listener
|
|
373
|
+
|
|
374
|
+
handler.close()
|
|
375
|
+
|
|
376
|
+
assert listener is not None
|
|
377
|
+
assert isinstance(handler.queue, Queue)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def test_production_async_handler_close_stops_listener_and_is_idempotent() -> None:
|
|
381
|
+
handler = ProductionAsyncHandler(io.StringIO())
|
|
382
|
+
|
|
383
|
+
handler.close()
|
|
384
|
+
handler.close()
|
|
385
|
+
|
|
386
|
+
assert not _listener_consumes_new_records(handler)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def test_telemetry_async_handler_close_stops_listener_and_is_idempotent() -> None:
|
|
390
|
+
handler = TelemetryAsyncHandler("test-service", "test-source", "test-token")
|
|
391
|
+
|
|
392
|
+
handler.close()
|
|
393
|
+
handler.close()
|
|
394
|
+
|
|
395
|
+
assert not _listener_consumes_new_records(handler)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def test_async_handler_init_leaves_a_closable_handler() -> None:
|
|
399
|
+
# The class-level defaults make a close() racing __init__ a safe no-op, but
|
|
400
|
+
# __init__ must still finish with a handler that a later close() can stop:
|
|
401
|
+
# a listener left running behind an already-true shutting_down would never
|
|
402
|
+
# be drained.
|
|
403
|
+
handler = ProductionAsyncHandler(io.StringIO())
|
|
404
|
+
|
|
405
|
+
assert handler.shutting_down is False
|
|
406
|
+
assert handler.listener is not None
|
|
407
|
+
|
|
408
|
+
handler.close()
|
|
409
|
+
|
|
410
|
+
assert not _listener_consumes_new_records(handler)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
class _BlockedStream(io.StringIO):
|
|
414
|
+
"""Stream whose writes wait for `release`, keeping the listener busy."""
|
|
415
|
+
|
|
416
|
+
def __init__(self) -> None:
|
|
417
|
+
super().__init__()
|
|
418
|
+
self.release = threading.Event()
|
|
419
|
+
|
|
420
|
+
def write(self, s: str) -> int:
|
|
421
|
+
self.release.wait(timeout=5)
|
|
422
|
+
return super().write(s)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _queue_of(handler: ProductionAsyncHandler) -> Queue[logging.LogRecord]:
|
|
426
|
+
return cast("Queue[logging.LogRecord]", handler.queue)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _record(message: str) -> logging.LogRecord:
|
|
430
|
+
return logging.LogRecord("test", logging.INFO, __file__, 0, message, None, None)
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def test_production_async_handler_is_unbounded_by_default() -> None:
|
|
434
|
+
handler = ProductionAsyncHandler(io.StringIO())
|
|
435
|
+
|
|
436
|
+
assert _queue_of(handler).maxsize == 0
|
|
437
|
+
assert handler.dropped == 0
|
|
438
|
+
|
|
439
|
+
handler.close()
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def test_production_async_handler_drops_records_when_the_queue_is_full() -> None:
|
|
443
|
+
stream = _BlockedStream()
|
|
444
|
+
handler = ProductionAsyncHandler(stream, max_queue_size=2)
|
|
445
|
+
total = 10
|
|
446
|
+
|
|
447
|
+
for index in range(total):
|
|
448
|
+
handler.emit(_record(f"message {index}"))
|
|
449
|
+
|
|
450
|
+
# One record may be held by the busy listener, the rest wait in the queue.
|
|
451
|
+
assert _queue_of(handler).qsize() == 2
|
|
452
|
+
assert total - 3 <= handler.dropped <= total - 2
|
|
453
|
+
|
|
454
|
+
stream.release.set()
|
|
455
|
+
handler.close()
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def test_production_async_handler_calls_on_drop_once_per_dropped_record() -> None:
|
|
459
|
+
drops: list[int] = []
|
|
460
|
+
|
|
461
|
+
class _Reporting(ProductionAsyncHandler):
|
|
462
|
+
def on_drop(self) -> None:
|
|
463
|
+
drops.append(1)
|
|
464
|
+
|
|
465
|
+
stream = _BlockedStream()
|
|
466
|
+
handler = _Reporting(stream, max_queue_size=1)
|
|
467
|
+
|
|
468
|
+
for index in range(5):
|
|
469
|
+
handler.emit(_record(f"message {index}"))
|
|
470
|
+
|
|
471
|
+
assert len(drops) == handler.dropped
|
|
472
|
+
assert handler.dropped >= 3
|
|
473
|
+
|
|
474
|
+
stream.release.set()
|
|
475
|
+
handler.close()
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def test_production_async_handler_closes_with_a_full_queue() -> None:
|
|
479
|
+
stream = _BlockedStream()
|
|
480
|
+
handler = ProductionAsyncHandler(stream, max_queue_size=1)
|
|
481
|
+
|
|
482
|
+
for index in range(4):
|
|
483
|
+
handler.emit(_record(f"message {index}"))
|
|
484
|
+
stream.release.set()
|
|
485
|
+
handler.close()
|
|
486
|
+
|
|
487
|
+
assert handler.listener is not None
|
|
488
|
+
assert handler.shutting_down is True
|
|
489
|
+
assert not _listener_consumes_new_records(handler)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
use_flake ".#python311.devShell"
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
{ src, workspace_root }:
|
|
2
|
-
{
|
|
3
|
-
inherit src workspace_root;
|
|
4
|
-
root_path = "develops/fluidattacks-core/python/logging";
|
|
5
|
-
module_name = "fluidattacks_core";
|
|
6
|
-
pypi_token = {
|
|
7
|
-
method = "oidc";
|
|
8
|
-
};
|
|
9
|
-
override = {
|
|
10
|
-
checks = {
|
|
11
|
-
basedpyright = false;
|
|
12
|
-
};
|
|
13
|
-
configs = {
|
|
14
|
-
import_linter = ../import-linter.cfg;
|
|
15
|
-
mypy = ../mypy.ini;
|
|
16
|
-
ruff = ../ruff.toml;
|
|
17
|
-
};
|
|
18
|
-
};
|
|
19
|
-
}
|
|
@@ -1,303 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"nodes": {
|
|
3
|
-
"flake-parts": {
|
|
4
|
-
"inputs": {
|
|
5
|
-
"nixpkgs-lib": "nixpkgs-lib"
|
|
6
|
-
},
|
|
7
|
-
"locked": {
|
|
8
|
-
"lastModified": 1762810396,
|
|
9
|
-
"narHash": "sha256-dxFVgQPG+R72dkhXTtqUm7KpxElw3u6E+YlQ2WaDgt8=",
|
|
10
|
-
"owner": "hercules-ci",
|
|
11
|
-
"repo": "flake-parts",
|
|
12
|
-
"rev": "0bdadb1b265fb4143a75bd1ec7d8c915898a9923",
|
|
13
|
-
"type": "github"
|
|
14
|
-
},
|
|
15
|
-
"original": {
|
|
16
|
-
"owner": "hercules-ci",
|
|
17
|
-
"repo": "flake-parts",
|
|
18
|
-
"type": "github"
|
|
19
|
-
}
|
|
20
|
-
},
|
|
21
|
-
"nix_filter": {
|
|
22
|
-
"locked": {
|
|
23
|
-
"lastModified": 1757882181,
|
|
24
|
-
"narHash": "sha256-+cCxYIh2UNalTz364p+QYmWHs0P+6wDhiWR4jDIKQIU=",
|
|
25
|
-
"owner": "numtide",
|
|
26
|
-
"repo": "nix-filter",
|
|
27
|
-
"rev": "59c44d1909c72441144b93cf0f054be7fe764de5",
|
|
28
|
-
"type": "github"
|
|
29
|
-
},
|
|
30
|
-
"original": {
|
|
31
|
-
"owner": "numtide",
|
|
32
|
-
"repo": "nix-filter",
|
|
33
|
-
"type": "github"
|
|
34
|
-
}
|
|
35
|
-
},
|
|
36
|
-
"nixpkgs": {
|
|
37
|
-
"locked": {
|
|
38
|
-
"lastModified": 1758690382,
|
|
39
|
-
"narHash": "sha256-NY3kSorgqE5LMm1LqNwGne3ZLMF2/ILgLpFr1fS4X3o=",
|
|
40
|
-
"owner": "nixos",
|
|
41
|
-
"repo": "nixpkgs",
|
|
42
|
-
"rev": "e643668fd71b949c53f8626614b21ff71a07379d",
|
|
43
|
-
"type": "github"
|
|
44
|
-
},
|
|
45
|
-
"original": {
|
|
46
|
-
"owner": "nixos",
|
|
47
|
-
"ref": "nixos-unstable",
|
|
48
|
-
"repo": "nixpkgs",
|
|
49
|
-
"type": "github"
|
|
50
|
-
}
|
|
51
|
-
},
|
|
52
|
-
"nixpkgs-lib": {
|
|
53
|
-
"locked": {
|
|
54
|
-
"lastModified": 1761765539,
|
|
55
|
-
"narHash": "sha256-b0yj6kfvO8ApcSE+QmA6mUfu8IYG6/uU28OFn4PaC8M=",
|
|
56
|
-
"owner": "nix-community",
|
|
57
|
-
"repo": "nixpkgs.lib",
|
|
58
|
-
"rev": "719359f4562934ae99f5443f20aa06c2ffff91fc",
|
|
59
|
-
"type": "github"
|
|
60
|
-
},
|
|
61
|
-
"original": {
|
|
62
|
-
"owner": "nix-community",
|
|
63
|
-
"repo": "nixpkgs.lib",
|
|
64
|
-
"type": "github"
|
|
65
|
-
}
|
|
66
|
-
},
|
|
67
|
-
"nixpkgs_2": {
|
|
68
|
-
"locked": {
|
|
69
|
-
"lastModified": 1758690382,
|
|
70
|
-
"narHash": "sha256-NY3kSorgqE5LMm1LqNwGne3ZLMF2/ILgLpFr1fS4X3o=",
|
|
71
|
-
"owner": "nixos",
|
|
72
|
-
"repo": "nixpkgs",
|
|
73
|
-
"rev": "e643668fd71b949c53f8626614b21ff71a07379d",
|
|
74
|
-
"type": "github"
|
|
75
|
-
},
|
|
76
|
-
"original": {
|
|
77
|
-
"owner": "nixos",
|
|
78
|
-
"ref": "nixos-unstable",
|
|
79
|
-
"repo": "nixpkgs",
|
|
80
|
-
"type": "github"
|
|
81
|
-
}
|
|
82
|
-
},
|
|
83
|
-
"nixpkgs_3": {
|
|
84
|
-
"locked": {
|
|
85
|
-
"lastModified": 1761373498,
|
|
86
|
-
"narHash": "sha256-Q/uhWNvd7V7k1H1ZPMy/vkx3F8C13ZcdrKjO7Jv7v0c=",
|
|
87
|
-
"owner": "nixos",
|
|
88
|
-
"repo": "nixpkgs",
|
|
89
|
-
"rev": "6a08e6bb4e46ff7fcbb53d409b253f6bad8a28ce",
|
|
90
|
-
"type": "github"
|
|
91
|
-
},
|
|
92
|
-
"original": {
|
|
93
|
-
"owner": "nixos",
|
|
94
|
-
"ref": "nixos-unstable",
|
|
95
|
-
"repo": "nixpkgs",
|
|
96
|
-
"type": "github"
|
|
97
|
-
}
|
|
98
|
-
},
|
|
99
|
-
"nixpkgs_flake": {
|
|
100
|
-
"locked": {
|
|
101
|
-
"lastModified": 1778253243,
|
|
102
|
-
"narHash": "sha256-KkH2QvU6iRLDTGGEwH5jnMqHWj4zMTuHxi1S1zdr01w=",
|
|
103
|
-
"owner": "nixos",
|
|
104
|
-
"repo": "nixpkgs",
|
|
105
|
-
"rev": "4fcda7103530b23c5ca44286344912082aad4220",
|
|
106
|
-
"type": "github"
|
|
107
|
-
},
|
|
108
|
-
"original": {
|
|
109
|
-
"owner": "nixos",
|
|
110
|
-
"repo": "nixpkgs",
|
|
111
|
-
"type": "github"
|
|
112
|
-
}
|
|
113
|
-
},
|
|
114
|
-
"pyproject-build-systems": {
|
|
115
|
-
"inputs": {
|
|
116
|
-
"nixpkgs": "nixpkgs",
|
|
117
|
-
"pyproject-nix": "pyproject-nix",
|
|
118
|
-
"uv2nix": "uv2nix"
|
|
119
|
-
},
|
|
120
|
-
"locked": {
|
|
121
|
-
"lastModified": 1761781027,
|
|
122
|
-
"narHash": "sha256-YDvxPAm2WnxrznRqWwHLjryBGG5Ey1ATEJXrON+TWt8=",
|
|
123
|
-
"owner": "pyproject-nix",
|
|
124
|
-
"repo": "build-system-pkgs",
|
|
125
|
-
"rev": "795a980d25301e5133eca37adae37283ec3c8e66",
|
|
126
|
-
"type": "github"
|
|
127
|
-
},
|
|
128
|
-
"original": {
|
|
129
|
-
"owner": "pyproject-nix",
|
|
130
|
-
"repo": "build-system-pkgs",
|
|
131
|
-
"type": "github"
|
|
132
|
-
}
|
|
133
|
-
},
|
|
134
|
-
"pyproject-nix": {
|
|
135
|
-
"inputs": {
|
|
136
|
-
"nixpkgs": [
|
|
137
|
-
"python_builder",
|
|
138
|
-
"pyproject-build-systems",
|
|
139
|
-
"nixpkgs"
|
|
140
|
-
]
|
|
141
|
-
},
|
|
142
|
-
"locked": {
|
|
143
|
-
"lastModified": 1758265079,
|
|
144
|
-
"narHash": "sha256-amLaLNwKSZPShQHzfgmc/9o76dU8xzN0743dWgvYlr8=",
|
|
145
|
-
"owner": "nix-community",
|
|
146
|
-
"repo": "pyproject.nix",
|
|
147
|
-
"rev": "02e9418fd4af638447dca4b17b1280da95527fc9",
|
|
148
|
-
"type": "github"
|
|
149
|
-
},
|
|
150
|
-
"original": {
|
|
151
|
-
"owner": "nix-community",
|
|
152
|
-
"repo": "pyproject.nix",
|
|
153
|
-
"type": "github"
|
|
154
|
-
}
|
|
155
|
-
},
|
|
156
|
-
"pyproject-nix_2": {
|
|
157
|
-
"inputs": {
|
|
158
|
-
"nixpkgs": "nixpkgs_2"
|
|
159
|
-
},
|
|
160
|
-
"locked": {
|
|
161
|
-
"lastModified": 1762427963,
|
|
162
|
-
"narHash": "sha256-CkPlAbIQ87wmjy5qHibfzk4DmMGBNqFer+lLfXjpP5M=",
|
|
163
|
-
"owner": "pyproject-nix",
|
|
164
|
-
"repo": "pyproject.nix",
|
|
165
|
-
"rev": "4540ea004e04fcd12dd2738d51383d10f956f7b9",
|
|
166
|
-
"type": "github"
|
|
167
|
-
},
|
|
168
|
-
"original": {
|
|
169
|
-
"owner": "pyproject-nix",
|
|
170
|
-
"repo": "pyproject.nix",
|
|
171
|
-
"type": "github"
|
|
172
|
-
}
|
|
173
|
-
},
|
|
174
|
-
"pyproject-nix_3": {
|
|
175
|
-
"inputs": {
|
|
176
|
-
"nixpkgs": [
|
|
177
|
-
"python_builder",
|
|
178
|
-
"uv2nix",
|
|
179
|
-
"nixpkgs"
|
|
180
|
-
]
|
|
181
|
-
},
|
|
182
|
-
"locked": {
|
|
183
|
-
"lastModified": 1760402624,
|
|
184
|
-
"narHash": "sha256-jF6UKLs2uGc2rtved8Vrt58oTWjTQoAssuYs/0578Z4=",
|
|
185
|
-
"owner": "pyproject-nix",
|
|
186
|
-
"repo": "pyproject.nix",
|
|
187
|
-
"rev": "84c4ea102127c77058ea1ed7be7300261fafc7d2",
|
|
188
|
-
"type": "github"
|
|
189
|
-
},
|
|
190
|
-
"original": {
|
|
191
|
-
"owner": "pyproject-nix",
|
|
192
|
-
"repo": "pyproject.nix",
|
|
193
|
-
"type": "github"
|
|
194
|
-
}
|
|
195
|
-
},
|
|
196
|
-
"python_builder": {
|
|
197
|
-
"inputs": {
|
|
198
|
-
"nix_filter": "nix_filter",
|
|
199
|
-
"nixpkgs_flake": "nixpkgs_flake",
|
|
200
|
-
"pyproject-build-systems": "pyproject-build-systems",
|
|
201
|
-
"pyproject-nix": "pyproject-nix_2",
|
|
202
|
-
"shell-helpers": "shell-helpers",
|
|
203
|
-
"uv2nix": "uv2nix_2"
|
|
204
|
-
},
|
|
205
|
-
"locked": {
|
|
206
|
-
"dir": "common/utils/python-builder",
|
|
207
|
-
"lastModified": 1787078362,
|
|
208
|
-
"narHash": "sha256-HYL7Rn6SuNjYxkt/PwFLksZjsauCiY+ENbOWryLsIlw=",
|
|
209
|
-
"ref": "refs/heads/trunk",
|
|
210
|
-
"rev": "09fd25a13309eb5e3b5b2a061bb340df3ae5d281",
|
|
211
|
-
"shallow": true,
|
|
212
|
-
"type": "git",
|
|
213
|
-
"url": "ssh://git@gitlab.com/fluidattacks/universe"
|
|
214
|
-
},
|
|
215
|
-
"original": {
|
|
216
|
-
"dir": "common/utils/python-builder",
|
|
217
|
-
"rev": "09fd25a13309eb5e3b5b2a061bb340df3ae5d281",
|
|
218
|
-
"shallow": true,
|
|
219
|
-
"type": "git",
|
|
220
|
-
"url": "ssh://git@gitlab.com/fluidattacks/universe"
|
|
221
|
-
}
|
|
222
|
-
},
|
|
223
|
-
"root": {
|
|
224
|
-
"inputs": {
|
|
225
|
-
"python_builder": "python_builder"
|
|
226
|
-
}
|
|
227
|
-
},
|
|
228
|
-
"shell-helpers": {
|
|
229
|
-
"inputs": {
|
|
230
|
-
"flake-parts": "flake-parts",
|
|
231
|
-
"nixpkgs": [
|
|
232
|
-
"python_builder",
|
|
233
|
-
"nixpkgs_flake"
|
|
234
|
-
]
|
|
235
|
-
},
|
|
236
|
-
"locked": {
|
|
237
|
-
"dir": "common/utils/shell-helpers",
|
|
238
|
-
"lastModified": 1762806066,
|
|
239
|
-
"narHash": "sha256-Wz99Fl7SpPU1NkTHJ36XOo6DjOEAMXCw3ThRABOS2Hs=",
|
|
240
|
-
"ref": "refs/heads/trunk",
|
|
241
|
-
"rev": "d73cfdc7ae9e7f547e22a4481d4c2ccce38b26a8",
|
|
242
|
-
"shallow": true,
|
|
243
|
-
"type": "git",
|
|
244
|
-
"url": "ssh://git@gitlab.com/fluidattacks/universe"
|
|
245
|
-
},
|
|
246
|
-
"original": {
|
|
247
|
-
"dir": "common/utils/shell-helpers",
|
|
248
|
-
"rev": "d73cfdc7ae9e7f547e22a4481d4c2ccce38b26a8",
|
|
249
|
-
"shallow": true,
|
|
250
|
-
"type": "git",
|
|
251
|
-
"url": "ssh://git@gitlab.com/fluidattacks/universe"
|
|
252
|
-
}
|
|
253
|
-
},
|
|
254
|
-
"uv2nix": {
|
|
255
|
-
"inputs": {
|
|
256
|
-
"nixpkgs": [
|
|
257
|
-
"python_builder",
|
|
258
|
-
"pyproject-build-systems",
|
|
259
|
-
"nixpkgs"
|
|
260
|
-
],
|
|
261
|
-
"pyproject-nix": [
|
|
262
|
-
"python_builder",
|
|
263
|
-
"pyproject-build-systems",
|
|
264
|
-
"pyproject-nix"
|
|
265
|
-
]
|
|
266
|
-
},
|
|
267
|
-
"locked": {
|
|
268
|
-
"lastModified": 1758933732,
|
|
269
|
-
"narHash": "sha256-HAmm1GBS1myZCFuog0DC2ZLaynvZtiUI2Crmo+cdQI0=",
|
|
270
|
-
"owner": "pyproject-nix",
|
|
271
|
-
"repo": "uv2nix",
|
|
272
|
-
"rev": "273ce18f913d8559e0d04f820d724308966d7c4d",
|
|
273
|
-
"type": "github"
|
|
274
|
-
},
|
|
275
|
-
"original": {
|
|
276
|
-
"owner": "pyproject-nix",
|
|
277
|
-
"repo": "uv2nix",
|
|
278
|
-
"type": "github"
|
|
279
|
-
}
|
|
280
|
-
},
|
|
281
|
-
"uv2nix_2": {
|
|
282
|
-
"inputs": {
|
|
283
|
-
"nixpkgs": "nixpkgs_3",
|
|
284
|
-
"pyproject-nix": "pyproject-nix_3"
|
|
285
|
-
},
|
|
286
|
-
"locked": {
|
|
287
|
-
"lastModified": 1761872265,
|
|
288
|
-
"narHash": "sha256-i25GRgp2vUOebY70L3NTAgkd+Pr1hnn5xM3qHxH0ONU=",
|
|
289
|
-
"owner": "pyproject-nix",
|
|
290
|
-
"repo": "uv2nix",
|
|
291
|
-
"rev": "74dfb62871be152ad3673b143b0cc56105a4f3c5",
|
|
292
|
-
"type": "github"
|
|
293
|
-
},
|
|
294
|
-
"original": {
|
|
295
|
-
"owner": "pyproject-nix",
|
|
296
|
-
"repo": "uv2nix",
|
|
297
|
-
"type": "github"
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
},
|
|
301
|
-
"root": "root",
|
|
302
|
-
"version": 7
|
|
303
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
description = "Fluid Attacks Core logging package.";
|
|
3
|
-
|
|
4
|
-
inputs = {
|
|
5
|
-
python_builder = {
|
|
6
|
-
url = "git+ssh://git@gitlab.com/fluidattacks/universe?shallow=1&rev=09fd25a13309eb5e3b5b2a061bb340df3ae5d281&dir=common/utils/python-builder";
|
|
7
|
-
};
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
outputs =
|
|
11
|
-
{ self, ... }@inputs:
|
|
12
|
-
let
|
|
13
|
-
build_args =
|
|
14
|
-
{
|
|
15
|
-
system,
|
|
16
|
-
python_version,
|
|
17
|
-
nixpkgs,
|
|
18
|
-
builders,
|
|
19
|
-
scripts,
|
|
20
|
-
}:
|
|
21
|
-
import ./build {
|
|
22
|
-
src = import ./build/filter.nix nixpkgs.nix-filter self;
|
|
23
|
-
workspace_root = ./.;
|
|
24
|
-
};
|
|
25
|
-
in
|
|
26
|
-
{
|
|
27
|
-
packages = inputs.python_builder.outputs.build build_args;
|
|
28
|
-
};
|
|
29
|
-
}
|
|
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
|