Skip to content

Commit 5f6e7a9

Browse files
authored
Merge pull request #8 from APIForge-Organisation/dev
Exclude ghost routes from health score and metrics
2 parents 1926550 + f93854d commit 5f6e7a9

5 files changed

Lines changed: 30 additions & 19 deletions

File tree

apiforgepy/aggregator.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,16 @@ def stop(self):
2828
self._flush()
2929

3030
def record(self, event: dict):
31-
key = f"{event['method']}|{event['route']}|{event['env']}|{event.get('release') or ''}"
31+
is_ghost = event.get("is_ghost", False)
32+
key = f"{event['method']}|{event['route']}|{event['env']}|{event.get('release') or ''}|{'1' if is_ghost else '0'}"
3233
with self._lock:
3334
if key not in self._buffer:
3435
self._buffer[key] = {
3536
"method": event["method"],
3637
"route": event["route"],
3738
"env": event["env"],
3839
"release": event.get("release"),
40+
"is_ghost": is_ghost,
3941
"durations": [],
4042
"response_sizes": [],
4143
"status_2xx": 0,
@@ -85,6 +87,7 @@ def _flush(self):
8587
"method": bucket["method"],
8688
"env": bucket["env"],
8789
"release_tag": bucket["release"],
90+
"is_ghost": 1 if bucket["is_ghost"] else 0,
8891
"status_2xx": bucket["status_2xx"],
8992
"status_4xx": bucket["status_4xx"],
9093
"status_5xx": bucket["status_5xx"],

apiforgepy/database.py

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -43,19 +43,24 @@ def _init(self):
4343
lat_p99 REAL,
4444
lat_min REAL,
4545
lat_max REAL,
46-
bytes_avg REAL
46+
bytes_avg REAL,
47+
is_ghost INTEGER NOT NULL DEFAULT 0
4748
);
4849
4950
CREATE INDEX IF NOT EXISTS idx_route_ts ON api_metrics (route, method, bucket_ts);
5051
CREATE INDEX IF NOT EXISTS idx_bucket_ts ON api_metrics (bucket_ts);
5152
CREATE INDEX IF NOT EXISTS idx_release ON api_metrics (release_tag)
5253
WHERE release_tag IS NOT NULL;
5354
""")
54-
# Migration for databases created before bytes_avg was introduced
55+
# Migrations for databases created before these columns were introduced
5556
try:
5657
c.execute("ALTER TABLE api_metrics ADD COLUMN bytes_avg REAL")
5758
except Exception:
58-
pass # column already exists
59+
pass
60+
try:
61+
c.execute("ALTER TABLE api_metrics ADD COLUMN is_ghost INTEGER NOT NULL DEFAULT 0")
62+
except Exception:
63+
pass
5964
c.commit()
6065

6166
def insert_batch(self, rows: list[dict]):
@@ -66,11 +71,11 @@ def insert_batch(self, rows: list[dict]):
6671
INSERT INTO api_metrics
6772
(bucket_ts, route, method, env, release_tag,
6873
status_2xx, status_4xx, status_5xx, total_calls,
69-
lat_p50, lat_p90, lat_p99, lat_min, lat_max, bytes_avg)
74+
lat_p50, lat_p90, lat_p99, lat_min, lat_max, bytes_avg, is_ghost)
7075
VALUES (
7176
:bucket_ts, :route, :method, :env, :release_tag,
7277
:status_2xx, :status_4xx, :status_5xx, :total_calls,
73-
:lat_p50, :lat_p90, :lat_p99, :lat_min, :lat_max, :bytes_avg
78+
:lat_p50, :lat_p90, :lat_p99, :lat_min, :lat_max, :bytes_avg, :is_ghost
7479
)
7580
""", rows)
7681
self._conn.commit()
@@ -107,22 +112,22 @@ def get_summary(self) -> dict:
107112
SUM(status_5xx) as calls_5xx,
108113
AVG(lat_p90) as avg_p90,
109114
AVG(lat_p99) as avg_p99
110-
FROM api_metrics WHERE bucket_ts >= ?
115+
FROM api_metrics WHERE bucket_ts >= ? AND is_ghost = 0
111116
""", (since_24h,)).fetchone()
112117

113118
baseline = c.execute("""
114119
SELECT AVG(lat_p90) as baseline_p90
115-
FROM api_metrics WHERE bucket_ts >= ? AND bucket_ts < ?
120+
FROM api_metrics WHERE bucket_ts >= ? AND bucket_ts < ? AND is_ghost = 0
116121
""", (since_7d, since_24h)).fetchone()
117122

118123
active = c.execute("""
119124
SELECT COUNT(DISTINCT route || '|' || method) as n
120-
FROM api_metrics WHERE bucket_ts >= ?
125+
FROM api_metrics WHERE bucket_ts >= ? AND is_ghost = 0
121126
""", (since_24h,)).fetchone()
122127

123128
total = c.execute("""
124129
SELECT COUNT(DISTINCT route || '|' || method) as n
125-
FROM api_metrics
130+
FROM api_metrics WHERE is_ghost = 0
126131
""").fetchone()
127132

128133
return {
@@ -136,7 +141,7 @@ def get_routes(self, hours: int = 24) -> list[dict]:
136141
since = _now_sec() - hours * 3600
137142
rows = self._conn.execute("""
138143
SELECT
139-
route, method,
144+
route, method, is_ghost,
140145
SUM(total_calls) as calls,
141146
SUM(status_2xx) as calls_2xx,
142147
SUM(status_4xx) as calls_4xx,
@@ -148,9 +153,9 @@ def get_routes(self, hours: int = 24) -> list[dict]:
148153
AVG(bytes_avg) as bytes_avg
149154
FROM api_metrics
150155
WHERE bucket_ts >= ?
151-
GROUP BY route, method
152-
ORDER BY calls DESC
153-
LIMIT 50
156+
GROUP BY route, method, is_ghost
157+
ORDER BY is_ghost ASC, calls DESC
158+
LIMIT 100
154159
""", (since,)).fetchall()
155160
return [dict(r) for r in rows]
156161

@@ -171,6 +176,7 @@ def get_dead_candidates(self, inactive_days: int = 21) -> list[dict]:
171176
rows = self._conn.execute("""
172177
SELECT route, method, MAX(bucket_ts) as last_seen
173178
FROM api_metrics
179+
WHERE is_ghost = 0
174180
GROUP BY route, method
175181
HAVING last_seen < ?
176182
ORDER BY last_seen ASC
@@ -217,14 +223,14 @@ def get_latency_anomaly_data(self) -> dict:
217223

