Skip to content

Commit edaef5f

Browse files
authored
Merge pull request #11 from APIForge-Organisation/dev
Dev
2 parents 663b3d9 + 2406e6a commit edaef5f

6 files changed

Lines changed: 106 additions & 36 deletions

File tree

CHANGELOG.md

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,39 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) — versioning
88

99
## [Unreleased]
1010

11+
---
12+
13+
## [3.0.0] — 2026-06-04
14+
15+
### Breaking Changes
16+
17+
- `flush_interval` parameter **removed** from `ApiForgeMiddleware` — passing it now raises `TypeError`. The flush window is fixed at **60 seconds**.
18+
- `env` no longer falls back to `os.environ.get("ENV")` — must be passed explicitly. Default is now `'production'`.
19+
- `release` no longer falls back to `os.environ.get("APP_VERSION")` — must be passed explicitly. Default is now `None`.
20+
1121
### Added
1222

13-
- `bytes_avg` field: average response body size (bytes) per route per bucket, sourced from the `Content-Length` response header — stored in SQLite and exposed via `/api/routes`
14-
- 4 unit tests covering `bytes_avg` aggregation and storage
23+
- `bytes_avg` field: average response body size (bytes) per route per bucket, sourced from the `Content-Length` response header
24+
- `inflight_avg` and `inflight_max` per route — inflight concurrency count captured via ASGI scope and aggregated per minute bucket
25+
26+
### Migration guide
27+
28+
```python
29+
# Before (v2.x)
30+
app.add_middleware(
31+
ApiForgeMiddleware,
32+
flush_interval=30_000, # ← remove (TypeError in v3)
33+
env=os.environ.get("ENV", "production"), # ← pass explicitly
34+
release=os.environ.get("APP_VERSION"), # ← still OK (your app reads the env var)
35+
)
36+
37+
# After (v3.0)
38+
app.add_middleware(
39+
ApiForgeMiddleware,
40+
env="production", # set explicitly
41+
release="v1.4.0", # set explicitly
42+
)
43+
```
1544

1645
---
1746

README.md

Lines changed: 65 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -27,60 +27,103 @@ from apiforgepy import ApiForgeMiddleware
2727

2828
app = FastAPI()
2929

30-
app.add_middleware(
31-
ApiForgeMiddleware,
32-
mode="local",
33-
)
30+
app.add_middleware(ApiForgeMiddleware)
3431

3532
@app.get("/users/{user_id}")
3633
def get_user(user_id: int):
3734
return {"id": user_id}
3835

39-
# Dashboard http://localhost:4242
36+
# Dashboard auto-starts at http://localhost:4242
4037
```
4138

39+
## Dashboard
40+
41+
Open **http://localhost:4242** after starting your app. No configuration needed — the dashboard server starts automatically in the background.
42+
43+
- **Health Score** (0–100) — global API health at a glance
44+
- **Latency percentiles** — P50 / P90 / P99 per route
45+
- **Error rates** — 4xx and 5xx breakdown
46+
- **Automatic insights** — latency anomalies, dead endpoints, release regressions
47+
- **Time series chart** — click any route to see its latency over time
48+
49+
Data is stored locally in `.apiforge.db` (SQLite). Nothing leaves your machine.
50+
4251
## Configuration
4352

4453
```python
4554
app.add_middleware(
4655
ApiForgeMiddleware,
47-
mode="local",
4856
db_path=".apiforge.db",
49-
dashboard_port=4242, # set to 0 to disable
50-
flush_interval=60_000, # ms
57+
dashboard_port=4242, # set to 0 to disable
5158
env="production",
52-
release="v1.4.0", # enables release regression detection
59+
release="v1.4.0", # enables release regression detection
5360
service="user-service",
54-
sampling=1.0, # 0.0–1.0
61+
sampling=1.0, # 0.0–1.0 sample rate
5562
ignore_paths=["/health", "/favicon.ico"],
5663
)
5764
```
5865

66+
## Cloud mode
67+
68+
Send metrics to the APIForge SaaS platform instead of storing them locally:
69+
70+
```python
71+
app.add_middleware(
72+
ApiForgeMiddleware,
73+
cloud_url=os.environ["APIFORGE_CLOUD_URL"],
74+
api_key=os.environ["APIFORGE_API_KEY"],
75+
service="user-service",
76+
env="production",
77+
release=os.environ.get("APP_VERSION"),
78+
)
79+
```
80+
81+
In cloud mode, metrics are aggregated in memory for 60 seconds and sent as a single batch — the local dashboard and SQLite database are not used.
82+
83+
## Release tracking
84+
85+
Pass your release version to enable before/after deployment comparison:
86+
87+
```python
88+
import os
89+
app.add_middleware(ApiForgeMiddleware, release=os.environ.get("APP_VERSION"))
90+
```
91+
92+
When a new release is detected, APIForge compares P90 latency before and after and surfaces regressions automatically.
93+
5994
## What you get
6095

61-
- **Latency percentiles** — P50 / P90 / P99 per endpoint, updated every 60s
62-
- **Error rate by route** — 2xx / 4xx / 5xx breakdown in real time
63-
- **API Health Score** — a single 0–100 score summarizing your API's health
64-
- **Automatic insights** — plain-language alerts with no configuration
65-
- **Dead endpoint detection** — routes with no traffic in 21+ days
66-
- **Release impact tracking** — before/after comparison on every deploy
96+
- **Per-route latency** — P50, P90, P99 per endpoint, updated every 60 s
97+
- **Error rate by route** — 2xx / 3xx / 4xx / 5xx breakdown
98+
- **API Health Score** — a single 0–100 score summarising your API's health
99+
- **Ghost route detection** — requests that match no declared Starlette/FastAPI route
100+
- **Latency anomaly alerts** — Z-score detection against a 7-day baseline
101+
- **Dead endpoint detection** — routes with no traffic for 21+ days
102+
- **Release regression analysis** — automatic P90 comparison per deploy
103+
- **Progressive drift detection** — slow latency increases over weeks
104+
- **Untracked route detection** — declared routes that never received traffic
105+
- **Inflight concurrency tracking**`inflight_avg` and `inflight_max` per route
67106

68107
## Graceful shutdown
69108

109+
Cleanup (flush buffer, close dashboard, close SQLite) happens automatically via `atexit`. For explicit control in long-running processes:
110+
70111
```python
71-
import signal
112+
from contextlib import asynccontextmanager
113+
from fastapi import FastAPI
114+
from apiforgepy import ApiForgeMiddleware
72115

