fastapi-loopguard 0.3.1__tar.gz → 0.4.1__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 (40) hide show
  1. fastapi_loopguard-0.4.1/PKG-INFO +131 -0
  2. fastapi_loopguard-0.4.1/README.md +86 -0
  3. fastapi_loopguard-0.4.1/assets/error-page-screenshot-console.png +0 -0
  4. fastapi_loopguard-0.4.1/assets/error-page-screenshot-endpoint.png +0 -0
  5. fastapi_loopguard-0.4.1/assets/error-page-screenshot.png +0 -0
  6. fastapi_loopguard-0.4.1/docs/CONFIGURATION.md +129 -0
  7. {fastapi_loopguard-0.3.1 → fastapi_loopguard-0.4.1}/pyproject.toml +1 -1
  8. {fastapi_loopguard-0.3.1 → fastapi_loopguard-0.4.1}/src/fastapi_loopguard/config.py +12 -1
  9. fastapi_loopguard-0.4.1/src/fastapi_loopguard/middleware.py +634 -0
  10. {fastapi_loopguard-0.3.1 → fastapi_loopguard-0.4.1}/tests/test_cumulative_blocking.py +15 -3
  11. fastapi_loopguard-0.4.1/tests/test_enforcement_mode.py +511 -0
  12. {fastapi_loopguard-0.3.1 → fastapi_loopguard-0.4.1}/tests/test_middleware.py +1 -0
  13. fastapi_loopguard-0.3.1/PKG-INFO +0 -154
  14. fastapi_loopguard-0.3.1/README.md +0 -109
  15. fastapi_loopguard-0.3.1/src/fastapi_loopguard/middleware.py +0 -236
  16. {fastapi_loopguard-0.3.1 → fastapi_loopguard-0.4.1}/.github/dependabot.yml +0 -0
  17. {fastapi_loopguard-0.3.1 → fastapi_loopguard-0.4.1}/.github/workflows/ci.yml +0 -0
  18. {fastapi_loopguard-0.3.1 → fastapi_loopguard-0.4.1}/.github/workflows/publish.yml +0 -0
  19. {fastapi_loopguard-0.3.1 → fastapi_loopguard-0.4.1}/.gitignore +0 -0
  20. {fastapi_loopguard-0.3.1 → fastapi_loopguard-0.4.1}/CLAUDE.md +0 -0
  21. {fastapi_loopguard-0.3.1 → fastapi_loopguard-0.4.1}/LICENSE +0 -0
  22. {fastapi_loopguard-0.3.1 → fastapi_loopguard-0.4.1}/assets/loopguard-logo.webp +0 -0
  23. {fastapi_loopguard-0.3.1 → fastapi_loopguard-0.4.1}/examples/demo_app.py +0 -0
  24. {fastapi_loopguard-0.3.1 → fastapi_loopguard-0.4.1}/examples/locustfile.py +0 -0
  25. {fastapi_loopguard-0.3.1 → fastapi_loopguard-0.4.1}/examples/run_stress_test.py +0 -0
  26. {fastapi_loopguard-0.3.1 → fastapi_loopguard-0.4.1}/examples/stress_app.py +0 -0
  27. {fastapi_loopguard-0.3.1 → fastapi_loopguard-0.4.1}/src/fastapi_loopguard/__init__.py +0 -0
  28. {fastapi_loopguard-0.3.1 → fastapi_loopguard-0.4.1}/src/fastapi_loopguard/context.py +0 -0
  29. {fastapi_loopguard-0.3.1 → fastapi_loopguard-0.4.1}/src/fastapi_loopguard/logging.py +0 -0
  30. {fastapi_loopguard-0.3.1 → fastapi_loopguard-0.4.1}/src/fastapi_loopguard/metrics.py +0 -0
  31. {fastapi_loopguard-0.3.1 → fastapi_loopguard-0.4.1}/src/fastapi_loopguard/monitor.py +0 -0
  32. {fastapi_loopguard-0.3.1 → fastapi_loopguard-0.4.1}/src/fastapi_loopguard/py.typed +0 -0
  33. {fastapi_loopguard-0.3.1 → fastapi_loopguard-0.4.1}/src/fastapi_loopguard/pytest_plugin.py +0 -0
  34. {fastapi_loopguard-0.3.1 → fastapi_loopguard-0.4.1}/tests/__init__.py +0 -0
  35. {fastapi_loopguard-0.3.1 → fastapi_loopguard-0.4.1}/tests/test_config.py +0 -0
  36. {fastapi_loopguard-0.3.1 → fastapi_loopguard-0.4.1}/tests/test_context.py +0 -0
  37. {fastapi_loopguard-0.3.1 → fastapi_loopguard-0.4.1}/tests/test_logging.py +0 -0
  38. {fastapi_loopguard-0.3.1 → fastapi_loopguard-0.4.1}/tests/test_metrics.py +0 -0
  39. {fastapi_loopguard-0.3.1 → fastapi_loopguard-0.4.1}/tests/test_monitor.py +0 -0
  40. {fastapi_loopguard-0.3.1 → fastapi_loopguard-0.4.1}/tests/test_pytest_plugin.py +0 -0