218224
recent = self._conn.execute("""
219225
SELECT route, method, AVG(lat_p99) as avg_p99
220-
FROM api_metrics WHERE bucket_ts >= ?
226+
FROM api_metrics WHERE bucket_ts >= ? AND is_ghost = 0
221227
GROUP BY route, method
222228
""", (since_1h,)).fetchall()
223229

224230
baseline = self._conn.execute("""
225231
SELECT route, method, lat_p99
226232
FROM api_metrics
227-
WHERE bucket_ts >= ? AND bucket_ts < ? AND lat_p99 IS NOT NULL
233+
WHERE bucket_ts >= ? AND bucket_ts < ? AND lat_p99 IS NOT NULL AND is_ghost = 0
228234
""", (since_7d, since_1h)).fetchall()
229235

230236
return {
@@ -270,7 +276,7 @@ def get_drift_data(self) -> list[dict]:
270276
CAST(bucket_ts / 86400 AS INTEGER) as day_bucket,
271277
AVG(lat_p90) as p90
272278
FROM api_metrics
273-
WHERE bucket_ts >= ? AND lat_p90 IS NOT NULL
279+
WHERE bucket_ts >= ? AND lat_p90 IS NOT NULL AND is_ghost = 0
274280
GROUP BY route, method, day_bucket
275281
ORDER BY route, method, day_bucket
276282
""", (since_30d,)).fetchall()
@@ -282,7 +288,7 @@ def get_global_time_series(self, hours: int = 24) -> list[dict]:
282288
SELECT bucket_ts, SUM(total_calls) as calls,
283289
AVG(lat_p50) as p50, AVG(lat_p90) as p90,
284290
AVG(lat_p99) as p99, SUM(status_5xx) as errors
285-
FROM api_metrics WHERE bucket_ts >= ?
291+
FROM api_metrics WHERE bucket_ts >= ? AND is_ghost = 0
286292
GROUP BY bucket_ts ORDER BY bucket_ts ASC
287293
""", (since,)).fetchall()
288294
return [dict(r) for r in rows]

apiforgepy/middleware.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ async def dispatch(self, request: Request, call_next):
9292
"release": self._release,
9393
"service": self._service,
9494
"response_size": int(content_length) if content_length else None,
95+
"is_ghost": route_obj is None,
9596
})
9697
except Exception:
9798
pass # never crash the host application

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.1.1"
7+
version = "2.1.2"
88

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

tests/test_database.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ def insert_row(db, **overrides):
2424
lat_min=10.0,
2525
lat_max=150.0,
2626
bytes_avg=None,
27+
is_ghost=0,
2728
)
2829
db.insert_batch([{**defaults, **overrides}])
2930

0 commit comments

Comments
 (0)