73-
mw = None
116+
forge = None
74117

75118
@asynccontextmanager
76119
async def lifespan(app):
77120
yield
78-
if mw:
79-
mw.shutdown()
121+
if forge:
122+
forge.shutdown()
80123

81124
app = FastAPI(lifespan=lifespan)
82-
mw = ApiForgeMiddleware.__new__(ApiForgeMiddleware)
83-
app.add_middleware(ApiForgeMiddleware, mode="local")
125+
forge = ApiForgeMiddleware(app) # store reference before adding
126+
app.add_middleware(ApiForgeMiddleware)
84127
```
85128

86129
## Privacy by design

apiforgepy/__init__.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
"""
1616

1717
import atexit
18-
import os
1918

2019
from .aggregator import Aggregator
2120
from .database import ApiForgeDatabase
@@ -24,7 +23,7 @@
2423
from .transport import LocalTransport
2524
from .cloud_transport import CloudTransport
2625

27-
__version__ = "2.2.1"
26+
__version__ = "3.0.0"
2827
__all__ = ["ApiForgeMiddleware"]
2928

3029

@@ -39,9 +38,8 @@ class ApiForgeMiddleware(_Base):
3938
api_key: Cloud mode: project API key starting with 'af_'.
4039
db_path: Local mode: SQLite file path. Default: '.apiforge.db'.
4140
dashboard_port: Local mode: dashboard port. 0 = disabled. Default: 4242.
42-
flush_interval: Aggregation flush interval in ms. Default: 60 000.
43-
env: Environment label. Default: ENV env var or 'production'.
44-
release: Release tag. Default: APP_VERSION env var.
41+
env: Environment label. Default: 'production'.
42+
release: Release tag. Default: None.
4543
service: Service name. Default: 'default'.
4644
sampling: Sample rate 0.0–1.0. Default: 1.0.
4745
ignore_paths: Paths to exclude. Default: ['/favicon.ico'].
@@ -55,12 +53,12 @@ def __init__(
5553
api_key: str | None = None,
5654
db_path: str = ".apiforge.db",
5755
dashboard_port: int = 4242,
58-
flush_interval: int = 60_000,
5956
env: str | None = None,
6057
release: str | None = None,
6158
service: str = "default",
6259
sampling: float = 1.0,
6360
ignore_paths: list[str] = None,
61+
_flush_interval: int = 60_000, # internal — not part of the public API
6462
):
6563
is_cloud = bool(cloud_url and api_key)
6664

@@ -69,8 +67,8 @@ def __init__(
6967

7068
config = {
7169
"mode": "cloud" if is_cloud else "local",
72-
"env": env or os.environ.get("ENV", "production"),
73-
"release": release or os.environ.get("APP_VERSION"),
70+
"env": env or "production",
71+
"release": release,
7472
"service": service,
7573
"sampling": sampling,
7674
"ignore_paths": ignore_paths or ["/favicon.ico"],
@@ -88,7 +86,7 @@ def __init__(
8886
transport = LocalTransport(self._db)
8987
config["store_routes"] = self._db.upsert_known_routes
9088

91-
aggregator = Aggregator(transport, flush_interval)
89+
aggregator = Aggregator(transport, _flush_interval)
9290
aggregator.start()
9391

9492
if not is_cloud and dashboard_port:

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "apiforgepy"
7-
version = "2.2.1"
7+
version = "3.0.0"
88

99
description = "API observability & intelligence for FastAPI/Starlette — local-first, privacy-first"
1010
readme = "README.md"

tests/test_middleware.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ def make_app(db_path=":memory:", sampling=1.0, ignore_paths=None):
1111
ApiForgeMiddleware,
1212
db_path=db_path,
1313
dashboard_port=0,
14-
flush_interval=999_999,
14+
_flush_interval=999_999,
1515
sampling=sampling,
1616
ignore_paths=ignore_paths or [],
1717
)

tests/test_smoke.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ def make_app(db_path=":memory:"):
1111
ApiForgeMiddleware,
1212
db_path=db_path,
1313
dashboard_port=0,
14-
flush_interval=999_999,
14+
_flush_interval=999_999,
1515
)
1616

1717
@app.get("/health")

0 commit comments

Comments
 (0)