fastuator 0.0.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.
- fastuator-0.0.1/.github/workflows/test.yml +41 -0
- fastuator-0.0.1/.gitignore +56 -0
- fastuator-0.0.1/LICENSE +21 -0
- fastuator-0.0.1/PKG-INFO +242 -0
- fastuator-0.0.1/README.md +218 -0
- fastuator-0.0.1/examples/basic_app.py +9 -0
- fastuator-0.0.1/fastuator/__init__.py +11 -0
- fastuator-0.0.1/fastuator/checks.py +36 -0
- fastuator-0.0.1/fastuator/core.py +266 -0
- fastuator-0.0.1/main.py +16 -0
- fastuator-0.0.1/pyproject.toml +57 -0
- fastuator-0.0.1/pytest.ini +13 -0
- fastuator-0.0.1/tests/__init__.py +0 -0
- fastuator-0.0.1/tests/conftest.py +91 -0
- fastuator-0.0.1/tests/test_endpoints.py +119 -0
- fastuator-0.0.1/tests/test_health_checks.py +79 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
name: Tests
|
|
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.10", "3.11", "3.12", "3.13", "3.14"]
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- name: Checkout code
|
|
18
|
+
uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
21
|
+
uses: actions/setup-python@v5
|
|
22
|
+
with:
|
|
23
|
+
python-version: ${{ matrix.python-version }}
|
|
24
|
+
allow-prereleases: true
|
|
25
|
+
|
|
26
|
+
- name: Install dependencies
|
|
27
|
+
run: |
|
|
28
|
+
python -m pip install --upgrade pip
|
|
29
|
+
pip install -e .
|
|
30
|
+
pip install pytest pytest-cov pytest-asyncio httpx
|
|
31
|
+
|
|
32
|
+
- name: Run tests with coverage
|
|
33
|
+
run: |
|
|
34
|
+
pytest -v --cov=fastuator --cov-report=xml --cov-report=term
|
|
35
|
+
|
|
36
|
+
- name: Upload coverage to Codecov
|
|
37
|
+
uses: codecov/codecov-action@v4
|
|
38
|
+
if: matrix.python-version == '3.12'
|
|
39
|
+
with:
|
|
40
|
+
file: ./coverage.xml
|
|
41
|
+
fail_ci_if_error: false
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# IDE
|
|
2
|
+
.idea
|
|
3
|
+
.vscode
|
|
4
|
+
.mypy_cache
|
|
5
|
+
|
|
6
|
+
# Python
|
|
7
|
+
__pycache__
|
|
8
|
+
*.py[cod]
|
|
9
|
+
*.so
|
|
10
|
+
*.egg
|
|
11
|
+
*.egg-info/
|
|
12
|
+
.Python
|
|
13
|
+
.pytest_cache
|
|
14
|
+
.coverage*
|
|
15
|
+
coverage.xml
|
|
16
|
+
htmlcov
|
|
17
|
+
|
|
18
|
+
# Virtual environments
|
|
19
|
+
venv/
|
|
20
|
+
env/
|
|
21
|
+
env3.*
|
|
22
|
+
.venv/
|
|
23
|
+
|
|
24
|
+
# Build
|
|
25
|
+
dist/
|
|
26
|
+
build/
|
|
27
|
+
site/
|
|
28
|
+
docs_build/
|
|
29
|
+
site_build/
|
|
30
|
+
|
|
31
|
+
# Testing
|
|
32
|
+
.cache
|
|
33
|
+
test.db
|
|
34
|
+
log.txt
|
|
35
|
+
|
|
36
|
+
# Jupyter
|
|
37
|
+
.ipynb_checkpoints
|
|
38
|
+
|
|
39
|
+
# Package files
|
|
40
|
+
Pipfile.lock
|
|
41
|
+
docs.zip
|
|
42
|
+
archive.zip
|
|
43
|
+
|
|
44
|
+
# vim
|
|
45
|
+
*~
|
|
46
|
+
.*.sw?
|
|
47
|
+
|
|
48
|
+
# macOS
|
|
49
|
+
.DS_Store
|
|
50
|
+
|
|
51
|
+
# Windows
|
|
52
|
+
Thumbs.db
|
|
53
|
+
|
|
54
|
+
# Misc
|
|
55
|
+
.netlify
|
|
56
|
+
.codspeed
|
fastuator-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Noh Hyeon Nam
|
|
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.
|
fastuator-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fastuator
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Production-ready actuator endpoints for FastAPI (health, metrics, info)
|
|
5
|
+
Project-URL: Homepage, https://github.com/fastuator/fastuator
|
|
6
|
+
Project-URL: Repository, https://github.com/fastuator/fastuator
|
|
7
|
+
Author-email: Noh Hyeon Nam <shgusska12@gmail.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Requires-Dist: fastapi>=0.115.0
|
|
12
|
+
Requires-Dist: prometheus-client>=0.20.0
|
|
13
|
+
Requires-Dist: psutil>=5.9.0
|
|
14
|
+
Requires-Dist: pydantic>=2.0.0
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: black>=24.0.0; extra == 'dev'
|
|
17
|
+
Requires-Dist: httpx>=0.25.0; extra == 'dev'
|
|
18
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
|
|
19
|
+
Requires-Dist: pytest-cov>=4.1.0; extra == 'dev'
|
|
20
|
+
Requires-Dist: pytest>=7.4.0; extra == 'dev'
|
|
21
|
+
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
22
|
+
Requires-Dist: uvicorn[standard]>=0.32.0; extra == 'dev'
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# Fastuator
|
|
26
|
+
|
|
27
|
+
[](https://github.com/fastuator/fastuator/actions/workflows/ci.yml)
|
|
28
|
+
[](https://github.com/fastuator/fastuator)
|
|
29
|
+
[](https://www.python.org/downloads/)
|
|
30
|
+
[](LICENSE)
|
|
31
|
+
|
|
32
|
+
Production-ready monitoring toolkit for FastAPI applications.
|
|
33
|
+
Kubernetes probes, Prometheus metrics, and health checks in one line.
|
|
34
|
+
|
|
35
|
+
## ✨ Features
|
|
36
|
+
|
|
37
|
+
- 🏥 **Health Checks**: Aggregated health status with customizable checks
|
|
38
|
+
- 🔍 **K8s Probes**: Built-in liveness and readiness endpoints
|
|
39
|
+
- 📊 **Prometheus Metrics**: Auto-instrumented HTTP metrics
|
|
40
|
+
- ℹ️ **System Info**: Build version and platform details
|
|
41
|
+
- 🎯 **Zero Config**: Works out of the box with sensible defaults
|
|
42
|
+
- ⚡ **100% Test Coverage**: Battle-tested and production-ready
|
|
43
|
+
|
|
44
|
+
## 📦 Installation
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install fastuator
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## 🚀 Quick Start
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from fastapi import FastAPI
|
|
54
|
+
from fastuator import Fastuator
|
|
55
|
+
|
|
56
|
+
app = FastAPI()
|
|
57
|
+
Fastuator(app) # That's it!
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Available Endpoints:**
|
|
61
|
+
|
|
62
|
+
| Endpoint | Description |
|
|
63
|
+
|----------|-------------|
|
|
64
|
+
| `GET /fastuator/health` | Aggregated health status with optional details |
|
|
65
|
+
| `GET /fastuator/liveness` | Kubernetes liveness probe (critical checks only) |
|
|
66
|
+
| `GET /fastuator/readiness` | Kubernetes readiness probe (all dependencies) |
|
|
67
|
+
| `GET /fastuator/metrics` | Prometheus-compatible metrics |
|
|
68
|
+
| `GET /fastuator/info` | Application and system information |
|
|
69
|
+
|
|
70
|
+
## 📖 Usage
|
|
71
|
+
|
|
72
|
+
### Basic Setup
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from fastapi import FastAPI
|
|
76
|
+
from fastuator import Fastuator
|
|
77
|
+
|
|
78
|
+
app = FastAPI()
|
|
79
|
+
|
|
80
|
+
# Use default configuration
|
|
81
|
+
Fastuator(app)
|
|
82
|
+
|
|
83
|
+
# Custom prefix
|
|
84
|
+
Fastuator(app, prefix="/monitoring")
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Custom Health Checks
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from fastuator import Fastuator
|
|
91
|
+
|
|
92
|
+
app = FastAPI()
|
|
93
|
+
|
|
94
|
+
# Define custom health checks
|
|
95
|
+
async def database_health():
|
|
96
|
+
try:
|
|
97
|
+
# Check database connection
|
|
98
|
+
await db.execute("SELECT 1")
|
|
99
|
+
return {"status": "UP", "database": "connected"}
|
|
100
|
+
except Exception as e:
|
|
101
|
+
return {"status": "DOWN", "database": str(e)}
|
|
102
|
+
|
|
103
|
+
async def redis_health():
|
|
104
|
+
try:
|
|
105
|
+
await redis.ping()
|
|
106
|
+
return {"status": "UP", "redis": "connected"}
|
|
107
|
+
except Exception:
|
|
108
|
+
return {"status": "DOWN", "redis": "unreachable"}
|
|
109
|
+
|
|
110
|
+
# Register custom checks
|
|
111
|
+
Fastuator(
|
|
112
|
+
app,
|
|
113
|
+
health_checks=[database_health, redis_health],
|
|
114
|
+
readiness_checks=[database_health, redis_health],
|
|
115
|
+
liveness_checks=[], # No external dependencies for liveness
|
|
116
|
+
)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Health Check Response
|
|
120
|
+
|
|
121
|
+
**Without details:**
|
|
122
|
+
```bash
|
|
123
|
+
curl http://localhost:8000/fastuator/health
|
|
124
|
+
```
|
|
125
|
+
```json
|
|
126
|
+
{"status": "UP"}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**With details:**
|
|
130
|
+
```bash
|
|
131
|
+
curl http://localhost:8000/fastuator/health?show_details=true
|
|
132
|
+
```
|
|
133
|
+
```json
|
|
134
|
+
{
|
|
135
|
+
"status": "UP",
|
|
136
|
+
"components": {
|
|
137
|
+
"check_0": {
|
|
138
|
+
"status": "UP",
|
|
139
|
+
"cpu_percent": 45.2
|
|
140
|
+
},
|
|
141
|
+
"check_1": {
|
|
142
|
+
"status": "UP",
|
|
143
|
+
"memory_percent": 62.1,
|
|
144
|
+
"memory_available_mb": 4096
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## ☸️ Kubernetes Integration
|
|
151
|
+
|
|
152
|
+
### Deployment Example
|
|
153
|
+
|
|
154
|
+
```yaml
|
|
155
|
+
apiVersion: apps/v1
|
|
156
|
+
kind: Deployment
|
|
157
|
+
metadata:
|
|
158
|
+
name: fastapi-app
|
|
159
|
+
spec:
|
|
160
|
+
template:
|
|
161
|
+
spec:
|
|
162
|
+
containers:
|
|
163
|
+
- name: app
|
|
164
|
+
image: myapp:latest
|
|
165
|
+
ports:
|
|
166
|
+
- containerPort: 8000
|
|
167
|
+
livenessProbe:
|
|
168
|
+
httpGet:
|
|
169
|
+
path: /fastuator/liveness
|
|
170
|
+
port: 8000
|
|
171
|
+
initialDelaySeconds: 10
|
|
172
|
+
periodSeconds: 10
|
|
173
|
+
readinessProbe:
|
|
174
|
+
httpGet:
|
|
175
|
+
path: /fastuator/readiness
|
|
176
|
+
port: 8000
|
|
177
|
+
initialDelaySeconds: 5
|
|
178
|
+
periodSeconds: 5
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## 📊 Prometheus Metrics
|
|
182
|
+
|
|
183
|
+
Fastuator automatically collects the following metrics:
|
|
184
|
+
|
|
185
|
+
- `http_requests_total`: Total HTTP requests (counter)
|
|
186
|
+
- `http_request_duration_seconds`: Request duration histogram
|
|
187
|
+
- `app_health_status`: Health status gauge (1=UP, 0=DOWN)
|
|
188
|
+
|
|
189
|
+
**Scrape configuration:**
|
|
190
|
+
```yaml
|
|
191
|
+
scrape_configs:
|
|
192
|
+
- job_name: 'fastapi'
|
|
193
|
+
static_configs:
|
|
194
|
+
- targets: ['localhost:8000']
|
|
195
|
+
metrics_path: '/fastuator/metrics'
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## ⚙️ Configuration
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
Fastuator(
|
|
202
|
+
app,
|
|
203
|
+
prefix="/fastuator", # URL prefix for all endpoints
|
|
204
|
+
health_checks=[...], # List of health check functions
|
|
205
|
+
liveness_checks=[...], # Checks for liveness probe
|
|
206
|
+
readiness_checks=[...], # Checks for readiness probe
|
|
207
|
+
enable_metrics=True, # Enable Prometheus metrics
|
|
208
|
+
)
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## 🧪 Built-in Health Checks
|
|
212
|
+
|
|
213
|
+
Fastuator includes these health checks by default:
|
|
214
|
+
|
|
215
|
+
- **CPU Usage**: Reports DOWN if CPU > 90%
|
|
216
|
+
- **Memory Usage**: Reports DOWN if memory > 90%
|
|
217
|
+
- **Disk Usage**: Reports DOWN if disk > 90%
|
|
218
|
+
|
|
219
|
+
## 🤝 Contributing
|
|
220
|
+
|
|
221
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
222
|
+
|
|
223
|
+
1. Fork the repository
|
|
224
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
225
|
+
3. Run tests (`pytest --cov=fastuator`)
|
|
226
|
+
4. Commit your changes (`git commit -m 'Add amazing feature'`)
|
|
227
|
+
5. Push to the branch (`git push origin feature/amazing-feature`)
|
|
228
|
+
6. Open a Pull Request
|
|
229
|
+
|
|
230
|
+
## 📄 License
|
|
231
|
+
|
|
232
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
233
|
+
|
|
234
|
+
## 🙏 Acknowledgments
|
|
235
|
+
|
|
236
|
+
Inspired by [Spring Boot Actuator](https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html).
|
|
237
|
+
|
|
238
|
+
## 📚 Related Projects
|
|
239
|
+
|
|
240
|
+
- [FastAPI](https://fastapi.tiangolo.com/) - Modern web framework for Python
|
|
241
|
+
- [Prometheus](https://prometheus.io/) - Monitoring and alerting toolkit
|
|
242
|
+
- [Kubernetes](https://kubernetes.io/) - Container orchestration platform
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# Fastuator
|
|
2
|
+
|
|
3
|
+
[](https://github.com/fastuator/fastuator/actions/workflows/ci.yml)
|
|
4
|
+
[](https://github.com/fastuator/fastuator)
|
|
5
|
+
[](https://www.python.org/downloads/)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
Production-ready monitoring toolkit for FastAPI applications.
|
|
9
|
+
Kubernetes probes, Prometheus metrics, and health checks in one line.
|
|
10
|
+
|
|
11
|
+
## ✨ Features
|
|
12
|
+
|
|
13
|
+
- 🏥 **Health Checks**: Aggregated health status with customizable checks
|
|
14
|
+
- 🔍 **K8s Probes**: Built-in liveness and readiness endpoints
|
|
15
|
+
- 📊 **Prometheus Metrics**: Auto-instrumented HTTP metrics
|
|
16
|
+
- ℹ️ **System Info**: Build version and platform details
|
|
17
|
+
- 🎯 **Zero Config**: Works out of the box with sensible defaults
|
|
18
|
+
- ⚡ **100% Test Coverage**: Battle-tested and production-ready
|
|
19
|
+
|
|
20
|
+
## 📦 Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install fastuator
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## 🚀 Quick Start
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from fastapi import FastAPI
|
|
30
|
+
from fastuator import Fastuator
|
|
31
|
+
|
|
32
|
+
app = FastAPI()
|
|
33
|
+
Fastuator(app) # That's it!
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**Available Endpoints:**
|
|
37
|
+
|
|
38
|
+
| Endpoint | Description |
|
|
39
|
+
|----------|-------------|
|
|
40
|
+
| `GET /fastuator/health` | Aggregated health status with optional details |
|
|
41
|
+
| `GET /fastuator/liveness` | Kubernetes liveness probe (critical checks only) |
|
|
42
|
+
| `GET /fastuator/readiness` | Kubernetes readiness probe (all dependencies) |
|
|
43
|
+
| `GET /fastuator/metrics` | Prometheus-compatible metrics |
|
|
44
|
+
| `GET /fastuator/info` | Application and system information |
|
|
45
|
+
|
|
46
|
+
## 📖 Usage
|
|
47
|
+
|
|
48
|
+
### Basic Setup
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from fastapi import FastAPI
|
|
52
|
+
from fastuator import Fastuator
|
|
53
|
+
|
|
54
|
+
app = FastAPI()
|
|
55
|
+
|
|
56
|
+
# Use default configuration
|
|
57
|
+
Fastuator(app)
|
|
58
|
+
|
|
59
|
+
# Custom prefix
|
|
60
|
+
Fastuator(app, prefix="/monitoring")
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Custom Health Checks
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from fastuator import Fastuator
|
|
67
|
+
|
|
68
|
+
app = FastAPI()
|
|
69
|
+
|
|
70
|
+
# Define custom health checks
|
|
71
|
+
async def database_health():
|
|
72
|
+
try:
|
|
73
|
+
# Check database connection
|
|
74
|
+
await db.execute("SELECT 1")
|
|
75
|
+
return {"status": "UP", "database": "connected"}
|
|
76
|
+
except Exception as e:
|
|
77
|
+
return {"status": "DOWN", "database": str(e)}
|
|
78
|
+
|
|
79
|
+
async def redis_health():
|
|
80
|
+
try:
|
|
81
|
+
await redis.ping()
|
|
82
|
+
return {"status": "UP", "redis": "connected"}
|
|
83
|
+
except Exception:
|
|
84
|
+
return {"status": "DOWN", "redis": "unreachable"}
|
|
85
|
+
|
|
86
|
+
# Register custom checks
|
|
87
|
+
Fastuator(
|
|
88
|
+
app,
|
|
89
|
+
health_checks=[database_health, redis_health],
|
|
90
|
+
readiness_checks=[database_health, redis_health],
|
|
91
|
+
liveness_checks=[], # No external dependencies for liveness
|
|
92
|
+
)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Health Check Response
|
|
96
|
+
|
|
97
|
+
**Without details:**
|
|
98
|
+
```bash
|
|
99
|
+
curl http://localhost:8000/fastuator/health
|
|
100
|
+
```
|
|
101
|
+
```json
|
|
102
|
+
{"status": "UP"}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**With details:**
|
|
106
|
+
```bash
|
|
107
|
+
curl http://localhost:8000/fastuator/health?show_details=true
|
|
108
|
+
```
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"status": "UP",
|
|
112
|
+
"components": {
|
|
113
|
+
"check_0": {
|
|
114
|
+
"status": "UP",
|
|
115
|
+
"cpu_percent": 45.2
|
|
116
|
+
},
|
|
117
|
+
"check_1": {
|
|
118
|
+
"status": "UP",
|
|
119
|
+
"memory_percent": 62.1,
|
|
120
|
+
"memory_available_mb": 4096
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## ☸️ Kubernetes Integration
|
|
127
|
+
|
|
128
|
+
### Deployment Example
|
|
129
|
+
|
|
130
|
+
```yaml
|
|
131
|
+
apiVersion: apps/v1
|
|
132
|
+
kind: Deployment
|
|
133
|
+
metadata:
|
|
134
|
+
name: fastapi-app
|
|
135
|
+
spec:
|
|
136
|
+
template:
|
|
137
|
+
spec:
|
|
138
|
+
containers:
|
|
139
|
+
- name: app
|
|
140
|
+
image: myapp:latest
|
|
141
|
+
ports:
|
|
142
|
+
- containerPort: 8000
|
|
143
|
+
livenessProbe:
|
|
144
|
+
httpGet:
|
|
145
|
+
path: /fastuator/liveness
|
|
146
|
+
port: 8000
|
|
147
|
+
initialDelaySeconds: 10
|
|
148
|
+
periodSeconds: 10
|
|
149
|
+
readinessProbe:
|
|
150
|
+
httpGet:
|
|
151
|
+
path: /fastuator/readiness
|
|
152
|
+
port: 8000
|
|
153
|
+
initialDelaySeconds: 5
|
|
154
|
+
periodSeconds: 5
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## 📊 Prometheus Metrics
|
|
158
|
+
|
|
159
|
+
Fastuator automatically collects the following metrics:
|
|
160
|
+
|
|
161
|
+
- `http_requests_total`: Total HTTP requests (counter)
|
|
162
|
+
- `http_request_duration_seconds`: Request duration histogram
|
|
163
|
+
- `app_health_status`: Health status gauge (1=UP, 0=DOWN)
|
|
164
|
+
|
|
165
|
+
**Scrape configuration:**
|
|
166
|
+
```yaml
|
|
167
|
+
scrape_configs:
|
|
168
|
+
- job_name: 'fastapi'
|
|
169
|
+
static_configs:
|
|
170
|
+
- targets: ['localhost:8000']
|
|
171
|
+
metrics_path: '/fastuator/metrics'
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## ⚙️ Configuration
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
Fastuator(
|
|
178
|
+
app,
|
|
179
|
+
prefix="/fastuator", # URL prefix for all endpoints
|
|
180
|
+
health_checks=[...], # List of health check functions
|
|
181
|
+
liveness_checks=[...], # Checks for liveness probe
|
|
182
|
+
readiness_checks=[...], # Checks for readiness probe
|
|
183
|
+
enable_metrics=True, # Enable Prometheus metrics
|
|
184
|
+
)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## 🧪 Built-in Health Checks
|
|
188
|
+
|
|
189
|
+
Fastuator includes these health checks by default:
|
|
190
|
+
|
|
191
|
+
- **CPU Usage**: Reports DOWN if CPU > 90%
|
|
192
|
+
- **Memory Usage**: Reports DOWN if memory > 90%
|
|
193
|
+
- **Disk Usage**: Reports DOWN if disk > 90%
|
|
194
|
+
|
|
195
|
+
## 🤝 Contributing
|
|
196
|
+
|
|
197
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
198
|
+
|
|
199
|
+
1. Fork the repository
|
|
200
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
201
|
+
3. Run tests (`pytest --cov=fastuator`)
|
|
202
|
+
4. Commit your changes (`git commit -m 'Add amazing feature'`)
|
|
203
|
+
5. Push to the branch (`git push origin feature/amazing-feature`)
|
|
204
|
+
6. Open a Pull Request
|
|
205
|
+
|
|
206
|
+
## 📄 License
|
|
207
|
+
|
|
208
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
209
|
+
|
|
210
|
+
## 🙏 Acknowledgments
|
|
211
|
+
|
|
212
|
+
Inspired by [Spring Boot Actuator](https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html).
|
|
213
|
+
|
|
214
|
+
## 📚 Related Projects
|
|
215
|
+
|
|
216
|
+
- [FastAPI](https://fastapi.tiangolo.com/) - Modern web framework for Python
|
|
217
|
+
- [Prometheus](https://prometheus.io/) - Monitoring and alerting toolkit
|
|
218
|
+
- [Kubernetes](https://kubernetes.io/) - Container orchestration platform
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""
|
|
2
|
+
fastuator - Production-ready monitoring for FastAPI
|
|
3
|
+
K8s probes, Prometheus metrics, health checks in 1 line.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
__version__ = "0.0.1"
|
|
7
|
+
|
|
8
|
+
from .core import Fastuator
|
|
9
|
+
from .checks import cpu_health, disk_health, memory_health
|
|
10
|
+
|
|
11
|
+
__all__ = ["Fastuator", "cpu_health", "disk_health", "memory_health"]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Default health check implementations."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
import psutil
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
async def cpu_health() -> dict[str, Any]:
|
|
8
|
+
"""Check CPU usage."""
|
|
9
|
+
cpu_percent = psutil.cpu_percent(interval=0.1)
|
|
10
|
+
status = "UP" if cpu_percent < 90 else "DOWN"
|
|
11
|
+
return {
|
|
12
|
+
"status": status,
|
|
13
|
+
"cpu_percent": cpu_percent,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def memory_health() -> dict[str, Any]:
|
|
18
|
+
"""Check memory usage."""
|
|
19
|
+
memory = psutil.virtual_memory()
|
|
20
|
+
status = "UP" if memory.percent < 90 else "DOWN"
|
|
21
|
+
return {
|
|
22
|
+
"status": status,
|
|
23
|
+
"memory_percent": memory.percent,
|
|
24
|
+
"memory_available_mb": memory.available // (1024 * 1024),
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
async def disk_health() -> dict[str, Any]:
|
|
29
|
+
"""Check disk usage."""
|
|
30
|
+
disk = psutil.disk_usage("/")
|
|
31
|
+
status = "UP" if disk.percent < 90 else "DOWN"
|
|
32
|
+
return {
|
|
33
|
+
"status": status,
|
|
34
|
+
"disk_percent": disk.percent,
|
|
35
|
+
"disk_free_gb": disk.free // (1024 ** 3),
|
|
36
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core implementation of Fastuator monitoring toolkit.
|
|
3
|
+
|
|
4
|
+
This module provides the main Fastuator class that automatically registers
|
|
5
|
+
health check, metrics, and info endpoints for FastAPI applications.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any, Awaitable, Callable
|
|
11
|
+
import asyncio
|
|
12
|
+
import time
|
|
13
|
+
|
|
14
|
+
from fastapi import APIRouter, FastAPI, HTTPException, Request
|
|
15
|
+
from prometheus_client import Counter, Gauge, Histogram, make_asgi_app
|
|
16
|
+
import psutil
|
|
17
|
+
|
|
18
|
+
# Prometheus metrics collectors
|
|
19
|
+
REQUEST_COUNT = Counter(
|
|
20
|
+
"http_requests_total",
|
|
21
|
+
"Total HTTP requests",
|
|
22
|
+
["method", "endpoint", "status"],
|
|
23
|
+
)
|
|
24
|
+
REQUEST_DURATION = Histogram(
|
|
25
|
+
"http_request_duration_seconds",
|
|
26
|
+
"HTTP request duration in seconds",
|
|
27
|
+
)
|
|
28
|
+
HEALTH_STATUS = Gauge(
|
|
29
|
+
"app_health_status",
|
|
30
|
+
"Application health status (1=UP, 0=DOWN)",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Fastuator:
|
|
35
|
+
"""
|
|
36
|
+
Production-ready monitoring toolkit for FastAPI applications.
|
|
37
|
+
|
|
38
|
+
Fastuator automatically registers the following endpoints:
|
|
39
|
+
- /fastuator/health: Aggregated health check with optional details
|
|
40
|
+
- /fastuator/liveness: Kubernetes liveness probe
|
|
41
|
+
- /fastuator/readiness: Kubernetes readiness probe
|
|
42
|
+
- /fastuator/info: Build and system information
|
|
43
|
+
- /fastuator/metrics: Prometheus metrics (optional)
|
|
44
|
+
|
|
45
|
+
The health check aggregation follows the Spring Boot Actuator pattern:
|
|
46
|
+
if any component is DOWN, the overall status is DOWN.
|
|
47
|
+
|
|
48
|
+
Example:
|
|
49
|
+
>>> from fastapi import FastAPI
|
|
50
|
+
>>> from fastuator import Fastuator
|
|
51
|
+
>>>
|
|
52
|
+
>>> app = FastAPI()
|
|
53
|
+
>>> Fastuator(app) # Registers all fastuator endpoints
|
|
54
|
+
>>>
|
|
55
|
+
>>> # With custom health checks
|
|
56
|
+
>>> async def redis_health():
|
|
57
|
+
... return {"status": "UP", "redis": "connected"}
|
|
58
|
+
>>>
|
|
59
|
+
>>> Fastuator(app, health_checks=[redis_health])
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
app: FastAPI application instance
|
|
63
|
+
prefix: URL prefix for fastuator endpoints (default: "/fastuator")
|
|
64
|
+
health_checks: Custom health check functions (async callables)
|
|
65
|
+
liveness_checks: Health checks for K8s liveness probe (default: CPU only)
|
|
66
|
+
readiness_checks: Health checks for K8s readiness probe (default: all checks)
|
|
67
|
+
enable_metrics: Enable Prometheus metrics collection (default: True)
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
app: FastAPI,
|
|
73
|
+
prefix: str = "/fastuator",
|
|
74
|
+
health_checks: list[Callable[[], Awaitable[dict[str, Any]]]] | None = None,
|
|
75
|
+
liveness_checks: list[Callable[[], Awaitable[dict[str, Any]]]] | None = None,
|
|
76
|
+
readiness_checks: list[Callable[[], Awaitable[dict[str, Any]]]] | None = None,
|
|
77
|
+
enable_metrics: bool = True,
|
|
78
|
+
) -> None:
|
|
79
|
+
self.app = app
|
|
80
|
+
self.prefix = prefix
|
|
81
|
+
self.router = APIRouter(prefix=prefix, tags=["fastuator"])
|
|
82
|
+
|
|
83
|
+
# Import default health checks
|
|
84
|
+
from .checks import cpu_health, disk_health, memory_health
|
|
85
|
+
|
|
86
|
+
# Configure health check functions
|
|
87
|
+
self.health_checks = health_checks or [cpu_health, disk_health, memory_health]
|
|
88
|
+
self.liveness_checks = liveness_checks or [cpu_health]
|
|
89
|
+
self.readiness_checks = readiness_checks or self.health_checks
|
|
90
|
+
|
|
91
|
+
# Setup endpoints
|
|
92
|
+
self._register_health_endpoints()
|
|
93
|
+
|
|
94
|
+
# Setup Prometheus metrics if enabled
|
|
95
|
+
if enable_metrics:
|
|
96
|
+
self._register_metrics_endpoint(app, prefix)
|
|
97
|
+
self._register_metrics_middleware(app)
|
|
98
|
+
|
|
99
|
+
# Register router with FastAPI app
|
|
100
|
+
app.include_router(self.router)
|
|
101
|
+
|
|
102
|
+
def _register_health_endpoints(self) -> None:
|
|
103
|
+
"""Register health check, liveness, readiness, and info endpoints."""
|
|
104
|
+
|
|
105
|
+
@self.router.get("/health")
|
|
106
|
+
async def health(show_details: bool = False) -> dict[str, Any]:
|
|
107
|
+
"""
|
|
108
|
+
Aggregate health check endpoint.
|
|
109
|
+
|
|
110
|
+
Returns overall status based on all registered health checks.
|
|
111
|
+
If any check returns DOWN, the overall status is DOWN.
|
|
112
|
+
|
|
113
|
+
Query Parameters:
|
|
114
|
+
show_details: Include detailed component status (default: False)
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
{"status": "UP"} or {"status": "DOWN", "components": {...}}
|
|
118
|
+
"""
|
|
119
|
+
checks = await asyncio.gather(
|
|
120
|
+
*[check() for check in self.health_checks],
|
|
121
|
+
return_exceptions=True,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Handle exceptions in health checks
|
|
125
|
+
processed_checks = []
|
|
126
|
+
for i, check_result in enumerate(checks):
|
|
127
|
+
if isinstance(check_result, Exception):
|
|
128
|
+
processed_checks.append(
|
|
129
|
+
{
|
|
130
|
+
"status": "DOWN",
|
|
131
|
+
"error": str(check_result),
|
|
132
|
+
}
|
|
133
|
+
)
|
|
134
|
+
else:
|
|
135
|
+
processed_checks.append(check_result)
|
|
136
|
+
|
|
137
|
+
# Aggregate status: DOWN if any component is DOWN
|
|
138
|
+
statuses = [c.get("status", "DOWN") for c in processed_checks]
|
|
139
|
+
overall_status = "DOWN" if "DOWN" in statuses else "UP"
|
|
140
|
+
|
|
141
|
+
# Update Prometheus gauge
|
|
142
|
+
HEALTH_STATUS.set(1 if overall_status == "UP" else 0)
|
|
143
|
+
|
|
144
|
+
result = {"status": overall_status}
|
|
145
|
+
if show_details:
|
|
146
|
+
result["components"] = {
|
|
147
|
+
f"check_{i}": check for i, check in enumerate(processed_checks)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return result
|
|
151
|
+
|
|
152
|
+
@self.router.get("/liveness")
|
|
153
|
+
async def liveness() -> dict[str, str]:
|
|
154
|
+
"""
|
|
155
|
+
Kubernetes liveness probe endpoint.
|
|
156
|
+
|
|
157
|
+
Checks only critical system components (e.g., CPU).
|
|
158
|
+
Returns 503 if any liveness check fails.
|
|
159
|
+
|
|
160
|
+
This follows the isolation pattern: external dependencies
|
|
161
|
+
(DB, Redis) should not affect liveness to prevent cascading failures.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
{"status": "UP"} or raises HTTPException(503)
|
|
165
|
+
"""
|
|
166
|
+
checks = await asyncio.gather(
|
|
167
|
+
*[check() for check in self.liveness_checks],
|
|
168
|
+
return_exceptions=True,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
for check_result in checks:
|
|
172
|
+
if isinstance(check_result, Exception) or check_result.get("status") != "UP":
|
|
173
|
+
raise HTTPException(
|
|
174
|
+
status_code=503,
|
|
175
|
+
detail="Liveness check failed",
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
return {"status": "UP"}
|
|
179
|
+
|
|
180
|
+
@self.router.get("/readiness")
|
|
181
|
+
async def readiness() -> dict[str, str]:
|
|
182
|
+
"""
|
|
183
|
+
Kubernetes readiness probe endpoint.
|
|
184
|
+
|
|
185
|
+
Checks all dependencies including external services.
|
|
186
|
+
Returns 503 if any readiness check fails.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
{"status": "UP"} or raises HTTPException(503)
|
|
190
|
+
"""
|
|
191
|
+
checks = await asyncio.gather(
|
|
192
|
+
*[check() for check in self.readiness_checks],
|
|
193
|
+
return_exceptions=True,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
for check_result in checks:
|
|
197
|
+
if isinstance(check_result, Exception) or check_result.get("status") != "UP":
|
|
198
|
+
raise HTTPException(
|
|
199
|
+
status_code=503,
|
|
200
|
+
detail="Readiness check failed",
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
return {"status": "UP"}
|
|
204
|
+
|
|
205
|
+
@self.router.get("/info")
|
|
206
|
+
async def info() -> dict[str, Any]:
|
|
207
|
+
"""
|
|
208
|
+
Application and system information endpoint.
|
|
209
|
+
|
|
210
|
+
Returns build version, Python version, and platform details.
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
Dictionary with build and system information
|
|
214
|
+
"""
|
|
215
|
+
import platform
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
"build": {
|
|
219
|
+
"version": "0.0.1",
|
|
220
|
+
"python": platform.python_version(),
|
|
221
|
+
},
|
|
222
|
+
"system": {
|
|
223
|
+
"platform": platform.platform(),
|
|
224
|
+
"python_implementation": platform.python_implementation(),
|
|
225
|
+
},
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
def _register_metrics_endpoint(self, app: FastAPI, prefix: str) -> None:
|
|
229
|
+
"""
|
|
230
|
+
Mount Prometheus metrics endpoint.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
app: FastAPI application instance
|
|
234
|
+
prefix: Fastuator URL prefix
|
|
235
|
+
"""
|
|
236
|
+
metrics_app = make_asgi_app()
|
|
237
|
+
app.mount(f"{prefix}/metrics", metrics_app)
|
|
238
|
+
|
|
239
|
+
def _register_metrics_middleware(self, app: FastAPI) -> None:
|
|
240
|
+
"""
|
|
241
|
+
Register middleware for automatic metrics collection.
|
|
242
|
+
|
|
243
|
+
Collects HTTP request count and duration for all endpoints.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
app: FastAPI application instance
|
|
247
|
+
"""
|
|
248
|
+
|
|
249
|
+
@app.middleware("http")
|
|
250
|
+
async def metrics_middleware(request: Request, call_next):
|
|
251
|
+
"""Collect HTTP request metrics."""
|
|
252
|
+
start_time = time.time()
|
|
253
|
+
|
|
254
|
+
# Process request
|
|
255
|
+
response = await call_next(request)
|
|
256
|
+
|
|
257
|
+
# Record metrics
|
|
258
|
+
duration = time.time() - start_time
|
|
259
|
+
REQUEST_COUNT.labels(
|
|
260
|
+
method=request.method,
|
|
261
|
+
endpoint=request.url.path,
|
|
262
|
+
status=response.status_code,
|
|
263
|
+
).inc()
|
|
264
|
+
REQUEST_DURATION.observe(duration)
|
|
265
|
+
|
|
266
|
+
return response
|
fastuator-0.0.1/main.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# This is a sample Python script.
|
|
2
|
+
|
|
3
|
+
# Press Shift+F10 to execute it or replace it with your code.
|
|
4
|
+
# Press Double Shift to search everywhere for classes, files, tool windows, actions, and settings.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def print_hi(name):
|
|
8
|
+
# Use a breakpoint in the code line below to debug your script.
|
|
9
|
+
print(f'Hi, {name}') # Press Ctrl+F8 to toggle the breakpoint.
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Press the green button in the gutter to run the script.
|
|
13
|
+
if __name__ == '__main__':
|
|
14
|
+
print_hi('PyCharm')
|
|
15
|
+
|
|
16
|
+
# See PyCharm help at https://www.jetbrains.com/help/pycharm/
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "fastuator"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "Production-ready actuator endpoints for FastAPI (health, metrics, info)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [{name = "Noh Hyeon Nam", email = "shgusska12@gmail.com"}]
|
|
13
|
+
|
|
14
|
+
dependencies = [
|
|
15
|
+
"fastapi>=0.115.0",
|
|
16
|
+
"pydantic>=2.0.0",
|
|
17
|
+
"psutil>=5.9.0",
|
|
18
|
+
"prometheus-client>=0.20.0",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.optional-dependencies]
|
|
22
|
+
dev = [
|
|
23
|
+
"pytest>=7.4.0",
|
|
24
|
+
"pytest-asyncio>=0.21.0",
|
|
25
|
+
"pytest-cov>=4.1.0",
|
|
26
|
+
"httpx>=0.25.0",
|
|
27
|
+
"uvicorn[standard]>=0.32.0",
|
|
28
|
+
"black>=24.0.0",
|
|
29
|
+
"ruff>=0.1.0",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/fastuator/fastuator"
|
|
34
|
+
Repository = "https://github.com/fastuator/fastuator"
|
|
35
|
+
|
|
36
|
+
[tool.hatch.build.targets.wheel]
|
|
37
|
+
packages = ["fastuator"]
|
|
38
|
+
|
|
39
|
+
[tool.hatch.build.targets.sdist]
|
|
40
|
+
exclude = [
|
|
41
|
+
"/.venv",
|
|
42
|
+
"/.git",
|
|
43
|
+
"/__pycache__",
|
|
44
|
+
"*.pyc",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[tool.black]
|
|
48
|
+
line-length = 100
|
|
49
|
+
target-version = ["py310", "py311", "py312"]
|
|
50
|
+
|
|
51
|
+
[tool.ruff]
|
|
52
|
+
line-length = 100
|
|
53
|
+
target-version = "py310"
|
|
54
|
+
|
|
55
|
+
[tool.ruff.lint]
|
|
56
|
+
select = ["E", "F", "I", "N", "W"]
|
|
57
|
+
ignore = []
|
|
File without changes
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Test configuration and fixtures."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from fastapi import FastAPI
|
|
5
|
+
from fastapi.testclient import TestClient
|
|
6
|
+
from fastuator import Fastuator
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.fixture
|
|
10
|
+
def client():
|
|
11
|
+
"""Create a test client with default health checks.
|
|
12
|
+
|
|
13
|
+
Uses real CPU/memory/disk checks - may fail if system resources are high.
|
|
14
|
+
"""
|
|
15
|
+
app = FastAPI()
|
|
16
|
+
Fastuator(app)
|
|
17
|
+
return TestClient(app)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@pytest.fixture
|
|
21
|
+
def success_client():
|
|
22
|
+
"""Create a test client with always-passing checks.
|
|
23
|
+
|
|
24
|
+
Guarantees 200 OK responses for testing happy paths.
|
|
25
|
+
"""
|
|
26
|
+
app = FastAPI()
|
|
27
|
+
|
|
28
|
+
async def passing_check():
|
|
29
|
+
return {"status": "UP"}
|
|
30
|
+
|
|
31
|
+
Fastuator(
|
|
32
|
+
app,
|
|
33
|
+
health_checks=[passing_check],
|
|
34
|
+
readiness_checks=[passing_check],
|
|
35
|
+
liveness_checks=[passing_check],
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
return TestClient(app)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@pytest.fixture
|
|
42
|
+
def failing_client():
|
|
43
|
+
"""Create a test client with always-failing checks.
|
|
44
|
+
|
|
45
|
+
Returns DOWN status without exceptions.
|
|
46
|
+
"""
|
|
47
|
+
app = FastAPI()
|
|
48
|
+
|
|
49
|
+
async def failing_check():
|
|
50
|
+
return {"status": "DOWN", "reason": "Test failure"}
|
|
51
|
+
|
|
52
|
+
Fastuator(
|
|
53
|
+
app,
|
|
54
|
+
health_checks=[failing_check],
|
|
55
|
+
readiness_checks=[failing_check],
|
|
56
|
+
liveness_checks=[failing_check],
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
return TestClient(app)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@pytest.fixture
|
|
63
|
+
def exception_client():
|
|
64
|
+
"""Create a test client with exception-raising health checks.
|
|
65
|
+
|
|
66
|
+
Tests exception handling in health endpoint (line 128 in core.py).
|
|
67
|
+
"""
|
|
68
|
+
app = FastAPI()
|
|
69
|
+
|
|
70
|
+
async def exception_check():
|
|
71
|
+
raise RuntimeError("Database connection failed")
|
|
72
|
+
|
|
73
|
+
Fastuator(app, health_checks=[exception_check])
|
|
74
|
+
|
|
75
|
+
return TestClient(app)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@pytest.fixture
|
|
79
|
+
def exception_readiness_client():
|
|
80
|
+
"""Create a test client with exception-raising readiness checks.
|
|
81
|
+
|
|
82
|
+
Tests exception handling in readiness endpoint (line 207 in core.py).
|
|
83
|
+
"""
|
|
84
|
+
app = FastAPI()
|
|
85
|
+
|
|
86
|
+
async def exception_check():
|
|
87
|
+
raise RuntimeError("Service unavailable")
|
|
88
|
+
|
|
89
|
+
Fastuator(app, readiness_checks=[exception_check])
|
|
90
|
+
|
|
91
|
+
return TestClient(app)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Test basic Fastuator endpoints."""
|
|
2
|
+
|
|
3
|
+
from fastapi.testclient import TestClient
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_health_endpoint(client: TestClient):
|
|
7
|
+
"""Test health check endpoint returns UP status."""
|
|
8
|
+
response = client.get("/fastuator/health")
|
|
9
|
+
assert response.status_code == 200
|
|
10
|
+
|
|
11
|
+
data = response.json()
|
|
12
|
+
assert data["status"] in ["UP", "DOWN"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_liveness_endpoint(success_client: TestClient):
|
|
16
|
+
"""Test liveness probe endpoint."""
|
|
17
|
+
response = success_client.get("/fastuator/liveness")
|
|
18
|
+
assert response.status_code == 200
|
|
19
|
+
assert response.json()["status"] == "UP"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_readiness_endpoint_flexible(client: TestClient):
|
|
23
|
+
"""Test readiness with real health checks (may vary)."""
|
|
24
|
+
response = client.get("/fastuator/readiness")
|
|
25
|
+
assert response.status_code in [200, 503]
|
|
26
|
+
|
|
27
|
+
if response.status_code == 200:
|
|
28
|
+
assert response.json() == {"status": "UP"}
|
|
29
|
+
else:
|
|
30
|
+
assert "detail" in response.json()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_readiness_success(success_client: TestClient):
|
|
34
|
+
"""Test readiness returns UP when all checks pass."""
|
|
35
|
+
response = success_client.get("/fastuator/readiness")
|
|
36
|
+
assert response.status_code == 200
|
|
37
|
+
assert response.json() == {"status": "UP"}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_info_endpoint(client: TestClient):
|
|
41
|
+
"""Test system info endpoint."""
|
|
42
|
+
response = client.get("/fastuator/info")
|
|
43
|
+
assert response.status_code == 200
|
|
44
|
+
|
|
45
|
+
data = response.json()
|
|
46
|
+
|
|
47
|
+
assert "build" in data
|
|
48
|
+
assert "system" in data
|
|
49
|
+
assert "version" in data["build"]
|
|
50
|
+
assert "python" in data["build"]
|
|
51
|
+
assert "platform" in data["system"]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_metrics_endpoint(client: TestClient):
|
|
55
|
+
"""Test Prometheus metrics endpoint."""
|
|
56
|
+
response = client.get("/fastuator/metrics")
|
|
57
|
+
assert response.status_code == 200
|
|
58
|
+
|
|
59
|
+
assert "text/plain" in response.headers["content-type"]
|
|
60
|
+
|
|
61
|
+
content = response.text
|
|
62
|
+
assert len(content) > 0
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_health_with_details(client: TestClient):
|
|
66
|
+
"""Test health endpoint with show_details parameter."""
|
|
67
|
+
response = client.get("/fastuator/health?show_details=true")
|
|
68
|
+
assert response.status_code in [200, 503]
|
|
69
|
+
|
|
70
|
+
data = response.json()
|
|
71
|
+
assert "status" in data
|
|
72
|
+
assert "components" in data
|
|
73
|
+
assert isinstance(data["components"], dict)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_health_with_exception(exception_client: TestClient):
|
|
77
|
+
"""Test health endpoint handles exceptions from checks."""
|
|
78
|
+
response = exception_client.get("/fastuator/health")
|
|
79
|
+
assert response.status_code == 200
|
|
80
|
+
|
|
81
|
+
data = response.json()
|
|
82
|
+
assert data["status"] == "DOWN"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_health_with_exception_details(exception_client: TestClient):
|
|
86
|
+
"""Test health endpoint captures exception in components when show_details=true."""
|
|
87
|
+
response = exception_client.get("/fastuator/health?show_details=true")
|
|
88
|
+
|
|
89
|
+
data = response.json()
|
|
90
|
+
assert "components" in data
|
|
91
|
+
components = data["components"]
|
|
92
|
+
assert any("error" in comp for comp in components.values())
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_failing_liveness(failing_client: TestClient):
|
|
96
|
+
"""Test liveness with failing check returns 503."""
|
|
97
|
+
response = failing_client.get("/fastuator/liveness")
|
|
98
|
+
assert response.status_code == 503
|
|
99
|
+
|
|
100
|
+
data = response.json()
|
|
101
|
+
assert "detail" in data
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_failing_readiness(failing_client: TestClient):
|
|
105
|
+
"""Test readiness with failing check returns 503."""
|
|
106
|
+
response = failing_client.get("/fastuator/readiness")
|
|
107
|
+
assert response.status_code == 503
|
|
108
|
+
|
|
109
|
+
data = response.json()
|
|
110
|
+
assert "detail" in data
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_readiness_with_exception(exception_readiness_client: TestClient):
|
|
114
|
+
"""Test readiness with exception returns 503."""
|
|
115
|
+
response = exception_readiness_client.get("/fastuator/readiness")
|
|
116
|
+
assert response.status_code == 503
|
|
117
|
+
|
|
118
|
+
data = response.json()
|
|
119
|
+
assert "detail" in data
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Test health check implementations."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from fastuator.checks import cpu_health, memory_health, disk_health
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@pytest.mark.asyncio
|
|
8
|
+
async def test_cpu_health():
|
|
9
|
+
"""Test CPU health check returns correct format."""
|
|
10
|
+
result = await cpu_health()
|
|
11
|
+
|
|
12
|
+
assert "status" in result
|
|
13
|
+
assert result["status"] in ["UP", "DOWN"]
|
|
14
|
+
assert "cpu_percent" in result
|
|
15
|
+
assert isinstance(result["cpu_percent"], (int, float))
|
|
16
|
+
assert 0 <= result["cpu_percent"] <= 100
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest.mark.asyncio
|
|
20
|
+
async def test_memory_health():
|
|
21
|
+
"""Test memory health check returns correct format."""
|
|
22
|
+
result = await memory_health()
|
|
23
|
+
|
|
24
|
+
assert "status" in result
|
|
25
|
+
assert result["status"] in ["UP", "DOWN"]
|
|
26
|
+
assert "memory_percent" in result
|
|
27
|
+
assert "memory_available_mb" in result
|
|
28
|
+
assert isinstance(result["memory_percent"], (int, float))
|
|
29
|
+
assert isinstance(result["memory_available_mb"], int)
|
|
30
|
+
assert 0 <= result["memory_percent"] <= 100
|
|
31
|
+
assert result["memory_available_mb"] >= 0
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@pytest.mark.asyncio
|
|
35
|
+
async def test_disk_health():
|
|
36
|
+
"""Test disk health check returns correct format."""
|
|
37
|
+
result = await disk_health()
|
|
38
|
+
|
|
39
|
+
assert "status" in result
|
|
40
|
+
assert result["status"] in ["UP", "DOWN"]
|
|
41
|
+
assert "disk_percent" in result
|
|
42
|
+
assert "disk_free_gb" in result
|
|
43
|
+
assert isinstance(result["disk_percent"], (int, float))
|
|
44
|
+
assert isinstance(result["disk_free_gb"], int)
|
|
45
|
+
assert 0 <= result["disk_percent"] <= 100
|
|
46
|
+
assert result["disk_free_gb"] >= 0
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@pytest.mark.asyncio
|
|
50
|
+
async def test_cpu_health_status_logic():
|
|
51
|
+
"""Test CPU health returns DOWN when usage is high."""
|
|
52
|
+
result = await cpu_health()
|
|
53
|
+
|
|
54
|
+
if result["cpu_percent"] < 90:
|
|
55
|
+
assert result["status"] == "UP"
|
|
56
|
+
else:
|
|
57
|
+
assert result["status"] == "DOWN"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@pytest.mark.asyncio
|
|
61
|
+
async def test_memory_health_status_logic():
|
|
62
|
+
"""Test memory health returns DOWN when usage is high."""
|
|
63
|
+
result = await memory_health()
|
|
64
|
+
|
|
65
|
+
if result["memory_percent"] < 90:
|
|
66
|
+
assert result["status"] == "UP"
|
|
67
|
+
else:
|
|
68
|
+
assert result["status"] == "DOWN"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@pytest.mark.asyncio
|
|
72
|
+
async def test_disk_health_status_logic():
|
|
73
|
+
"""Test disk health returns DOWN when usage is high."""
|
|
74
|
+
result = await disk_health()
|
|
75
|
+
|
|
76
|
+
if result["disk_percent"] < 90:
|
|
77
|
+
assert result["status"] == "UP"
|
|
78
|
+
else:
|
|
79
|
+
assert result["status"] == "DOWN"
|