fluidattacks_core_logging 12.1.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.1.0 → fluidattacks_core_logging-12.2.0}/PKG-INFO +1 -1
- {fluidattacks_core_logging-12.1.0 → fluidattacks_core_logging-12.2.0}/fluidattacks_core/logging/handlers.py +29 -5
- {fluidattacks_core_logging-12.1.0 → fluidattacks_core_logging-12.2.0}/pyproject.toml +1 -1
- {fluidattacks_core_logging-12.1.0 → fluidattacks_core_logging-12.2.0}/tests/test_logging.py +81 -0
- {fluidattacks_core_logging-12.1.0 → fluidattacks_core_logging-12.2.0}/uv.lock +1 -1
- {fluidattacks_core_logging-12.1.0 → fluidattacks_core_logging-12.2.0}/.envrc +0 -0
- {fluidattacks_core_logging-12.1.0 → fluidattacks_core_logging-12.2.0}/.gitignore +0 -0
- {fluidattacks_core_logging-12.1.0 → fluidattacks_core_logging-12.2.0}/fluidattacks_core/logging/__init__.py +0 -0
- {fluidattacks_core_logging-12.1.0 → fluidattacks_core_logging-12.2.0}/fluidattacks_core/logging/capture.py +0 -0
- {fluidattacks_core_logging-12.1.0 → fluidattacks_core_logging-12.2.0}/fluidattacks_core/logging/filters.py +0 -0
- {fluidattacks_core_logging-12.1.0 → fluidattacks_core_logging-12.2.0}/fluidattacks_core/logging/formatters.py +0 -0
- {fluidattacks_core_logging-12.1.0 → fluidattacks_core_logging-12.2.0}/fluidattacks_core/logging/presets.py +0 -0
- {fluidattacks_core_logging-12.1.0 → fluidattacks_core_logging-12.2.0}/fluidattacks_core/logging/py.typed +0 -0
- {fluidattacks_core_logging-12.1.0 → fluidattacks_core_logging-12.2.0}/fluidattacks_core/logging/sources/__init__.py +0 -0
- {fluidattacks_core_logging-12.1.0 → fluidattacks_core_logging-12.2.0}/fluidattacks_core/logging/sources/types.py +0 -0
- {fluidattacks_core_logging-12.1.0 → fluidattacks_core_logging-12.2.0}/fluidattacks_core/logging/sources/utils.py +0 -0
- {fluidattacks_core_logging-12.1.0 → fluidattacks_core_logging-12.2.0}/fluidattacks_core/logging/utils.py +0 -0
- {fluidattacks_core_logging-12.1.0 → fluidattacks_core_logging-12.2.0}/import-linter.cfg +0 -0
- {fluidattacks_core_logging-12.1.0 → fluidattacks_core_logging-12.2.0}/mypy.ini +0 -0
- {fluidattacks_core_logging-12.1.0 → fluidattacks_core_logging-12.2.0}/ruff.toml +0 -0
- {fluidattacks_core_logging-12.1.0 → fluidattacks_core_logging-12.2.0}/tests/__init__.py +0 -0
- {fluidattacks_core_logging-12.1.0 → fluidattacks_core_logging-12.2.0}/tests/test_capture.py +0 -0
|
@@ -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,8 +1,10 @@
|
|
|
1
1
|
import io
|
|
2
2
|
import logging
|
|
3
3
|
import sys
|
|
4
|
+
import threading
|
|
4
5
|
import time
|
|
5
6
|
from queue import Queue
|
|
7
|
+
from typing import cast
|
|
6
8
|
|
|
7
9
|
import pytest
|
|
8
10
|
import simplejson as json
|
|
@@ -406,3 +408,82 @@ def test_async_handler_init_leaves_a_closable_handler() -> None:
|
|
|
406
408
|
handler.close()
|
|
407
409
|
|
|
408
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)
|
|
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
|