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.
Files changed (22) hide show
  1. {fluidattacks_core_logging-12.1.0 → fluidattacks_core_logging-12.2.0}/PKG-INFO +1 -1
  2. {fluidattacks_core_logging-12.1.0 → fluidattacks_core_logging-12.2.0}/fluidattacks_core/logging/handlers.py +29 -5
  3. {fluidattacks_core_logging-12.1.0 → fluidattacks_core_logging-12.2.0}/pyproject.toml +1 -1
  4. {fluidattacks_core_logging-12.1.0 → fluidattacks_core_logging-12.2.0}/tests/test_logging.py +81 -0
  5. {fluidattacks_core_logging-12.1.0 → fluidattacks_core_logging-12.2.0}/uv.lock +1 -1
  6. {fluidattacks_core_logging-12.1.0 → fluidattacks_core_logging-12.2.0}/.envrc +0 -0
  7. {fluidattacks_core_logging-12.1.0 → fluidattacks_core_logging-12.2.0}/.gitignore +0 -0
  8. {fluidattacks_core_logging-12.1.0 → fluidattacks_core_logging-12.2.0}/fluidattacks_core/logging/__init__.py +0 -0
  9. {fluidattacks_core_logging-12.1.0 → fluidattacks_core_logging-12.2.0}/fluidattacks_core/logging/capture.py +0 -0
  10. {fluidattacks_core_logging-12.1.0 → fluidattacks_core_logging-12.2.0}/fluidattacks_core/logging/filters.py +0 -0
  11. {fluidattacks_core_logging-12.1.0 → fluidattacks_core_logging-12.2.0}/fluidattacks_core/logging/formatters.py +0 -0
  12. {fluidattacks_core_logging-12.1.0 → fluidattacks_core_logging-12.2.0}/fluidattacks_core/logging/presets.py +0 -0
  13. {fluidattacks_core_logging-12.1.0 → fluidattacks_core_logging-12.2.0}/fluidattacks_core/logging/py.typed +0 -0
  14. {fluidattacks_core_logging-12.1.0 → fluidattacks_core_logging-12.2.0}/fluidattacks_core/logging/sources/__init__.py +0 -0
  15. {fluidattacks_core_logging-12.1.0 → fluidattacks_core_logging-12.2.0}/fluidattacks_core/logging/sources/types.py +0 -0
  16. {fluidattacks_core_logging-12.1.0 → fluidattacks_core_logging-12.2.0}/fluidattacks_core/logging/sources/utils.py +0 -0
  17. {fluidattacks_core_logging-12.1.0 → fluidattacks_core_logging-12.2.0}/fluidattacks_core/logging/utils.py +0 -0
  18. {fluidattacks_core_logging-12.1.0 → fluidattacks_core_logging-12.2.0}/import-linter.cfg +0 -0
  19. {fluidattacks_core_logging-12.1.0 → fluidattacks_core_logging-12.2.0}/mypy.ini +0 -0
  20. {fluidattacks_core_logging-12.1.0 → fluidattacks_core_logging-12.2.0}/ruff.toml +0 -0
  21. {fluidattacks_core_logging-12.1.0 → fluidattacks_core_logging-12.2.0}/tests/__init__.py +0 -0
  22. {fluidattacks_core_logging-12.1.0 → fluidattacks_core_logging-12.2.0}/tests/test_capture.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: fluidattacks_core_logging
3
- Version: 12.1.0
3
+ Version: 12.2.0
4
4
  Summary: Fluid Attacks Core Logging Library
5
5
  Author-email: Development <development@fluidattacks.com>
6
6
  License: MPL-2.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(QueueListener(self.queue, handler))
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,6 +1,6 @@
1
1
  [project]
2
2
  name = "fluidattacks_core_logging"
3
- version = "12.1.0"
3
+ version = "12.2.0"
4
4
  description = "Fluid Attacks Core Logging Library"
5
5
  authors = [{ name = "Development", email = "development@fluidattacks.com" }]
6
6
  license = { text = "MPL-2.0" }
@@ -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)
@@ -148,7 +148,7 @@ wheels = [
148
148
 
149
149
  [[package]]
150
150
  name = "fluidattacks-core-logging"
151
- version = "12.1.0"
151
+ version = "12.2.0"
152
152
  source = { editable = "." }
153
153
  dependencies = [
154
154
  { name = "python-json-logger" },