fastapi-alertengine 1.0.0__tar.gz → 1.0.2__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_alertengine-1.0.0 → fastapi_alertengine-1.0.2}/PKG-INFO +1 -1
- fastapi_alertengine-1.0.2/fastapi_alertengine/__init__.py +7 -0
- fastapi_alertengine-1.0.2/fastapi_alertengine/app_main.py +150 -0
- fastapi_alertengine-1.0.2/fastapi_alertengine/client.py +17 -0
- fastapi_alertengine-1.0.2/fastapi_alertengine/config.py +0 -0
- fastapi_alertengine-1.0.2/fastapi_alertengine/engine.py.py +0 -0
- fastapi_alertengine-1.0.2/fastapi_alertengine/middleware.py +0 -0
- fastapi_alertengine-1.0.2/fastapi_alertengine/storage.py.py +0 -0
- {fastapi_alertengine-1.0.0 → fastapi_alertengine-1.0.2}/fastapi_alertengine.egg-info/PKG-INFO +1 -1
- fastapi_alertengine-1.0.2/fastapi_alertengine.egg-info/SOURCES.txt +22 -0
- fastapi_alertengine-1.0.2/fastapi_alertengine.egg-info/top_level.txt +1 -0
- {fastapi_alertengine-1.0.0 → fastapi_alertengine-1.0.2}/pyproject.toml +1 -1
- fastapi_alertengine-1.0.2/setup.cfg +16 -0
- fastapi_alertengine-1.0.0/fastapi_alertengine.egg-info/SOURCES.txt +0 -7
- fastapi_alertengine-1.0.0/fastapi_alertengine.egg-info/top_level.txt +0 -1
- fastapi_alertengine-1.0.0/setup.cfg +0 -4
- {fastapi_alertengine-1.0.0 → fastapi_alertengine-1.0.2}/README.md +0 -0
- {fastapi_alertengine-1.0.0 → fastapi_alertengine-1.0.2}/fastapi_alertengine.egg-info/dependency_links.txt +0 -0
- {fastapi_alertengine-1.0.0 → fastapi_alertengine-1.0.2}/fastapi_alertengine.egg-info/requires.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-alertengine
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.2
|
|
4
4
|
Summary: FastAPI AlertEngine: fintech-grade request monitoring (p95 latency + error rate) without Prometheus.
|
|
5
5
|
Author: Tandem Media
|
|
6
6
|
Project-URL: Homepage, https://github.com/Tandem-Media/fastapi-alertengine
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# fastapi_alertengine/__init__.py
|
|
2
|
+
|
|
3
|
+
from .engine import AlertEngine # or from .alert_engine import AlertEngine
|
|
4
|
+
from .middleware import RequestMetricsMiddleware
|
|
5
|
+
from .client import get_alert_engine
|
|
6
|
+
|
|
7
|
+
__all__ = ["AlertEngine", "RequestMetricsMiddleware", "get_alert_engine"]
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from typing import Callable
|
|
3
|
+
|
|
4
|
+
import redis
|
|
5
|
+
from fastapi import FastAPI, Request
|
|
6
|
+
from fastapi.responses import JSONResponse
|
|
7
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
8
|
+
|
|
9
|
+
app = FastAPI()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RequestMetricsMiddleware(BaseHTTPMiddleware):
|
|
13
|
+
def __init__(self, app, redis_url: str = "redis://localhost:6379/0"):
|
|
14
|
+
super().__init__(app)
|
|
15
|
+
self.redis = redis.Redis.from_url(redis_url, decode_responses=True)
|
|
16
|
+
|
|
17
|
+
async def dispatch(self, request: Request, call_next: Callable):
|
|
18
|
+
start = time.time()
|
|
19
|
+
try:
|
|
20
|
+
response = await call_next(request)
|
|
21
|
+
status_code = response.status_code
|
|
22
|
+
except Exception:
|
|
23
|
+
status_code = 500
|
|
24
|
+
response = JSONResponse({"detail": "Internal server error"}, status_code=500)
|
|
25
|
+
|
|
26
|
+
duration_ms = (time.time() - start) * 1000
|
|
27
|
+
# Write a minimal metric to Redis Stream
|
|
28
|
+
self.redis.xadd(
|
|
29
|
+
"request_metrics",
|
|
30
|
+
{
|
|
31
|
+
"path": request.url.path,
|
|
32
|
+
"status": status_code,
|
|
33
|
+
"duration_ms": f"{duration_ms:.2f}",
|
|
34
|
+
},
|
|
35
|
+
maxlen=1000,
|
|
36
|
+
)
|
|
37
|
+
return response
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class AlertEngine:
|
|
41
|
+
"""
|
|
42
|
+
AlertEngine Lite:
|
|
43
|
+
- Reads from Redis Stream `request_metrics`
|
|
44
|
+
- Tracks rolling error rate and P95 latency
|
|
45
|
+
- For now: prints alerts to console (no email/slack yet)
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
redis_url: str = "redis://localhost:6379/0",
|
|
51
|
+
stream_key: str = "request_metrics",
|
|
52
|
+
group: str = "alert_engine",
|
|
53
|
+
consumer: str = "alert_engine_1",
|
|
54
|
+
error_rate_threshold: float = 0.05,
|
|
55
|
+
p95_latency_threshold_ms: float = 500.0,
|
|
56
|
+
window_size: int = 100,
|
|
57
|
+
):
|
|
58
|
+
self.redis = redis.Redis.from_url(redis_url, decode_responses=True)
|
|
59
|
+
self.stream_key = stream_key
|
|
60
|
+
self.group = group
|
|
61
|
+
self.consumer = consumer
|
|
62
|
+
self.error_rate_threshold = error_rate_threshold
|
|
63
|
+
self.p95_latency_threshold_ms = p95_latency_threshold_ms
|
|
64
|
+
self.window_size = window_size
|
|
65
|
+
|
|
66
|
+
# Create consumer group if it doesn't exist
|
|
67
|
+
try:
|
|
68
|
+
self.redis.xgroup_create(self.stream_key, self.group, id="$", mkstream=True)
|
|
69
|
+
except redis.ResponseError as e:
|
|
70
|
+
# Group already exists
|
|
71
|
+
if "BUSYGROUP" not in str(e):
|
|
72
|
+
raise
|
|
73
|
+
|
|
74
|
+
def _calculate_alerts(self, events: list[dict]) -> None:
|
|
75
|
+
if not events:
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
durations = []
|
|
79
|
+
errors = 0
|
|
80
|
+
|
|
81
|
+
for e in events:
|
|
82
|
+
fields = e["fields"]
|
|
83
|
+
status = int(fields.get("status", 500))
|
|
84
|
+
duration_ms = float(fields.get("duration_ms", 0.0))
|
|
85
|
+
durations.append(duration_ms)
|
|
86
|
+
if status >= 500:
|
|
87
|
+
errors += 1
|
|
88
|
+
|
|
89
|
+
error_rate = errors / len(events)
|
|
90
|
+
p95_latency = self._p95(durations)
|
|
91
|
+
|
|
92
|
+
if error_rate >= self.error_rate_threshold:
|
|
93
|
+
print(
|
|
94
|
+
f"[ALERT] High error rate: {error_rate:.2%} "
|
|
95
|
+
f"(threshold {self.error_rate_threshold:.2%})"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
if p95_latency >= self.p95_latency_threshold_ms:
|
|
99
|
+
print(
|
|
100
|
+
f"[ALERT] High P95 latency: {p95_latency:.1f} ms "
|
|
101
|
+
f"(threshold {self.p95_latency_threshold_ms:.1f} ms)"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
@staticmethod
|
|
105
|
+
def _p95(values: list[float]) -> float:
|
|
106
|
+
if not values:
|
|
107
|
+
return 0.0
|
|
108
|
+
values_sorted = sorted(values)
|
|
109
|
+
idx = int(0.95 * (len(values_sorted) - 1))
|
|
110
|
+
return values_sorted[idx]
|
|
111
|
+
|
|
112
|
+
def poll_once(self, count: int = 200, block_ms: int = 1000) -> None:
|
|
113
|
+
"""
|
|
114
|
+
Polls Redis Streams once and prints alerts if thresholds are crossed.
|
|
115
|
+
Can be called in a loop or a background task.
|
|
116
|
+
"""
|
|
117
|
+
resp = self.redis.xreadgroup(
|
|
118
|
+
groupname=self.group,
|
|
119
|
+
consumername=self.consumer,
|
|
120
|
+
streams={self.stream_key: ">"},
|
|
121
|
+
count=count,
|
|
122
|
+
block=block_ms,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
if not resp:
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
# resp is a list of (stream, [ (id, fields), ... ])
|
|
129
|
+
events = []
|
|
130
|
+
ids_to_ack = []
|
|
131
|
+
for stream_name, entries in resp:
|
|
132
|
+
for entry_id, fields in entries:
|
|
133
|
+
events.append({"id": entry_id, "fields": fields})
|
|
134
|
+
ids_to_ack.append(entry_id)
|
|
135
|
+
|
|
136
|
+
# Use only the latest window_size events for alert calculations
|
|
137
|
+
events = events[-self.window_size :]
|
|
138
|
+
self._calculate_alerts(events)
|
|
139
|
+
|
|
140
|
+
if ids_to_ack:
|
|
141
|
+
self.redis.xack(self.stream_key, self.group, *ids_to_ack)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# Wire up middleware
|
|
145
|
+
app.add_middleware(RequestMetricsMiddleware)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@app.get("/health")
|
|
149
|
+
async def health():
|
|
150
|
+
return {"status": "ok"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# fastapi_alertengine/client.py
|
|
2
|
+
|
|
3
|
+
from functools import lru_cache
|
|
4
|
+
|
|
5
|
+
from .engine import AlertEngine # or from .alert_engine import AlertEngine if you didn't rename
|
|
6
|
+
from .config import AlertConfig
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@lru_cache(maxsize=1)
|
|
10
|
+
def get_alert_engine(config: AlertConfig | None = None) -> AlertEngine:
|
|
11
|
+
"""
|
|
12
|
+
Return a singleton AlertEngine instance.
|
|
13
|
+
|
|
14
|
+
If a config is provided on first call, it is used to initialize the engine.
|
|
15
|
+
Subsequent calls ignore the config argument and return the same instance.
|
|
16
|
+
"""
|
|
17
|
+
return AlertEngine(config=config)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_alertengine-1.0.0 → fastapi_alertengine-1.0.2}/fastapi_alertengine.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-alertengine
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.2
|
|
4
4
|
Summary: FastAPI AlertEngine: fintech-grade request monitoring (p95 latency + error rate) without Prometheus.
|
|
5
5
|
Author: Tandem Media
|
|
6
6
|
Project-URL: Homepage, https://github.com/Tandem-Media/fastapi-alertengine
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
setup.cfg
|
|
4
|
+
./fastapi_alertengine/__init__.py
|
|
5
|
+
./fastapi_alertengine/app_main.py
|
|
6
|
+
./fastapi_alertengine/client.py
|
|
7
|
+
./fastapi_alertengine/config.py
|
|
8
|
+
./fastapi_alertengine/engine.py.py
|
|
9
|
+
./fastapi_alertengine/middleware.py
|
|
10
|
+
./fastapi_alertengine/storage.py.py
|
|
11
|
+
fastapi_alertengine/__init__.py
|
|
12
|
+
fastapi_alertengine/app_main.py
|
|
13
|
+
fastapi_alertengine/client.py
|
|
14
|
+
fastapi_alertengine/config.py
|
|
15
|
+
fastapi_alertengine/engine.py.py
|
|
16
|
+
fastapi_alertengine/middleware.py
|
|
17
|
+
fastapi_alertengine/storage.py.py
|
|
18
|
+
fastapi_alertengine.egg-info/PKG-INFO
|
|
19
|
+
fastapi_alertengine.egg-info/SOURCES.txt
|
|
20
|
+
fastapi_alertengine.egg-info/dependency_links.txt
|
|
21
|
+
fastapi_alertengine.egg-info/requires.txt
|
|
22
|
+
fastapi_alertengine.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fastapi_alertengine
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "fastapi-alertengine"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.2"
|
|
8
8
|
description = "FastAPI AlertEngine: fintech-grade request monitoring (p95 latency + error rate) without Prometheus."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
File without changes
|
|
File without changes
|
{fastapi_alertengine-1.0.0 → fastapi_alertengine-1.0.2}/fastapi_alertengine.egg-info/requires.txt
RENAMED
|
File without changes
|