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.
@@ -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
@@ -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.
@@ -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
+ [![CI](https://github.com/fastuator/fastuator/actions/workflows/test.yml/badge.svg)](https://github.com/fastuator/fastuator/actions/workflows/ci.yml)
28
+ [![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://github.com/fastuator/fastuator)
29
+ [![Python](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
30
+ [![License](https://img.shields.io/badge/license-MIT-green.svg)](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
+ [![CI](https://github.com/fastuator/fastuator/actions/workflows/test.yml/badge.svg)](https://github.com/fastuator/fastuator/actions/workflows/ci.yml)
4
+ [![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://github.com/fastuator/fastuator)
5
+ [![Python](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
6
+ [![License](https://img.shields.io/badge/license-MIT-green.svg)](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,9 @@
1
+ from fastapi import FastAPI
2
+ from fastuator import Fastuator
3
+
4
+ app = FastAPI()
5
+ Fastuator(app)
6
+
7
+ if __name__ == "__main__":
8
+ import uvicorn
9
+ uvicorn.run(app, host="0.0.0.0", port=8000)
@@ -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
@@ -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 = []
@@ -0,0 +1,13 @@
1
+ [pytest]
2
+ testpaths = tests
3
+ python_files = test_*.py
4
+ python_classes = Test*
5
+ python_functions = test_*
6
+ addopts =
7
+ -v
8
+ --strict-markers
9
+ --tb=short
10
+ --cov=fastuator
11
+ --cov-report=term-missing
12
+ --cov-report=html
13
+ asyncio_mode = auto
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"