fastapi-armor-lib 0.1.0__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_armor_lib-0.1.0/PKG-INFO +140 -0
- fastapi_armor_lib-0.1.0/README.md +127 -0
- fastapi_armor_lib-0.1.0/fastapi_armor/__init__.py +3 -0
- fastapi_armor_lib-0.1.0/fastapi_armor/armor.py +27 -0
- fastapi_armor_lib-0.1.0/fastapi_armor/error_handler.py +36 -0
- fastapi_armor_lib-0.1.0/fastapi_armor/logger.py +37 -0
- fastapi_armor_lib-0.1.0/fastapi_armor/sentry.py +19 -0
- fastapi_armor_lib-0.1.0/fastapi_armor_lib.egg-info/PKG-INFO +140 -0
- fastapi_armor_lib-0.1.0/fastapi_armor_lib.egg-info/SOURCES.txt +13 -0
- fastapi_armor_lib-0.1.0/fastapi_armor_lib.egg-info/dependency_links.txt +1 -0
- fastapi_armor_lib-0.1.0/fastapi_armor_lib.egg-info/requires.txt +7 -0
- fastapi_armor_lib-0.1.0/fastapi_armor_lib.egg-info/top_level.txt +1 -0
- fastapi_armor_lib-0.1.0/pyproject.toml +20 -0
- fastapi_armor_lib-0.1.0/setup.cfg +4 -0
- fastapi_armor_lib-0.1.0/tests/test_armor.py +63 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fastapi-armor-lib
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Production-ready error handling, logging, and monitoring for FastAPI apps
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: fastapi>=0.100.0
|
|
8
|
+
Requires-Dist: sentry-sdk[fastapi]>=1.40.0
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: pytest; extra == "dev"
|
|
11
|
+
Requires-Dist: httpx; extra == "dev"
|
|
12
|
+
Requires-Dist: uvicorn; extra == "dev"
|
|
13
|
+
|
|
14
|
+
# fastapi-armor 🛡️
|
|
15
|
+
|
|
16
|
+
> Production-ready error handling, logging, and Sentry monitoring for FastAPI — in one line.
|
|
17
|
+
|
|
18
|
+
[](https://www.python.org/)
|
|
19
|
+
[](https://fastapi.tiangolo.com/)
|
|
20
|
+
[](https://opensource.org/licenses/MIT)
|
|
21
|
+
[](https://pypi.org/project/fastapi-armor/)
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Why fastapi-armor?
|
|
26
|
+
|
|
27
|
+
Every production FastAPI app needs the same boilerplate: structured logging, clean error responses, and error monitoring. **fastapi-armor** wires all of that up automatically — no copy-pasting, no missing edge cases.
|
|
28
|
+
|
|
29
|
+
**Before:**
|
|
30
|
+
```python
|
|
31
|
+
from fastapi import FastAPI, HTTPException, Request
|
|
32
|
+
from fastapi.responses import JSONResponse
|
|
33
|
+
import logging, logging.handlers, sentry_sdk
|
|
34
|
+
# ... 60+ lines of setup code
|
|
35
|
+
|
|
36
|
+
app = FastAPI()
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**After:**
|
|
40
|
+
```python
|
|
41
|
+
from fastapi import FastAPI
|
|
42
|
+
from fastapi_armor import Armor
|
|
43
|
+
|
|
44
|
+
app = FastAPI()
|
|
45
|
+
Armor(app) # ✅ Done.
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install fastapi-armor
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Quick Start
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from fastapi import FastAPI
|
|
62
|
+
from fastapi_armor import Armor
|
|
63
|
+
|
|
64
|
+
app = FastAPI()
|
|
65
|
+
|
|
66
|
+
Armor(
|
|
67
|
+
app,
|
|
68
|
+
log_file="logs/app.log",
|
|
69
|
+
sentry_dsn="https://your-dsn@sentry.io/123", # optional
|
|
70
|
+
environment="production",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
@app.get("/")
|
|
74
|
+
def root():
|
|
75
|
+
return {"status": "running"}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
That's it. Your app now has:
|
|
79
|
+
- ✅ Rotating file logs + console output
|
|
80
|
+
- ✅ Clean JSON error responses for all exception types
|
|
81
|
+
- ✅ Sentry integration (if DSN is provided)
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## What It Does
|
|
86
|
+
|
|
87
|
+
### 🔴 Error Handling
|
|
88
|
+
|
|
89
|
+
| Exception Type | Response |
|
|
90
|
+
|---|---|
|
|
91
|
+
| `HTTPException` | `{"detail": "..."}` with original status code |
|
|
92
|
+
| `RequestValidationError` | `{"error": "Validation failed", "detail": [...]}` with 422 |
|
|
93
|
+
| Any other `Exception` | `{"error": "Internal server error"}` with 500 (full traceback logged internally) |
|
|
94
|
+
|
|
95
|
+
All responses use `Content-Type: application/json; charset=utf-8`.
|
|
96
|
+
|
|
97
|
+
### 📋 Logging
|
|
98
|
+
|
|
99
|
+
- **Rotating file logs** — auto-creates the `logs/` directory
|
|
100
|
+
- **Console output** — for local development
|
|
101
|
+
- **Format:** `2024-01-01 12:00:00 | ERROR | Traceback (most recent call last): ...`
|
|
102
|
+
- Named logger: `fastapi_armor`
|
|
103
|
+
|
|
104
|
+
### 🔍 Sentry (optional)
|
|
105
|
+
|
|
106
|
+
Initializes `sentry_sdk` with `FastApiIntegration` and `StarletteIntegration` when a DSN is provided. Private data is not sent by default.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Parameters
|
|
111
|
+
|
|
112
|
+
| Parameter | Type | Default | Description |
|
|
113
|
+
|---|---|---|---|
|
|
114
|
+
| `app` | `FastAPI` | required | Your FastAPI application instance |
|
|
115
|
+
| `log_file` | `str` | `"logs/app.log"` | Path to the rotating log file |
|
|
116
|
+
| `log_max_bytes` | `int` | `5_000_000` | Max size per log file (5 MB) |
|
|
117
|
+
| `log_backup_count` | `int` | `3` | Number of backup log files to keep |
|
|
118
|
+
| `sentry_dsn` | `str` | `""` | Sentry DSN — leave empty to disable |
|
|
119
|
+
| `environment` | `str` | `"development"` | Environment tag sent to Sentry |
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Requirements
|
|
124
|
+
|
|
125
|
+
- Python 3.9+
|
|
126
|
+
- FastAPI 0.100+
|
|
127
|
+
- sentry-sdk 1.40+ *(installed automatically)*
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Development
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
git clone https://github.com/raghadalb-tech/fastapi-armor.git
|
|
135
|
+
cd fastapi-armor
|
|
136
|
+
pip install -e ".[dev]"
|
|
137
|
+
pytest tests/ -v
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Test output:
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# fastapi-armor 🛡️
|
|
2
|
+
|
|
3
|
+
> Production-ready error handling, logging, and Sentry monitoring for FastAPI — in one line.
|
|
4
|
+
|
|
5
|
+
[](https://www.python.org/)
|
|
6
|
+
[](https://fastapi.tiangolo.com/)
|
|
7
|
+
[](https://opensource.org/licenses/MIT)
|
|
8
|
+
[](https://pypi.org/project/fastapi-armor/)
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Why fastapi-armor?
|
|
13
|
+
|
|
14
|
+
Every production FastAPI app needs the same boilerplate: structured logging, clean error responses, and error monitoring. **fastapi-armor** wires all of that up automatically — no copy-pasting, no missing edge cases.
|
|
15
|
+
|
|
16
|
+
**Before:**
|
|
17
|
+
```python
|
|
18
|
+
from fastapi import FastAPI, HTTPException, Request
|
|
19
|
+
from fastapi.responses import JSONResponse
|
|
20
|
+
import logging, logging.handlers, sentry_sdk
|
|
21
|
+
# ... 60+ lines of setup code
|
|
22
|
+
|
|
23
|
+
app = FastAPI()
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**After:**
|
|
27
|
+
```python
|
|
28
|
+
from fastapi import FastAPI
|
|
29
|
+
from fastapi_armor import Armor
|
|
30
|
+
|
|
31
|
+
app = FastAPI()
|
|
32
|
+
Armor(app) # ✅ Done.
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install fastapi-armor
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Quick Start
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from fastapi import FastAPI
|
|
49
|
+
from fastapi_armor import Armor
|
|
50
|
+
|
|
51
|
+
app = FastAPI()
|
|
52
|
+
|
|
53
|
+
Armor(
|
|
54
|
+
app,
|
|
55
|
+
log_file="logs/app.log",
|
|
56
|
+
sentry_dsn="https://your-dsn@sentry.io/123", # optional
|
|
57
|
+
environment="production",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
@app.get("/")
|
|
61
|
+
def root():
|
|
62
|
+
return {"status": "running"}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
That's it. Your app now has:
|
|
66
|
+
- ✅ Rotating file logs + console output
|
|
67
|
+
- ✅ Clean JSON error responses for all exception types
|
|
68
|
+
- ✅ Sentry integration (if DSN is provided)
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## What It Does
|
|
73
|
+
|
|
74
|
+
### 🔴 Error Handling
|
|
75
|
+
|
|
76
|
+
| Exception Type | Response |
|
|
77
|
+
|---|---|
|
|
78
|
+
| `HTTPException` | `{"detail": "..."}` with original status code |
|
|
79
|
+
| `RequestValidationError` | `{"error": "Validation failed", "detail": [...]}` with 422 |
|
|
80
|
+
| Any other `Exception` | `{"error": "Internal server error"}` with 500 (full traceback logged internally) |
|
|
81
|
+
|
|
82
|
+
All responses use `Content-Type: application/json; charset=utf-8`.
|
|
83
|
+
|
|
84
|
+
### 📋 Logging
|
|
85
|
+
|
|
86
|
+
- **Rotating file logs** — auto-creates the `logs/` directory
|
|
87
|
+
- **Console output** — for local development
|
|
88
|
+
- **Format:** `2024-01-01 12:00:00 | ERROR | Traceback (most recent call last): ...`
|
|
89
|
+
- Named logger: `fastapi_armor`
|
|
90
|
+
|
|
91
|
+
### 🔍 Sentry (optional)
|
|
92
|
+
|
|
93
|
+
Initializes `sentry_sdk` with `FastApiIntegration` and `StarletteIntegration` when a DSN is provided. Private data is not sent by default.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Parameters
|
|
98
|
+
|
|
99
|
+
| Parameter | Type | Default | Description |
|
|
100
|
+
|---|---|---|---|
|
|
101
|
+
| `app` | `FastAPI` | required | Your FastAPI application instance |
|
|
102
|
+
| `log_file` | `str` | `"logs/app.log"` | Path to the rotating log file |
|
|
103
|
+
| `log_max_bytes` | `int` | `5_000_000` | Max size per log file (5 MB) |
|
|
104
|
+
| `log_backup_count` | `int` | `3` | Number of backup log files to keep |
|
|
105
|
+
| `sentry_dsn` | `str` | `""` | Sentry DSN — leave empty to disable |
|
|
106
|
+
| `environment` | `str` | `"development"` | Environment tag sent to Sentry |
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Requirements
|
|
111
|
+
|
|
112
|
+
- Python 3.9+
|
|
113
|
+
- FastAPI 0.100+
|
|
114
|
+
- sentry-sdk 1.40+ *(installed automatically)*
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Development
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
git clone https://github.com/raghadalb-tech/fastapi-armor.git
|
|
122
|
+
cd fastapi-armor
|
|
123
|
+
pip install -e ".[dev]"
|
|
124
|
+
pytest tests/ -v
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Test output:
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from fastapi import FastAPI
|
|
2
|
+
|
|
3
|
+
from .error_handler import add_exception_handlers
|
|
4
|
+
from .logger import setup_logger
|
|
5
|
+
from .sentry import init_sentry
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Armor:
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
app: FastAPI,
|
|
12
|
+
log_file: str = "logs/app.log",
|
|
13
|
+
log_max_bytes: int = 5_000_000,
|
|
14
|
+
log_backup_count: int = 3,
|
|
15
|
+
sentry_dsn: str = "",
|
|
16
|
+
environment: str = "development",
|
|
17
|
+
):
|
|
18
|
+
self.app = app
|
|
19
|
+
self.logger = setup_logger(
|
|
20
|
+
log_file=log_file,
|
|
21
|
+
max_bytes=log_max_bytes,
|
|
22
|
+
backup_count=log_backup_count,
|
|
23
|
+
)
|
|
24
|
+
add_exception_handlers(app=self.app, logger=self.logger)
|
|
25
|
+
|
|
26
|
+
if sentry_dsn:
|
|
27
|
+
init_sentry(dsn=sentry_dsn, environment=environment)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from fastapi import FastAPI, HTTPException, Request
|
|
4
|
+
from fastapi.exceptions import RequestValidationError
|
|
5
|
+
from fastapi.responses import JSONResponse
|
|
6
|
+
|
|
7
|
+
JSON_MEDIA_TYPE = "application/json; charset=utf-8"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def add_exception_handlers(app: FastAPI, logger: logging.Logger) -> None:
|
|
11
|
+
@app.exception_handler(HTTPException)
|
|
12
|
+
async def http_exception_handler(_: Request, exc: HTTPException) -> JSONResponse:
|
|
13
|
+
return JSONResponse(
|
|
14
|
+
status_code=exc.status_code,
|
|
15
|
+
content={"detail": exc.detail},
|
|
16
|
+
media_type=JSON_MEDIA_TYPE,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
@app.exception_handler(RequestValidationError)
|
|
20
|
+
async def validation_exception_handler(
|
|
21
|
+
_: Request, exc: RequestValidationError
|
|
22
|
+
) -> JSONResponse:
|
|
23
|
+
return JSONResponse(
|
|
24
|
+
status_code=422,
|
|
25
|
+
content={"error": "Validation failed", "detail": exc.errors()},
|
|
26
|
+
media_type=JSON_MEDIA_TYPE,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
@app.exception_handler(Exception)
|
|
30
|
+
async def generic_exception_handler(_: Request, exc: Exception) -> JSONResponse:
|
|
31
|
+
logger.exception("Unhandled exception occurred", exc_info=exc)
|
|
32
|
+
return JSONResponse(
|
|
33
|
+
status_code=500,
|
|
34
|
+
content={"error": "Internal server error"},
|
|
35
|
+
media_type=JSON_MEDIA_TYPE,
|
|
36
|
+
)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from logging.handlers import RotatingFileHandler
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def setup_logger(
|
|
7
|
+
log_file: str = "logs/app.log",
|
|
8
|
+
max_bytes: int = 5_000_000,
|
|
9
|
+
backup_count: int = 3,
|
|
10
|
+
) -> logging.Logger:
|
|
11
|
+
logger = logging.getLogger("fastapi_armor")
|
|
12
|
+
logger.setLevel(logging.INFO)
|
|
13
|
+
|
|
14
|
+
if logger.handlers:
|
|
15
|
+
return logger
|
|
16
|
+
|
|
17
|
+
log_path = Path(log_file)
|
|
18
|
+
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
19
|
+
|
|
20
|
+
formatter = logging.Formatter("%(asctime)s | %(levelname)s | %(message)s")
|
|
21
|
+
|
|
22
|
+
file_handler = RotatingFileHandler(
|
|
23
|
+
filename=log_path,
|
|
24
|
+
maxBytes=max_bytes,
|
|
25
|
+
backupCount=backup_count,
|
|
26
|
+
encoding="utf-8",
|
|
27
|
+
)
|
|
28
|
+
file_handler.setFormatter(formatter)
|
|
29
|
+
|
|
30
|
+
console_handler = logging.StreamHandler()
|
|
31
|
+
console_handler.setFormatter(formatter)
|
|
32
|
+
|
|
33
|
+
logger.addHandler(file_handler)
|
|
34
|
+
logger.addHandler(console_handler)
|
|
35
|
+
logger.propagate = False
|
|
36
|
+
|
|
37
|
+
return logger
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
import sentry_sdk
|
|
4
|
+
from sentry_sdk.integrations.fastapi import FastApiIntegration
|
|
5
|
+
from sentry_sdk.integrations.starlette import StarletteIntegration
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def init_sentry(dsn: str, environment: str = "development") -> Optional[None]:
|
|
9
|
+
if not dsn:
|
|
10
|
+
return None
|
|
11
|
+
|
|
12
|
+
sentry_sdk.init(
|
|
13
|
+
dsn=dsn,
|
|
14
|
+
environment=environment,
|
|
15
|
+
integrations=[FastApiIntegration(), StarletteIntegration()],
|
|
16
|
+
traces_sample_rate=1.0,
|
|
17
|
+
send_default_pii=False,
|
|
18
|
+
)
|
|
19
|
+
return None
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fastapi-armor-lib
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Production-ready error handling, logging, and monitoring for FastAPI apps
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: fastapi>=0.100.0
|
|
8
|
+
Requires-Dist: sentry-sdk[fastapi]>=1.40.0
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: pytest; extra == "dev"
|
|
11
|
+
Requires-Dist: httpx; extra == "dev"
|
|
12
|
+
Requires-Dist: uvicorn; extra == "dev"
|
|
13
|
+
|
|
14
|
+
# fastapi-armor 🛡️
|
|
15
|
+
|
|
16
|
+
> Production-ready error handling, logging, and Sentry monitoring for FastAPI — in one line.
|
|
17
|
+
|
|
18
|
+
[](https://www.python.org/)
|
|
19
|
+
[](https://fastapi.tiangolo.com/)
|
|
20
|
+
[](https://opensource.org/licenses/MIT)
|
|
21
|
+
[](https://pypi.org/project/fastapi-armor/)
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Why fastapi-armor?
|
|
26
|
+
|
|
27
|
+
Every production FastAPI app needs the same boilerplate: structured logging, clean error responses, and error monitoring. **fastapi-armor** wires all of that up automatically — no copy-pasting, no missing edge cases.
|
|
28
|
+
|
|
29
|
+
**Before:**
|
|
30
|
+
```python
|
|
31
|
+
from fastapi import FastAPI, HTTPException, Request
|
|
32
|
+
from fastapi.responses import JSONResponse
|
|
33
|
+
import logging, logging.handlers, sentry_sdk
|
|
34
|
+
# ... 60+ lines of setup code
|
|
35
|
+
|
|
36
|
+
app = FastAPI()
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**After:**
|
|
40
|
+
```python
|
|
41
|
+
from fastapi import FastAPI
|
|
42
|
+
from fastapi_armor import Armor
|
|
43
|
+
|
|
44
|
+
app = FastAPI()
|
|
45
|
+
Armor(app) # ✅ Done.
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install fastapi-armor
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Quick Start
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from fastapi import FastAPI
|
|
62
|
+
from fastapi_armor import Armor
|
|
63
|
+
|
|
64
|
+
app = FastAPI()
|
|
65
|
+
|
|
66
|
+
Armor(
|
|
67
|
+
app,
|
|
68
|
+
log_file="logs/app.log",
|
|
69
|
+
sentry_dsn="https://your-dsn@sentry.io/123", # optional
|
|
70
|
+
environment="production",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
@app.get("/")
|
|
74
|
+
def root():
|
|
75
|
+
return {"status": "running"}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
That's it. Your app now has:
|
|
79
|
+
- ✅ Rotating file logs + console output
|
|
80
|
+
- ✅ Clean JSON error responses for all exception types
|
|
81
|
+
- ✅ Sentry integration (if DSN is provided)
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## What It Does
|
|
86
|
+
|
|
87
|
+
### 🔴 Error Handling
|
|
88
|
+
|
|
89
|
+
| Exception Type | Response |
|
|
90
|
+
|---|---|
|
|
91
|
+
| `HTTPException` | `{"detail": "..."}` with original status code |
|
|
92
|
+
| `RequestValidationError` | `{"error": "Validation failed", "detail": [...]}` with 422 |
|
|
93
|
+
| Any other `Exception` | `{"error": "Internal server error"}` with 500 (full traceback logged internally) |
|
|
94
|
+
|
|
95
|
+
All responses use `Content-Type: application/json; charset=utf-8`.
|
|
96
|
+
|
|
97
|
+
### 📋 Logging
|
|
98
|
+
|
|
99
|
+
- **Rotating file logs** — auto-creates the `logs/` directory
|
|
100
|
+
- **Console output** — for local development
|
|
101
|
+
- **Format:** `2024-01-01 12:00:00 | ERROR | Traceback (most recent call last): ...`
|
|
102
|
+
- Named logger: `fastapi_armor`
|
|
103
|
+
|
|
104
|
+
### 🔍 Sentry (optional)
|
|
105
|
+
|
|
106
|
+
Initializes `sentry_sdk` with `FastApiIntegration` and `StarletteIntegration` when a DSN is provided. Private data is not sent by default.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Parameters
|
|
111
|
+
|
|
112
|
+
| Parameter | Type | Default | Description |
|
|
113
|
+
|---|---|---|---|
|
|
114
|
+
| `app` | `FastAPI` | required | Your FastAPI application instance |
|
|
115
|
+
| `log_file` | `str` | `"logs/app.log"` | Path to the rotating log file |
|
|
116
|
+
| `log_max_bytes` | `int` | `5_000_000` | Max size per log file (5 MB) |
|
|
117
|
+
| `log_backup_count` | `int` | `3` | Number of backup log files to keep |
|
|
118
|
+
| `sentry_dsn` | `str` | `""` | Sentry DSN — leave empty to disable |
|
|
119
|
+
| `environment` | `str` | `"development"` | Environment tag sent to Sentry |
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Requirements
|
|
124
|
+
|
|
125
|
+
- Python 3.9+
|
|
126
|
+
- FastAPI 0.100+
|
|
127
|
+
- sentry-sdk 1.40+ *(installed automatically)*
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Development
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
git clone https://github.com/raghadalb-tech/fastapi-armor.git
|
|
135
|
+
cd fastapi-armor
|
|
136
|
+
pip install -e ".[dev]"
|
|
137
|
+
pytest tests/ -v
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Test output:
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
fastapi_armor/__init__.py
|
|
4
|
+
fastapi_armor/armor.py
|
|
5
|
+
fastapi_armor/error_handler.py
|
|
6
|
+
fastapi_armor/logger.py
|
|
7
|
+
fastapi_armor/sentry.py
|
|
8
|
+
fastapi_armor_lib.egg-info/PKG-INFO
|
|
9
|
+
fastapi_armor_lib.egg-info/SOURCES.txt
|
|
10
|
+
fastapi_armor_lib.egg-info/dependency_links.txt
|
|
11
|
+
fastapi_armor_lib.egg-info/requires.txt
|
|
12
|
+
fastapi_armor_lib.egg-info/top_level.txt
|
|
13
|
+
tests/test_armor.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fastapi_armor
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "fastapi-armor-lib"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Production-ready error handling, logging, and monitoring for FastAPI apps"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"fastapi>=0.100.0",
|
|
13
|
+
"sentry-sdk[fastapi]>=1.40.0",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.optional-dependencies]
|
|
17
|
+
dev = ["pytest", "httpx", "uvicorn"]
|
|
18
|
+
|
|
19
|
+
[tool.setuptools.packages.find]
|
|
20
|
+
include = ["fastapi_armor*"]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
from fastapi import FastAPI, HTTPException
|
|
4
|
+
from httpx import ASGITransport, AsyncClient
|
|
5
|
+
|
|
6
|
+
from fastapi_armor import Armor
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _request(app: FastAPI, method: str, path: str):
|
|
10
|
+
async def _run():
|
|
11
|
+
transport = ASGITransport(app=app, raise_app_exceptions=False)
|
|
12
|
+
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
|
13
|
+
return await client.request(method, path)
|
|
14
|
+
|
|
15
|
+
return asyncio.run(_run())
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_armor_initializes_without_errors():
|
|
19
|
+
app = FastAPI()
|
|
20
|
+
armor = Armor(app)
|
|
21
|
+
assert armor.app is app
|
|
22
|
+
assert armor.logger.name == "fastapi_armor"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_http_exception_returns_clean_json():
|
|
26
|
+
app = FastAPI()
|
|
27
|
+
Armor(app)
|
|
28
|
+
|
|
29
|
+
@app.get("/http-error")
|
|
30
|
+
async def raise_http_error():
|
|
31
|
+
raise HTTPException(status_code=404, detail="Not found")
|
|
32
|
+
|
|
33
|
+
response = _request(app, "GET", "/http-error")
|
|
34
|
+
assert response.status_code == 404
|
|
35
|
+
assert response.json() == {"detail": "Not found"}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_unhandled_exception_returns_internal_server_error():
|
|
39
|
+
app = FastAPI()
|
|
40
|
+
Armor(app)
|
|
41
|
+
|
|
42
|
+
@app.get("/boom")
|
|
43
|
+
async def raise_runtime_error():
|
|
44
|
+
raise RuntimeError("unexpected")
|
|
45
|
+
|
|
46
|
+
response = _request(app, "GET", "/boom")
|
|
47
|
+
assert response.status_code == 500
|
|
48
|
+
assert response.json() == {"error": "Internal server error"}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_validation_error_returns_validation_failed():
|
|
52
|
+
app = FastAPI()
|
|
53
|
+
Armor(app)
|
|
54
|
+
|
|
55
|
+
@app.get("/items/{item_id}")
|
|
56
|
+
async def get_item(item_id: int):
|
|
57
|
+
return {"item_id": item_id}
|
|
58
|
+
|
|
59
|
+
response = _request(app, "GET", "/items/not-an-int")
|
|
60
|
+
assert response.status_code == 422
|
|
61
|
+
body = response.json()
|
|
62
|
+
assert body["error"] == "Validation failed"
|
|
63
|
+
assert isinstance(body["detail"], list)
|