elven-logs-interceptor-python 0.1.2__tar.gz → 0.1.3__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 (87) hide show
  1. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/PKG-INFO +1 -1
  2. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/pyproject.toml +1 -1
  3. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/src/logs_interceptor/application/log_service.py +3 -3
  4. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/src/logs_interceptor/infrastructure/buffer/memory_buffer.py +5 -1
  5. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/src/logs_interceptor/infrastructure/transport/loki_json_transport.py +1 -1
  6. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/src/logs_interceptor/infrastructure/transport/loki_protobuf_transport.py +1 -1
  7. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/src/logs_interceptor/presentation/factory.py +3 -3
  8. elven_logs_interceptor_python-0.1.3/test-apps/elven-live-demo/.env.example +49 -0
  9. elven_logs_interceptor_python-0.1.3/test-apps/elven-live-demo/README.md +17 -0
  10. elven_logs_interceptor_python-0.1.3/test-apps/elven-live-demo/app.py +97 -0
  11. elven_logs_interceptor_python-0.1.3/test-apps/elven-observability-smoke/run.sh +21 -0
  12. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/tests/unit/test_memory_buffer_extra.py +27 -0
  13. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/.github/workflows/ci.yml +0 -0
  14. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/.github/workflows/publish.yml +0 -0
  15. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/.gitignore +0 -0
  16. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/ARCHITECTURE.md +0 -0
  17. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/Makefile +0 -0
  18. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/README.md +0 -0
  19. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/examples/basic_app.py +0 -0
  20. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/examples/fastapi_integration.py +0 -0
  21. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/examples/full_config_reference.py +0 -0
  22. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/examples/high_volume.py +0 -0
  23. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/examples/tracking_usage.py +0 -0
  24. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/scripts/publish.sh +0 -0
  25. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/src/logs_interceptor/__init__.py +0 -0
  26. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/src/logs_interceptor/application/__init__.py +0 -0
  27. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/src/logs_interceptor/application/config_service.py +0 -0
  28. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/src/logs_interceptor/config.py +0 -0
  29. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/src/logs_interceptor/domain/__init__.py +0 -0
  30. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/src/logs_interceptor/domain/entities.py +0 -0
  31. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/src/logs_interceptor/domain/interfaces.py +0 -0
  32. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/src/logs_interceptor/domain/value_objects.py +0 -0
  33. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/src/logs_interceptor/infrastructure/__init__.py +0 -0
  34. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/src/logs_interceptor/infrastructure/buffer/__init__.py +0 -0
  35. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/src/logs_interceptor/infrastructure/circuit_breaker/__init__.py +0 -0
  36. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/src/logs_interceptor/infrastructure/circuit_breaker/circuit_breaker.py +0 -0
  37. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/src/logs_interceptor/infrastructure/compression/__init__.py +0 -0
  38. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/src/logs_interceptor/infrastructure/compression/base.py +0 -0
  39. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/src/logs_interceptor/infrastructure/compression/brotli_compressor.py +0 -0
  40. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/src/logs_interceptor/infrastructure/compression/factory.py +0 -0
  41. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/src/logs_interceptor/infrastructure/compression/gzip_compressor.py +0 -0
  42. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/src/logs_interceptor/infrastructure/compression/noop_compressor.py +0 -0
  43. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/src/logs_interceptor/infrastructure/context/__init__.py +0 -0
  44. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/src/logs_interceptor/infrastructure/context/context_provider.py +0 -0
  45. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/src/logs_interceptor/infrastructure/dlq/__init__.py +0 -0
  46. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/src/logs_interceptor/infrastructure/dlq/file_dlq.py +0 -0
  47. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/src/logs_interceptor/infrastructure/dlq/memory_dlq.py +0 -0
  48. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/src/logs_interceptor/infrastructure/filter/__init__.py +0 -0
  49. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/src/logs_interceptor/infrastructure/filter/log_filter.py +0 -0
  50. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/src/logs_interceptor/infrastructure/interceptors/__init__.py +0 -0
  51. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/src/logs_interceptor/infrastructure/interceptors/runtime_interceptor.py +0 -0
  52. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/src/logs_interceptor/infrastructure/memory/__init__.py +0 -0
  53. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/src/logs_interceptor/infrastructure/memory/memory_tracker.py +0 -0
  54. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/src/logs_interceptor/infrastructure/metrics/__init__.py +0 -0
  55. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/src/logs_interceptor/infrastructure/metrics/metrics_collector.py +0 -0
  56. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/src/logs_interceptor/infrastructure/transport/__init__.py +0 -0
  57. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/src/logs_interceptor/infrastructure/transport/resilient_transport.py +0 -0
  58. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/src/logs_interceptor/infrastructure/transport/transport_factory.py +0 -0
  59. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/src/logs_interceptor/infrastructure/workers/__init__.py +0 -0
  60. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/src/logs_interceptor/infrastructure/workers/worker_pool.py +0 -0
  61. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/src/logs_interceptor/integrations/__init__.py +0 -0
  62. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/src/logs_interceptor/integrations/celery.py +0 -0
  63. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/src/logs_interceptor/integrations/django.py +0 -0
  64. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/src/logs_interceptor/integrations/fastapi.py +0 -0
  65. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/src/logs_interceptor/integrations/flask.py +0 -0
  66. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/src/logs_interceptor/integrations/logging_handler.py +0 -0
  67. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/src/logs_interceptor/integrations/loguru.py +0 -0
  68. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/src/logs_interceptor/integrations/structlog.py +0 -0
  69. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/src/logs_interceptor/preload.py +0 -0
  70. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/src/logs_interceptor/presentation/__init__.py +0 -0
  71. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/src/logs_interceptor/types.py +0 -0
  72. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/src/logs_interceptor/utils.py +0 -0
  73. {elven_logs_interceptor_python-0.1.2/test-apps/elven-observability-smoke → elven_logs_interceptor_python-0.1.3/test-apps/elven-live-demo}/run.sh +0 -0
  74. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/test-apps/elven-observability-smoke/.env.example +0 -0
  75. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/test-apps/elven-observability-smoke/README.md +0 -0
  76. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/test-apps/elven-observability-smoke/app.py +0 -0
  77. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/tests/integration/test_api.py +0 -0
  78. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/tests/unit/test_circuit_breaker_extra.py +0 -0
  79. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/tests/unit/test_config_service.py +0 -0
  80. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/tests/unit/test_core_components.py +0 -0
  81. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/tests/unit/test_env_config.py +0 -0
  82. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/tests/unit/test_log_filter_extra.py +0 -0
  83. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/tests/unit/test_log_service_unit.py +0 -0
  84. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/tests/unit/test_loki_json_transport.py +0 -0
  85. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/tests/unit/test_protobuf_transport_safety.py +0 -0
  86. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/tests/unit/test_resilient_transport.py +0 -0
  87. {elven_logs_interceptor_python-0.1.2 → elven_logs_interceptor_python-0.1.3}/tests/unit/test_utils_extra.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: elven-logs-interceptor-python
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Production-grade logs interceptor for Python with Loki transport, resilience, batching, and framework integrations.
5
5
  Author: Elven Observability
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "elven-logs-interceptor-python"
7
- version = "0.1.2"
7
+ version = "0.1.3"
8
8
  description = "Production-grade logs interceptor for Python with Loki transport, resilience, batching, and framework integrations."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -22,11 +22,11 @@ try:
