fastapi-loopguard 0.3.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.
- fastapi_loopguard-0.3.1/.github/dependabot.yml +13 -0
- fastapi_loopguard-0.3.1/.github/workflows/ci.yml +48 -0
- fastapi_loopguard-0.3.1/.github/workflows/publish.yml +32 -0
- fastapi_loopguard-0.3.1/.gitignore +115 -0
- fastapi_loopguard-0.3.1/CLAUDE.md +79 -0
- fastapi_loopguard-0.3.1/LICENSE +21 -0
- fastapi_loopguard-0.3.1/PKG-INFO +154 -0
- fastapi_loopguard-0.3.1/README.md +109 -0
- fastapi_loopguard-0.3.1/assets/loopguard-logo.webp +0 -0
- fastapi_loopguard-0.3.1/examples/demo_app.py +58 -0
- fastapi_loopguard-0.3.1/examples/locustfile.py +196 -0
- fastapi_loopguard-0.3.1/examples/run_stress_test.py +407 -0
- fastapi_loopguard-0.3.1/examples/stress_app.py +196 -0
- fastapi_loopguard-0.3.1/pyproject.toml +126 -0
- fastapi_loopguard-0.3.1/src/fastapi_loopguard/__init__.py +62 -0
- fastapi_loopguard-0.3.1/src/fastapi_loopguard/config.py +88 -0
- fastapi_loopguard-0.3.1/src/fastapi_loopguard/context.py +210 -0
- fastapi_loopguard-0.3.1/src/fastapi_loopguard/logging.py +100 -0
- fastapi_loopguard-0.3.1/src/fastapi_loopguard/metrics.py +204 -0
- fastapi_loopguard-0.3.1/src/fastapi_loopguard/middleware.py +236 -0
- fastapi_loopguard-0.3.1/src/fastapi_loopguard/monitor.py +412 -0
- fastapi_loopguard-0.3.1/src/fastapi_loopguard/py.typed +0 -0
- fastapi_loopguard-0.3.1/src/fastapi_loopguard/pytest_plugin.py +135 -0
- fastapi_loopguard-0.3.1/tests/__init__.py +1 -0
- fastapi_loopguard-0.3.1/tests/test_config.py +135 -0
- fastapi_loopguard-0.3.1/tests/test_context.py +506 -0
- fastapi_loopguard-0.3.1/tests/test_cumulative_blocking.py +99 -0
- fastapi_loopguard-0.3.1/tests/test_logging.py +242 -0
- fastapi_loopguard-0.3.1/tests/test_metrics.py +246 -0
- fastapi_loopguard-0.3.1/tests/test_middleware.py +617 -0
- fastapi_loopguard-0.3.1/tests/test_monitor.py +804 -0
- fastapi_loopguard-0.3.1/tests/test_pytest_plugin.py +280 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
updates:
|
|
3
|
+
- package-ecosystem: "pip"
|
|
4
|
+
directory: "/"
|
|
5
|
+
schedule:
|
|
6
|
+
interval: "weekly"
|
|
7
|
+
open-pull-requests-limit: 5
|
|
8
|
+
|
|
9
|
+
- package-ecosystem: "github-actions"
|
|
10
|
+
directory: "/"
|
|
11
|
+
schedule:
|
|
12
|
+
interval: "weekly"
|
|
13
|
+
open-pull-requests-limit: 5
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.12", "3.13"]
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v6
|
|
18
|
+
|
|
19
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
20
|
+
uses: actions/setup-python@v6
|
|
21
|
+
with:
|
|
22
|
+
python-version: ${{ matrix.python-version }}
|
|
23
|
+
|
|
24
|
+
- name: Install dependencies
|
|
25
|
+
run: |
|
|
26
|
+
python -m pip install --upgrade pip
|
|
27
|
+
pip install -e ".[dev]"
|
|
28
|
+
|
|
29
|
+
- name: Lint with ruff
|
|
30
|
+
run: |
|
|
31
|
+
ruff check src/ tests/
|
|
32
|
+
ruff format --check src/ tests/
|
|
33
|
+
|
|
34
|
+
- name: Type check with mypy
|
|
35
|
+
run: mypy src/
|
|
36
|
+
|
|
37
|
+
- name: Run tests with coverage
|
|
38
|
+
run: |
|
|
39
|
+
coverage run -m pytest tests/ -v
|
|
40
|
+
coverage report --fail-under=80
|
|
41
|
+
coverage xml
|
|
42
|
+
|
|
43
|
+
- name: Upload coverage to Codecov
|
|
44
|
+
uses: codecov/codecov-action@v5
|
|
45
|
+
if: matrix.python-version == '3.12'
|
|
46
|
+
with:
|
|
47
|
+
files: ./coverage.xml
|
|
48
|
+
fail_ci_if_error: false
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
environment: release
|
|
12
|
+
permissions:
|
|
13
|
+
id-token: write # Required for trusted publishing
|
|
14
|
+
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- name: Set up Python
|
|
19
|
+
uses: actions/setup-python@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: "3.12"
|
|
22
|
+
|
|
23
|
+
- name: Install build tools
|
|
24
|
+
run: |
|
|
25
|
+
python -m pip install --upgrade pip
|
|
26
|
+
pip install build
|
|
27
|
+
|
|
28
|
+
- name: Build package
|
|
29
|
+
run: python -m build
|
|
30
|
+
|
|
31
|
+
- name: Publish to PyPI
|
|
32
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
*.manifest
|
|
31
|
+
*.spec
|
|
32
|
+
|
|
33
|
+
# Installer logs
|
|
34
|
+
pip-log.txt
|
|
35
|
+
pip-delete-this-directory.txt
|
|
36
|
+
|
|
37
|
+
# Unit test / coverage reports
|
|
38
|
+
htmlcov/
|
|
39
|
+
.tox/
|
|
40
|
+
.nox/
|
|
41
|
+
.coverage
|
|
42
|
+
.coverage.*
|
|
43
|
+
.cache
|
|
44
|
+
nosetests.xml
|
|
45
|
+
coverage.xml
|
|
46
|
+
*.cover
|
|
47
|
+
*.py,cover
|
|
48
|
+
.hypothesis/
|
|
49
|
+
.pytest_cache/
|
|
50
|
+
|
|
51
|
+
# Translations
|
|
52
|
+
*.mo
|
|
53
|
+
*.pot
|
|
54
|
+
|
|
55
|
+
# Environments
|
|
56
|
+
.env
|
|
57
|
+
.venv
|
|
58
|
+
env/
|
|
59
|
+
venv/
|
|
60
|
+
ENV/
|
|
61
|
+
env.bak/
|
|
62
|
+
venv.bak/
|
|
63
|
+
|
|
64
|
+
# Spyder project settings
|
|
65
|
+
.spyderproject
|
|
66
|
+
.spyproject
|
|
67
|
+
|
|
68
|
+
# Rope project settings
|
|
69
|
+
.ropeproject
|
|
70
|
+
|
|
71
|
+
# mkdocs documentation
|
|
72
|
+
/site
|
|
73
|
+
|
|
74
|
+
# mypy
|
|
75
|
+
.mypy_cache/
|
|
76
|
+
.dmypy.json
|
|
77
|
+
dmypy.json
|
|
78
|
+
|
|
79
|
+
# Pyre type checker
|
|
80
|
+
.pyre/
|
|
81
|
+
|
|
82
|
+
# pytype static type analyzer
|
|
83
|
+
.pytype/
|
|
84
|
+
|
|
85
|
+
# Cython debug symbols
|
|
86
|
+
cython_debug/
|
|
87
|
+
|
|
88
|
+
# IDEs
|
|
89
|
+
.idea/
|
|
90
|
+
.vscode/
|
|
91
|
+
*.swp
|
|
92
|
+
*.swo
|
|
93
|
+
*~
|
|
94
|
+
|
|
95
|
+
# OS files
|
|
96
|
+
.DS_Store
|
|
97
|
+
Thumbs.db
|
|
98
|
+
|
|
99
|
+
# Project specific
|
|
100
|
+
*.log
|
|
101
|
+
.ruff_cache/
|
|
102
|
+
|
|
103
|
+
# Data files
|
|
104
|
+
*.csv
|
|
105
|
+
*.sqlite
|
|
106
|
+
*.db
|
|
107
|
+
|
|
108
|
+
# Performance profiling
|
|
109
|
+
perf.data
|
|
110
|
+
*.prof
|
|
111
|
+
*.pstat
|
|
112
|
+
|
|
113
|
+
# AI / Editors
|
|
114
|
+
.claude/
|
|
115
|
+
.cursor/
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Project Overview
|
|
6
|
+
|
|
7
|
+
fastapi-loopguard is a middleware library that detects event-loop blocking in FastAPI/Starlette applications with per-request attribution. It identifies when synchronous operations (like `time.sleep()`, blocking I/O, or CPU-bound code) block the async event loop and reports which request was responsible.
|
|
8
|
+
|
|
9
|
+
## Development Commands
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# Install dependencies (including dev tools)
|
|
13
|
+
pip install -e ".[dev]"
|
|
14
|
+
|
|
15
|
+
# Run tests
|
|
16
|
+
pytest
|
|
17
|
+
|
|
18
|
+
# Run single test
|
|
19
|
+
pytest tests/test_middleware.py::TestLoopGuardMiddleware::test_dev_mode_headers
|
|
20
|
+
|
|
21
|
+
# Type checking (strict mode)
|
|
22
|
+
mypy src/
|
|
23
|
+
|
|
24
|
+
# Linting and formatting
|
|
25
|
+
ruff check src/ tests/
|
|
26
|
+
ruff format src/ tests/
|
|
27
|
+
|
|
28
|
+
# Coverage
|
|
29
|
+
coverage run -m pytest && coverage report
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Architecture
|
|
33
|
+
|
|
34
|
+
### Core Components
|
|
35
|
+
|
|
36
|
+
**LoopGuardMiddleware** (`middleware.py`): Pure ASGI middleware (no BaseHTTPMiddleware) that:
|
|
37
|
+
- Handles ASGI lifespan events for proper startup/shutdown of the monitor
|
|
38
|
+
- Registers request contexts for attribution via `RequestRegistry`
|
|
39
|
+
- Injects debug headers (X-Request-Id, X-Blocking-Count, etc.) in dev mode via send wrapper
|
|
40
|
+
|
|
41
|
+
**SentinelMonitor** (`monitor.py`): Background async task that detects blocking by:
|
|
42
|
+
1. Sleeping for short intervals (default 10ms)
|
|
43
|
+
2. Measuring actual elapsed time vs expected time
|
|
44
|
+
3. If lag exceeds threshold (baseline × multiplier), blocking occurred
|
|
45
|
+
4. Attributes blocking to ALL active requests since we can't determine the specific cause
|
|
46
|
+
|
|
47
|
+
**RequestRegistry** (`context.py`): Thread-safe (via single-threaded asyncio) registry tracking concurrent requests. Uses dict keyed by request_id. When blocking is detected, the monitor iterates all active contexts.
|
|
48
|
+
|
|
49
|
+
**LoopGuardConfig** (`config.py`): Frozen dataclass with validation. Key settings:
|
|
50
|
+
- `monitor_interval_ms`: Check frequency (default 10ms)
|
|
51
|
+
- `threshold_multiplier`: Blocking = lag > baseline × multiplier (default 5.0)
|
|
52
|
+
- `dev_mode`: Enables X-Blocking-* response headers
|
|
53
|
+
- `adaptive_threshold`: Enable sliding-window based automatic threshold adjustment (v0.3.0+)
|
|
54
|
+
|
|
55
|
+
**AdaptiveThreshold** (`monitor.py`): Sliding window percentile-based threshold calculator:
|
|
56
|
+
- Maintains a bounded deque of recent lag samples (`adaptive_window_size`)
|
|
57
|
+
- Recalculates threshold as `P{adaptive_percentile} × multiplier`
|
|
58
|
+
- Only activates after `adaptive_min_samples` collected
|
|
59
|
+
- Reduces false positives in high-concurrency environments
|
|
60
|
+
|
|
61
|
+
### Blocking Detection Flow
|
|
62
|
+
|
|
63
|
+
1. Middleware registers `RequestContext` in global `RequestRegistry`
|
|
64
|
+
2. `SentinelMonitor` runs continuous sleep-measure loop
|
|
65
|
+
3. On startup: background calibration measures baseline latency (P75 of samples)
|
|
66
|
+
4. When lag > threshold: `_handle_blocking()` iterates all active contexts via `get_active_requests()`
|
|
67
|
+
5. Each context's `record_blocking()` appends event to `blocking_events` list
|
|
68
|
+
6. On response: middleware reads context stats and adds headers
|
|
69
|
+
|
|
70
|
+
### Optional Components
|
|
71
|
+
|
|
72
|
+
- **Prometheus metrics** (`metrics.py`): Optional `prometheus_client` integration
|
|
73
|
+
- **pytest plugin** (`pytest_plugin.py`): `@pytest.mark.no_blocking` marker fails tests that block
|
|
74
|
+
|
|
75
|
+
## Test Configuration
|
|
76
|
+
|
|
77
|
+
- Uses `pytest-asyncio` with `asyncio_mode = "auto"` and `asyncio_default_fixture_loop_scope = "function"`
|
|
78
|
+
- Tests use `httpx.AsyncClient` with `ASGITransport` for testing ASGI apps
|
|
79
|
+
- Each test clears the global `RequestRegistry` via `clear_registry` fixture
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Parham
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fastapi-loopguard
|
|
3
|
+
Version: 0.3.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 logo" width="320" />
|
|
48
|
+
</p>
|
|
49
|
+
|
|
50
|
+
<p align="center">
|
|
51
|
+
Detect event-loop blocking in FastAPI/Starlette with <strong>per-request attribution</strong>.
|
|
52
|
+
</p>
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
[](https://badge.fury.io/py/fastapi-loopguard)
|
|
57
|
+
[](https://www.python.org/downloads/)
|
|
58
|
+
[](https://opensource.org/licenses/MIT)
|
|
59
|
+
|
|
60
|
+
`fastapi-loopguard` monitors your event loop and tells you exactly which request caused the blocking. It handles high-concurrency environments with adaptive thresholds and integrates seamlessly with Prometheus.
|
|
61
|
+
|
|
62
|
+
###
|
|
63
|
+
|
|
64
|
+
### Quick Start
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pip install fastapi-loopguard
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from fastapi import FastAPI
|
|
72
|
+
from fastapi_loopguard import LoopGuardMiddleware
|
|
73
|
+
|
|
74
|
+
app = FastAPI()
|
|
75
|
+
app.add_middleware(LoopGuardMiddleware)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Features
|
|
79
|
+
|
|
80
|
+
* **Per-Request Attribution**: Pinpoint the exact endpoint causing latency.
|
|
81
|
+
* **Cumulative Blocking Detection**: Catch "death by a thousand cuts" where frequent small blocks freeze the loop.
|
|
82
|
+
* **Adaptive Thresholds**: Smart baselines that adjust to server load.
|
|
83
|
+
* **Zero-Overhead Sentinel**: Cooperative monitoring with negligible CPU usage.
|
|
84
|
+
* **Observability Ready**: Built-in support for Prometheus and structured logging.
|
|
85
|
+
|
|
86
|
+
### Configuration
|
|
87
|
+
|
|
88
|
+
LoopGuard is highly configurable to suit your environment:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from fastapi import FastAPI
|
|
92
|
+
from fastapi_loopguard import LoopGuardMiddleware, LoopGuardConfig
|
|
93
|
+
|
|
94
|
+
config = LoopGuardConfig(
|
|
95
|
+
# Basic settings
|
|
96
|
+
monitor_interval_ms=10.0,
|
|
97
|
+
fallback_threshold_ms=50.0,
|
|
98
|
+
|
|
99
|
+
# Enable Cumulative Blocking Detection
|
|
100
|
+
# Detects frequent small blocks (e.g., 20ms) that sum up to a large freeze
|
|
101
|
+
cumulative_blocking_enabled=True,
|
|
102
|
+
cumulative_blocking_threshold_ms=200.0, # Alert if total block > 200ms...
|
|
103
|
+
cumulative_window_ms=1000.0, # ...within any 1-second window
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
app = FastAPI()
|
|
107
|
+
app.add_middleware(LoopGuardMiddleware, config=config)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Request/Response flows
|
|
111
|
+
|
|
112
|
+
```mermaid
|
|
113
|
+
sequenceDiagram
|
|
114
|
+
autonumber
|
|
115
|
+
actor Client
|
|
116
|
+
participant MW as Middleware<br/>(middleware.py)
|
|
117
|
+
participant Reg as Context Registry<br/>(context.py)
|
|
118
|
+
participant Mon as Sentinel Monitor<br/>(monitor.py)
|
|
119
|
+
participant App as FastAPI App<br/>(Your Code)
|
|
120
|
+
|
|
121
|
+
Note over MW, App: Request Flow
|
|
122
|
+
|
|
123
|
+
Client->>MW: HTTP Request
|
|
124
|
+
activate MW
|
|
125
|
+
|
|
126
|
+
MW->>Reg: Register Context
|
|
127
|
+
activate Reg
|
|
128
|
+
Reg-->>MW: Done
|
|
129
|
+
deactivate Reg
|
|
130
|
+
|
|
131
|
+
MW->>App: Forward Request
|
|
132
|
+
activate App
|
|
133
|
+
|
|
134
|
+
par Async Monitoring
|
|
135
|
+
Mon->>Reg: Detect Blocking &<br/>Update Contexts
|
|
136
|
+
activate Mon
|
|
137
|
+
activate Reg
|
|
138
|
+
Reg-->>Mon: Updated
|
|
139
|
+
deactivate Reg
|
|
140
|
+
deactivate Mon
|
|
141
|
+
and App Processing
|
|
142
|
+
App-->>MW: Process Complete
|
|
143
|
+
deactivate App
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
MW->>Reg: Unregister Context
|
|
147
|
+
activate Reg
|
|
148
|
+
Reg-->>MW: Done
|
|
149
|
+
deactivate Reg
|
|
150
|
+
|
|
151
|
+
MW->>Client: Response + Headers
|
|
152
|
+
deactivate MW
|
|
153
|
+
```
|
|
154
|
+
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="assets/loopguard-logo.webp" alt="LoopGuard logo" width="320" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
Detect event-loop blocking in FastAPI/Starlette with <strong>per-request attribution</strong>.
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
[](https://badge.fury.io/py/fastapi-loopguard)
|
|
12
|
+
[](https://www.python.org/downloads/)
|
|
13
|
+
[](https://opensource.org/licenses/MIT)
|
|
14
|
+
|
|
15
|
+
`fastapi-loopguard` monitors your event loop and tells you exactly which request caused the blocking. It handles high-concurrency environments with adaptive thresholds and integrates seamlessly with Prometheus.
|
|
16
|
+
|
|
17
|
+
###
|
|
18
|
+
|
|
19
|
+
### Quick Start
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install fastapi-loopguard
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from fastapi import FastAPI
|
|
27
|
+
from fastapi_loopguard import LoopGuardMiddleware
|
|
28
|
+
|
|
29
|
+
app = FastAPI()
|
|
30
|
+
app.add_middleware(LoopGuardMiddleware)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Features
|
|
34
|
+
|
|
35
|
+
* **Per-Request Attribution**: Pinpoint the exact endpoint causing latency.
|
|
36
|
+
* **Cumulative Blocking Detection**: Catch "death by a thousand cuts" where frequent small blocks freeze the loop.
|
|
37
|
+
* **Adaptive Thresholds**: Smart baselines that adjust to server load.
|
|
38
|
+
* **Zero-Overhead Sentinel**: Cooperative monitoring with negligible CPU usage.
|
|
39
|
+
* **Observability Ready**: Built-in support for Prometheus and structured logging.
|
|
40
|
+
|
|
41
|
+
### Configuration
|
|
42
|
+
|
|
43
|
+
LoopGuard is highly configurable to suit your environment:
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from fastapi import FastAPI
|
|
47
|
+
from fastapi_loopguard import LoopGuardMiddleware, LoopGuardConfig
|
|
48
|
+
|
|
49
|
+
config = LoopGuardConfig(
|
|
50
|
+
# Basic settings
|
|
51
|
+
monitor_interval_ms=10.0,
|
|
52
|
+
fallback_threshold_ms=50.0,
|
|
53
|
+
|
|
54
|
+
# Enable Cumulative Blocking Detection
|
|
55
|
+
# Detects frequent small blocks (e.g., 20ms) that sum up to a large freeze
|
|
56
|
+
cumulative_blocking_enabled=True,
|
|
57
|
+
cumulative_blocking_threshold_ms=200.0, # Alert if total block > 200ms...
|
|
58
|
+
cumulative_window_ms=1000.0, # ...within any 1-second window
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
app = FastAPI()
|
|
62
|
+
app.add_middleware(LoopGuardMiddleware, config=config)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Request/Response flows
|
|
66
|
+
|
|
67
|
+
```mermaid
|
|
68
|
+
sequenceDiagram
|
|
69
|
+
autonumber
|
|
70
|
+
actor Client
|
|
71
|
+
participant MW as Middleware<br/>(middleware.py)
|
|
72
|
+
participant Reg as Context Registry<br/>(context.py)
|
|
73
|
+
participant Mon as Sentinel Monitor<br/>(monitor.py)
|
|
74
|
+
participant App as FastAPI App<br/>(Your Code)
|
|
75
|
+
|
|
76
|
+
Note over MW, App: Request Flow
|
|
77
|
+
|
|
78
|
+
Client->>MW: HTTP Request
|
|
79
|
+
activate MW
|
|
80
|
+
|
|
81
|
+
MW->>Reg: Register Context
|
|
82
|
+
activate Reg
|
|
83
|
+
Reg-->>MW: Done
|
|
84
|
+
deactivate Reg
|
|
85
|
+
|
|
86
|
+
MW->>App: Forward Request
|
|
87
|
+
activate App
|
|
88
|
+
|
|
89
|
+
par Async Monitoring
|
|
90
|
+
Mon->>Reg: Detect Blocking &<br/>Update Contexts
|
|
91
|
+
activate Mon
|
|
92
|
+
activate Reg
|
|
93
|
+
Reg-->>Mon: Updated
|
|
94
|
+
deactivate Reg
|
|
95
|
+
deactivate Mon
|
|
96
|
+
and App Processing
|
|
97
|
+
App-->>MW: Process Complete
|
|
98
|
+
deactivate App
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
MW->>Reg: Unregister Context
|
|
102
|
+
activate Reg
|
|
103
|
+
Reg-->>MW: Done
|
|
104
|
+
deactivate Reg
|
|
105
|
+
|
|
106
|
+
MW->>Client: Response + Headers
|
|
107
|
+
deactivate MW
|
|
108
|
+
```
|
|
109
|
+
|
|
Binary file
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Demo app showing fastapi-loopguard blocking detection."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
from fastapi import FastAPI
|
|
7
|
+
|
|
8
|
+
from fastapi_loopguard import LoopGuardConfig, LoopGuardMiddleware
|
|
9
|
+
|
|
10
|
+
app = FastAPI(title="LoopGuard Demo")
|
|
11
|
+
|
|
12
|
+
# Configure with dev_mode for headers and logging enabled
|
|
13
|
+
config = LoopGuardConfig(
|
|
14
|
+
dev_mode=True,
|
|
15
|
+
log_blocking_events=True,
|
|
16
|
+
fallback_threshold_ms=30.0, # Low threshold for demo
|
|
17
|
+
)
|
|
18
|
+
app.add_middleware(LoopGuardMiddleware, config=config)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@app.get("/")
|
|
22
|
+
async def root():
|
|
23
|
+
"""Non-blocking endpoint."""
|
|
24
|
+
return {"message": "Hello, this is non-blocking!"}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@app.get("/api/users")
|
|
28
|
+
async def get_users():
|
|
29
|
+
"""Endpoint that blocks the event loop - will trigger warning."""
|
|
30
|
+
# This blocks the event loop! BAD!
|
|
31
|
+
time.sleep(0.15) # 150ms blocking
|
|
32
|
+
# Small yield to let monitor detect the blocking while context is still active
|
|
33
|
+
await asyncio.sleep(0.02)
|
|
34
|
+
return {"users": ["alice", "bob", "charlie"]}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@app.get("/api/items")
|
|
38
|
+
async def get_items():
|
|
39
|
+
"""Non-blocking endpoint using async sleep."""
|
|
40
|
+
# This is correct - doesn't block
|
|
41
|
+
await asyncio.sleep(0.1)
|
|
42
|
+
return {"items": ["item1", "item2", "item3"]}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
if __name__ == "__main__":
|
|
46
|
+
import uvicorn
|
|
47
|
+
|
|
48
|
+
print("\n" + "=" * 60)
|
|
49
|
+
print("LoopGuard Demo App")
|
|
50
|
+
print("=" * 60)
|
|
51
|
+
print("\nEndpoints:")
|
|
52
|
+
print(" GET / - Non-blocking (no warning)")
|
|
53
|
+
print(" GET /api/users - BLOCKING (will trigger warning)")
|
|
54
|
+
print(" GET /api/items - Non-blocking async sleep (no warning)")
|
|
55
|
+
print("\nTry: curl http://localhost:8765/api/users")
|
|
56
|
+
print("=" * 60 + "\n")
|
|
57
|
+
|
|
58
|
+
uvicorn.run(app, host="0.0.0.0", port=8765, log_level="info")
|