@@ -0,0 +1,131 @@
1
+ Metadata-Version: 2.4
2
+ Name: fastapi-loopguard
3
+ Version: 0.4.1
4
+ Summary: Detect event-loop blocking in FastAPI/Starlette with per-request attribution
5
+ Project-URL: Homepage, https://github.com/parhamdavari/fastapi-loopguard
6
+ Project-URL: Documentation, https://github.com/parhamdavari/fastapi-loopguard#readme
7
+ Project-URL: Repository, https://github.com/parhamdavari/fastapi-loopguard
8
+ Project-URL: Issues, https://github.com/parhamdavari/fastapi-loopguard/issues
9
+ Author: Parham
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: asyncio,blocking,event-loop,fastapi,middleware,monitoring,performance,starlette
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Framework :: AsyncIO
15
+ Classifier: Framework :: FastAPI
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: License :: OSI Approved :: MIT License
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Classifier: Topic :: System :: Monitoring
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.12
25
+ Requires-Dist: starlette<1.0,>=0.37.0
26
+ Provides-Extra: all
27
+ Requires-Dist: prometheus-client>=0.19.0; extra == 'all'
28
+ Requires-Dist: structlog>=24.1.0; extra == 'all'
29
+ Provides-Extra: dev
30
+ Requires-Dist: coverage>=7.4.0; extra == 'dev'
31
+ Requires-Dist: fastapi>=0.110.0; extra == 'dev'
32
+ Requires-Dist: httpx>=0.27.0; extra == 'dev'
33
+ Requires-Dist: mypy>=1.8.0; extra == 'dev'
34
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
35
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
36
+ Requires-Dist: ruff>=0.3.0; extra == 'dev'
37
+ Provides-Extra: prometheus
38
+ Requires-Dist: prometheus-client>=0.19.0; extra == 'prometheus'
39
+ Provides-Extra: stress
40
+ Requires-Dist: locust>=2.20.0; extra == 'stress'
41
+ Requires-Dist: uvicorn>=0.27.0; extra == 'stress'
42
+ Provides-Extra: structlog
43
+ Requires-Dist: structlog>=24.1.0; extra == 'structlog'
44
+ Description-Content-Type: text/markdown
45
+
46
+ <p align="center">
47
+ <img src="assets/loopguard-logo.webp" alt="LoopGuard" width="280" />
48
+ </p>
49
+
50
+ <p align="center">
51
+ <strong>Catch event-loop blocking in FastAPI with per-request attribution.</strong>
52
+ </p>
53
+
54
+ <p align="center">
55
+ <a href="https://badge.fury.io/py/fastapi-loopguard"><img src="https://badge.fury.io/py/fastapi-loopguard.svg" alt="PyPI version"></a>
56
+ <a href="https://www.python.org/downloads/"><img src="https://img.shields.io/badge/python-3.12+-blue.svg" alt="Python 3.12+"></a>
57
+ <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
58
+ </p>
59
+
60
+ ---
61
+
62
+ When a request blocks your event loop (via `time.sleep()`, blocking I/O, or CPU work), LoopGuard detects it **and tells you which endpoint caused it**.
63
+
64
+ ## Install
65
+
66
+ ```bash
67
+ pip install fastapi-loopguard
68
+ ```
69
+
70
+ ## Quick Start
71
+
72
+ ```python
73
+ from fastapi import FastAPI
74
+ from fastapi_loopguard import LoopGuardMiddleware
75
+
76
+ app = FastAPI()
77
+ app.add_middleware(LoopGuardMiddleware)
78
+ ```
79
+
80
+ ## Enforcement Modes
81
+
82
+ | Mode | Behavior | Use Case |
83
+ |------|----------|----------|
84
+ | `"warn"` | Console warnings + headers | **Default** |
85
+ | `"strict"` | HTTP 503 + error page | Development / CI |
86
+ | `"log"` | Silent logging | Production |
87
+
88
+ ```python
89
+ from fastapi_loopguard import LoopGuardConfig
90
+
91
+ # Development: strict enforcement (503 on blocking)
92
+ config = LoopGuardConfig(dev_mode=True)
93
+
94
+ # Production: silent logging
95
+ config = LoopGuardConfig(enforcement_mode="log")
96
+
97
+ app.add_middleware(LoopGuardMiddleware, config=config)
98
+ ```
99
+
100
+ ## What You Get
101
+
102
+ ### Strict Mode
103
+ Returns an educational 503 page that explains what went wrong and how to fix it:
104
+
105
+ <p align="center">
106
+ <img src="assets/error-page-screenshot.png" alt="Strict mode error page" width="600" />
107
+ </p>
108
+
109
+ ---
110
+
111
+ ### Warn Mode
112
+ Adds diagnostic headers to every response for debugging:
113
+
114
+ <p align="center">
115
+ <img src="assets/error-page-screenshot-endpoint.png" alt="Warn mode headers" width="600" />
116
+ </p>
117
+
118
+ ---
119
+
120
+ ### Log Mode
121
+ Writes structured logs with full request attribution:
122
+
123
+ <p align="center">
124
+ <img src="assets/error-page-screenshot-console.png" alt="Console output" width="600" />
125
+ </p>
126
+
127
+ ---
128
+
129
+ <p align="center">
130
+ <a href="docs/CONFIGURATION.md"><strong>Full Configuration Reference</strong></a>
131
+ </p>
@@ -0,0 +1,86 @@
1
+ <p align="center">
2
+ <img src="assets/loopguard-logo.webp" alt="LoopGuard" width="280" />
3
+ </p>
4
+
5
+ <p align="center">
6
+ <strong>Catch event-loop blocking in FastAPI with per-request attribution.</strong>
7
+ </p>
8
+
9
+ <p align="center">
10
+ <a href="https://badge.fury.io/py/fastapi-loopguard"><img src="https://badge.fury.io/py/fastapi-loopguard.svg" alt="PyPI version"></a>
11
+ <a href="https://www.python.org/downloads/"><img src="https://img.shields.io/badge/python-3.12+-blue.svg" alt="Python 3.12+"></a>
12
+ <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
13
+ </p>
14
+
15
+ ---
16
+
17
+ When a request blocks your event loop (via `time.sleep()`, blocking I/O, or CPU work), LoopGuard detects it **and tells you which endpoint caused it**.
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ pip install fastapi-loopguard
23
+ ```
24
+
25
+ ## Quick Start
26
+
27
+ ```python
28
+ from fastapi import FastAPI
29
+ from fastapi_loopguard import LoopGuardMiddleware
30
+
31
+ app = FastAPI()
32
+ app.add_middleware(LoopGuardMiddleware)
33
+ ```
34
+
35
+ ## Enforcement Modes
36
+
37
+ | Mode | Behavior | Use Case |
38
+ |------|----------|----------|
39
+ | `"warn"` | Console warnings + headers | **Default** |
40
+ | `"strict"` | HTTP 503 + error page | Development / CI |
41
+ | `"log"` | Silent logging | Production |
42
+
43
+ ```python
44
+ from fastapi_loopguard import LoopGuardConfig
45
+
46
+ # Development: strict enforcement (503 on blocking)
47
+ config = LoopGuardConfig(dev_mode=True)
48
+
49
+ # Production: silent logging
50
+ config = LoopGuardConfig(enforcement_mode="log")
51
+
52
+ app.add_middleware(LoopGuardMiddleware, config=config)
53
+ ```
54
+
55
+ ## What You Get
56
+
57
+ ### Strict Mode
58
+ Returns an educational 503 page that explains what went wrong and how to fix it:
59
+
60
+ <p align="center">
61
+ <img src="assets/error-page-screenshot.png" alt="Strict mode error page" width="600" />
62
+ </p>
63
+
64
+ ---
65
+
66
+ ### Warn Mode
67
+ Adds diagnostic headers to every response for debugging:
68
+
69
+ <p align="center">
70
+ <img src="assets/error-page-screenshot-endpoint.png" alt="Warn mode headers" width="600" />
71
+ </p>
72
+
73
+ ---
74
+
75
+ ### Log Mode
76
+ Writes structured logs with full request attribution:
77
+
78
+ <p align="center">
79
+ <img src="assets/error-page-screenshot-console.png" alt="Console output" width="600" />
80
+ </p>
81
+
82
+ ---
83
+
84
+ <p align="center">
85
+ <a href="docs/CONFIGURATION.md"><strong>Full Configuration Reference</strong></a>
86
+ </p>
@@ -0,0 +1,129 @@
1
+ # Configuration Reference
2
+
3
+ All configuration options for `LoopGuardConfig`.
4
+
5
+ ## Quick Reference
6
+
7
+ ```python
8
+ from fastapi_loopguard import LoopGuardConfig
9
+
10
+ config = LoopGuardConfig(
11
+ # Enforcement
12
+ enforcement_mode="warn", # "log" | "warn" | "strict"
13
+ dev_mode=False, # Auto-escalates to strict when True
14
+
15
+ # Detection tuning
16
+ monitor_interval_ms=10.0, # How often to check (ms)
17
+ threshold_multiplier=5.0, # Blocking = lag > baseline × multiplier
18
+ fallback_threshold_ms=50.0, # Threshold if calibration fails
19
+
20
+ # Cumulative detection (enabled by default)
21
+ cumulative_blocking_enabled=True,
22
+ cumulative_blocking_threshold_ms=200.0,
23
+ cumulative_window_ms=1000.0,
24
+
25
+ # Adaptive threshold (disabled by default)
26
+ adaptive_threshold=False,
27
+
28
+ # Integrations
29
+ prometheus_enabled=False,
30
+ log_blocking_events=True,
31
+ )
32
+ ```
33
+
34
+ ---
35
+
36
+ ## Core Settings
37
+
38
+ | Option | Type | Default | Description |
39
+ |--------|------|---------|-------------|
40
+ | `enabled` | bool | `True` | Master switch. Set `False` to disable entirely. |
41
+ | `enforcement_mode` | str | `"warn"` | How to respond: `"log"`, `"warn"`, or `"strict"` |
42
+ | `dev_mode` | bool | `False` | Enables response headers. Auto-escalates to strict mode. |
43
+ | `log_blocking_events` | bool | `True` | Log blocking events to console |
44
+ | `exclude_paths` | frozenset | `{"/health", ...}` | Paths to skip monitoring |
45
+
46
+ ---
47
+
48
+ ## Detection Tuning
49
+
50
+ | Option | Type | Default | Description |
51
+ |--------|------|---------|-------------|
52
+ | `monitor_interval_ms` | float | `10.0` | Sentinel check frequency (ms) |
53
+ | `threshold_multiplier` | float | `5.0` | Blocking detected when lag > baseline × this |
54
+ | `calibration_iterations` | int | `100` | Samples during startup calibration |
55
+ | `fallback_threshold_ms` | float | `50.0` | Used if calibration is unreliable |
56
+
57
+ ---
58
+
59
+ ## Cumulative Blocking Detection
60
+
61
+ Catches "death by a thousand cuts" - many small blocks that add up.
62
+
63
+ | Option | Type | Default | Description |
64
+ |--------|------|---------|-------------|
65
+ | `cumulative_blocking_enabled` | bool | `True` | Enable cumulative detection |
66
+ | `cumulative_blocking_threshold_ms` | float | `200.0` | Alert if total blocking exceeds this... |
67
+ | `cumulative_window_ms` | float | `1000.0` | ...within this time window (ms) |
68
+
69
+ **Example:** With defaults, alerts if blocking totals >200ms within any 1-second window.
70
+
71
+ ---
72
+
73
+ ## Adaptive Threshold
74
+
75
+ Dynamically adjusts threshold based on observed latency. Useful for high-concurrency environments.
76
+
77
+ | Option | Type | Default | Description |
78
+ |--------|------|---------|-------------|
79
+ | `adaptive_threshold` | bool | `False` | Enable adaptive mode |
80
+ | `adaptive_window_size` | int | `1000` | Samples in sliding window |
81
+ | `adaptive_percentile` | float | `0.95` | Percentile for baseline (0.5-0.99) |
82
+ | `adaptive_min_samples` | int | `100` | Min samples before activation |
83
+ | `adaptive_update_interval_ms` | float | `1000.0` | Recalculation frequency (ms) |
84
+
85
+ ---
86
+
87
+ ## Integrations
88
+
89
+ | Option | Type | Default | Description |
90
+ |--------|------|---------|-------------|
91
+ | `prometheus_enabled` | bool | `False` | Expose Prometheus metrics |
92
+
93
+ When enabled, exposes:
94
+ - `loopguard_blocking_events_total` - Counter of blocking events
95
+ - `loopguard_blocking_duration_ms` - Histogram of blocking durations
96
+
97
+ ---
98
+
99
+ ## Common Configurations
100
+
101
+ ### Development (strict enforcement)
102
+ ```python
103
+ config = LoopGuardConfig(dev_mode=True)
104
+ ```
105
+
106
+ ### Production (silent monitoring)
107
+ ```python
108
+ config = LoopGuardConfig(
109
+ enforcement_mode="log",
110
+ prometheus_enabled=True,
111
+ )
112
+ ```
113
+
114
+ ### High-concurrency (adaptive threshold)
115
+ ```python
116
+ config = LoopGuardConfig(
117
+ adaptive_threshold=True,
118
+ adaptive_percentile=0.99,
119
+ )
120
+ ```
121
+
122
+ ### Sensitive detection (lower threshold)
123
+ ```python
124
+ config = LoopGuardConfig(
125
+ monitor_interval_ms=5.0,
126
+ threshold_multiplier=3.0,
127
+ fallback_threshold_ms=30.0,
128
+ )
129
+ ```
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "fastapi-loopguard"
7
- version = "0.3.1"
7
+ version = "0.4.1"
8
8
  description = "Detect event-loop blocking in FastAPI/Starlette with per-request attribution"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -34,6 +34,10 @@ class LoopGuardConfig:
34
34
  log_blocking_events: bool = True
35
35
  prometheus_enabled: bool = False
36
36
 
37
+ # Enforcement mode: how aggressively to respond to blocking
38
+ # "log" = just log (production), "warn" = loud warnings, "strict" = 503 errors
39
+ enforcement_mode: str = "warn"
40
+
37
41
  # Adaptive threshold settings
38
42
  adaptive_threshold: bool = False
39
43
  adaptive_window_size: int = 1000
@@ -42,7 +46,7 @@ class LoopGuardConfig:
42
46
  adaptive_update_interval_ms: float = 1000.0
43
47
 
44
48
  # Cumulative blocking detection
45
- cumulative_blocking_enabled: bool = False
49
+ cumulative_blocking_enabled: bool = True
46
50
  cumulative_blocking_threshold_ms: float = 200.0
47
51
  cumulative_window_ms: float = 1000.0
48
52
 
@@ -86,3 +90,10 @@ class LoopGuardConfig:
86
90
  raise ValueError(
87
91
  "cumulative_window_ms cannot be less than monitor_interval_ms"
88
92
  )
93
+ # Enforcement mode validation
94
+ valid_enforcement_modes = {"log", "warn", "strict"}
95
+ if self.enforcement_mode not in valid_enforcement_modes:
96
+ raise ValueError(
97
+ f"enforcement_mode must be one of {valid_enforcement_modes}, "
98
+ f"got '{self.enforcement_mode}'"
99
+ )