22
22
  except Exception: # pragma: no cover
23
23
  pass
24
24
 
25
- otel_trace: Any = None
26
25
  try:
27
- from opentelemetry import trace as otel_trace
26
+ from opentelemetry import trace as _otel_trace # type: ignore[import-not-found]
28
27
  except Exception: # pragma: no cover - optional dependency
29
- pass
28
+ _otel_trace = None
29
+ otel_trace: Any = _otel_trace
30
30
 
31
31
 
32
32
  @dataclass(slots=True)
@@ -165,12 +165,16 @@ class MemoryBuffer:
165
165
  return
166
166
 
167
167
  def _on_timer() -> None:
168
+ callback: Callable[[], None] | None = None
168
169
  with self._lock:
169
170
  self._flush_timer = None
170
171
  if self._destroyed:
171
172
  return
172
173
  if self._flush_callback and self._entries:
173
- self._flush_callback()
174
+ callback = self._flush_callback
175
+
176
+ if callback is not None:
177
+ callback()
174
178
 
175
179
  self._flush_timer = threading.Timer(self._config.flush_interval / 1000, _on_timer)
176
180
  self._flush_timer.daemon = True
@@ -88,7 +88,7 @@ class LokiJsonTransport:
88
88
  headers: dict[str, str] = {
89
89
  "Content-Type": "application/json",
90
90
  "X-Scope-OrgID": self._config.tenant_id,
91
- "User-Agent": "elven-logs-interceptor-python/0.1.2",
91
+ "User-Agent": "elven-logs-interceptor-python/0.1.3",
92
92
  **self._extra_headers,
93
93
  }
94
94
  if self._config.auth_token:
@@ -94,7 +94,7 @@ class LokiProtobufTransport:
94
94
  "Content-Type": "application/x-protobuf",
95
95
  "Content-Encoding": "snappy",
96
96
  "X-Scope-OrgID": self._config.tenant_id,
97
- "User-Agent": "elven-logs-interceptor-python/0.1.2",
97
+ "User-Agent": "elven-logs-interceptor-python/0.1.3",
98
98
  **self._extra_headers,
99
99
  }
100
100
  if self._config.auth_token:
@@ -15,11 +15,11 @@ from ..infrastructure.filter import LogFilter
15
15
  from ..infrastructure.interceptors import RuntimeInterceptor
16
16
  from ..infrastructure.transport import TransportFactory
17
17
 
18
- otel_trace: Any = None
19
18
  try:
20
- from opentelemetry import trace as otel_trace
19
+ from opentelemetry import trace as _otel_trace # type: ignore[import-not-found]
21
20
  except Exception: # pragma: no cover - optional dependency
22
- pass
21
+ _otel_trace = None
22
+ otel_trace: Any = _otel_trace
23
23
 
24
24
 
25
25
  @dataclass(slots=True)
@@ -0,0 +1,49 @@
1
+ LOGS_URL=https://loki.elvenobservability.com/loki/api/v1/push
2
+ LOGS_TENANT=elven
3
+ LOGS_TOKEN=replace-with-your-token
4
+ LOGS_APP_NAME=elven-live-demo
5
+ LOGS_APP_VERSION=1.0.0
6
+ LOGS_ENVIRONMENT=local
7
+
8
+ LOGS_COMPRESSION=gzip
9
+ LOGS_COMPRESSION_LEVEL=4
10
+ LOGS_COMPRESSION_THRESHOLD=1024
11
+ LOGS_CONNECTION_POOLING=true
12
+ LOGS_MAX_SOCKETS=50
13
+ LOGS_TIMEOUT=10000
14
+ LOGS_MAX_RETRIES=3
15
+ LOGS_RETRY_DELAY=1000
16
+
17
+ LOGS_BUFFER_MAX_SIZE=25
18
+ LOGS_BUFFER_FLUSH_INTERVAL=1500
19
+ LOGS_BUFFER_MAX_MEMORY_MB=128
20
+ LOGS_BUFFER_MAX_AGE=30000
21
+ LOGS_BUFFER_AUTO_FLUSH=true
22
+
23
+ LOGS_FILTER_LEVELS=debug,info,warn,error,fatal
24
+ LOGS_FILTER_SAMPLING_RATE=1.0
25
+ LOGS_FILTER_SANITIZE=true
26
+ LOGS_FILTER_MAX_MESSAGE_LENGTH=8192
27
+
28
+ LOGS_CIRCUIT_BREAKER_ENABLED=true
29
+ LOGS_CIRCUIT_BREAKER_FAILURE_THRESHOLD=20
30
+ LOGS_CIRCUIT_BREAKER_RESET_TIMEOUT=15000
31
+ LOGS_CIRCUIT_BREAKER_HALF_OPEN_REQUESTS=3
32
+
33
+ LOGS_DLQ_ENABLED=true
34
+ LOGS_DLQ_TYPE=file
35
+ LOGS_DLQ_MAX_SIZE=1000
36
+ LOGS_DLQ_MAX_RETRIES=3
37
+ LOGS_DLQ_BASE_PATH=./.logs-dlq
38
+
39
+ LOGS_MAX_CONCURRENT_FLUSHES=5
40
+ LOGS_INTERCEPT_CONSOLE=true
41
+ LOGS_PRESERVE_ORIGINAL_CONSOLE=true
42
+ LOGS_ENABLE_METRICS=true
43
+ LOGS_ENABLE_HEALTH_CHECK=true
44
+ LOGS_DEBUG=false
45
+ LOGS_SILENT_ERRORS=false
46
+ LOGS_ENABLED=true
47
+
48
+ LOGS_LABEL_SERVICE=elven-live-demo
49
+ LOGS_LABEL_ENVIRONMENT=local
@@ -0,0 +1,17 @@
1
+ # Elven Live Demo
2
+
3
+ Local Python app to exercise `elven-logs-interceptor-python` against the Elven Loki endpoint.
4
+
5
+ ## Run
6
+
7
+ ```bash
8
+ cd /Users/leonardozwirtes/Documents/Projects/logs-interceptor-python
9
+ chmod +x test-apps/elven-live-demo/run.sh
10
+ ./test-apps/elven-live-demo/run.sh
11
+ ```
12
+
13
+ ## Notes
14
+
15
+ - The tracked config template is `.env.example`.
16
+ - The real token lives only in local `.env`, which is ignored by Git.
17
+ - The package import remains `logs_interceptor`.
@@ -0,0 +1,97 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import random
5
+ import time
6
+
7
+ from logs_interceptor import destroy, init, logger
8
+
9
+
10
+ def emit_startup_logs() -> None:
11
+ logger.info("demo app started", {"component": "startup"})
12
+ logger.track_event("demo_started", {"source": "local-run", "at": time.time()})
13
+
14
+
15
+ def emit_business_logs() -> None:
16
+ for idx in range(10):
17
+ logger.info(
18
+ "processing item",
19
+ {
20
+ "idx": idx,
21
+ "value": random.randint(100, 999),
22
+ "duration_ms": round(random.uniform(5, 30), 2),
23
+ },
24
+ )
25
+
26
+ logger.warn("slow dependency detected", {"dependency": "inventory-service", "latency_ms": 187})
27
+
28
+ try:
29
+ raise RuntimeError("simulated business exception")
30
+ except RuntimeError as exc:
31
+ logger.error(
32
+ "handled domain error",
33
+ {
34
+ "error": str(exc),
35
+ "kind": "business",
36
+ },
37
+ )
38
+
39
+
40
+ def emit_sync_context_logs() -> None:
41
+ def _run() -> None:
42
+ logger.info("sync request started", {"method": "GET", "path": "/products"})
43
+ print("plain print still works and is intercepted")
44
+
45
+ logger.with_context(
46
+ {
47
+ "request_id": "demo-sync-request-1",
48
+ "trace_id": "demo-sync-trace-1",
49
+ "span_id": "demo-sync-span-1",
50
+ },
51
+ _run,
52
+ )
53
+
54
+
55
+ async def emit_async_context_logs() -> None:
56
+ async def _run() -> None:
57
+ logger.info("async job started", {"job": "catalog-refresh"})
58
+ await asyncio.sleep(0.05)
59
+ logger.track_event("catalog_refresh_finished", {"ok": True})
60
+
61
+ await logger.with_context_async(
62
+ {
63
+ "request_id": "demo-async-request-1",
64
+ "trace_id": "demo-async-trace-1",
65
+ "span_id": "demo-async-span-1",
66
+ },
67
+ _run,
68
+ )
69
+
70
+
71
+ def print_runtime_summary() -> None:
72
+ metrics = logger.get_metrics()
73
+ health = logger.get_health()
74
+ print(
75
+ {
76
+ "processed": metrics.get("logs_processed"),
77
+ "dropped": metrics.get("logs_dropped"),
78
+ "flush_count": metrics.get("flush_count"),
79
+ "error_count": metrics.get("error_count"),
80
+ }
81
+ )
82
+ print({"health": health})
83
+
84
+
85
+ def main() -> None:
86
+ init()
87
+ emit_startup_logs()
88
+ emit_business_logs()
89
+ emit_sync_context_logs()
90
+ asyncio.run(emit_async_context_logs())
91
+ logger.flush()
92
+ print_runtime_summary()
93
+ destroy()
94
+
95
+
96
+ if __name__ == "__main__":
97
+ main()
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env bash
2
+ set -Eeuo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ ROOT_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)"
6
+
7
+ if [[ ! -f "${SCRIPT_DIR}/.env" ]]; then
8
+ echo "Missing .env at ${SCRIPT_DIR}/.env" >&2
9
+ if [[ -f "${SCRIPT_DIR}/.env.example" ]]; then
10
+ echo "Create it with: cp ${SCRIPT_DIR}/.env.example ${SCRIPT_DIR}/.env" >&2
11
+ fi
12
+ exit 1
13
+ fi
14
+
15
+ set -a
16
+ source "${SCRIPT_DIR}/.env"
17
+ set +a
18
+
19
+ export PYTHONPATH="${ROOT_DIR}/src:${PYTHONPATH:-}"
20
+
21
+ python3 "${SCRIPT_DIR}/app.py"
@@ -106,6 +106,33 @@ def test_schedule_flush_callback_runs_with_entries() -> None:
106
106
  buffer.destroy()
107
107
 
108
108
 
109
+ def test_schedule_flush_callback_does_not_hold_buffer_lock() -> None:
110
+ callback_started = threading.Event()
111
+ release_callback = threading.Event()
112
+ add_completed = threading.Event()
113
+ buffer = _buffer(flush_interval=5, auto_flush=True)
114
+
115
+ def flush_callback() -> None:
116
+ callback_started.set()
117
+ release_callback.wait(timeout=1)
118
+
119
+ buffer.set_flush_callback(flush_callback)
120
+ buffer.add(_entry("1", "2026-01-01T00:00:00+00:00"))
121
+ assert callback_started.wait(timeout=1)
122
+
123
+ def add_entry() -> None:
124
+ buffer.add(_entry("2", "2026-01-01T00:00:01+00:00"))
125
+ add_completed.set()
126
+
127
+ add_thread = threading.Thread(target=add_entry)
128
+ add_thread.start()
129
+ assert add_completed.wait(timeout=0.2)
130
+
131
+ release_callback.set()
132
+ add_thread.join(timeout=1)
133
+ buffer.destroy()
134
+
135
+
109
136
  def test_schedule_flush_destroyed_guard_inside_timer() -> None:
110
137
  event = threading.Event()
111
138
  buffer = _buffer(flush_interval=40, auto_flush=False)