-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path__init__.py
More file actions
130 lines (110 loc) · 4.45 KB
/
Copy path__init__.py
File metadata and controls
130 lines (110 loc) · 4.45 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
"""
apiforgepy — API observability & intelligence for FastAPI/Starlette.
Local-first, privacy-first. Dashboard on port 4242.
Usage (local):
from apiforgepy import ApiForgeMiddleware
app.add_middleware(ApiForgeMiddleware)
Usage (cloud):
app.add_middleware(
ApiForgeMiddleware,
cloud_url="https://api.apiforge.fr",
api_key="af_...",
)
"""
import atexit
from .aggregator import Aggregator
from .cloud_transport import CloudTransport
from .dashboard import start_dashboard
from .database import ApiForgeDatabase
from .middleware import ApiForgeMiddleware as _Base
from .transport import LocalTransport
__version__ = "4.0.0"
__all__ = ["ApiForgeMiddleware"]
# Hard floor for the send cadence. The `_flush_interval` kwarg stays internal (used by
# the test suite with larger values); the floor guarantees no caller can shorten the
# delay below 60s. This only guards against misconfiguration — real ingest throttling
# is enforced server-side (per-key rate limit + monthly quota).
_MIN_FLUSH_INTERVAL_MS = 60_000
class ApiForgeMiddleware(_Base):
"""
Starlette/FastAPI middleware for APIForge observability.
Parameters
----------
app: The ASGI app to wrap.
cloud_url: Cloud mode: SaaS API base URL (e.g. 'https://api.apiforge.fr').
api_key: Cloud mode: project API key starting with 'af_'.
db_path: Local mode: SQLite file path. Default: '.apiforge.db'.
dashboard_port: Local mode: dashboard port. 0 = disabled. Default: 4242.
env: Environment label. Default: 'production'.
release: Release tag. Default: None.
service: Service name. Default: 'default'.
sampling: Sample rate 0.0–1.0. Default: 1.0.
ignore_paths: Paths to exclude. Default: ['/favicon.ico'].
"""
def __init__(
self,
app,
*,
cloud_url: str | None = None,
api_key: str | None = None,
db_path: str = ".apiforge.db",
dashboard_port: int = 4242,
env: str | None = None,
release: str | None = None,
service: str = "default",
sampling: float = 1.0,
ignore_paths: list[str] = None,
_flush_interval: int = 60_000, # internal — not part of the public API
):
is_cloud = bool(cloud_url and api_key)
if (cloud_url and not api_key) or (api_key and not cloud_url):
raise ValueError("[apiforgepy] Cloud mode requires both cloud_url and api_key.")
config = {
"mode": "cloud" if is_cloud else "local",
"env": env or "production",
"release": release,
"service": service,
"sampling": sampling,
"ignore_paths": ignore_paths or ["/favicon.ico"],
}
self._db = None
self._dashboard_server = None
self._stopped = False
if is_cloud:
transport = CloudTransport(cloud_url, api_key, service)
config["store_routes"] = transport.write_routes
else:
self._db = ApiForgeDatabase(db_path)
transport = LocalTransport(self._db)
config["store_routes"] = self._db.upsert_known_routes
aggregator = Aggregator(transport, max(_flush_interval, _MIN_FLUSH_INTERVAL_MS))
aggregator.start()
if not is_cloud and dashboard_port:
self._dashboard_server = start_dashboard(self._db, dashboard_port)
self._aggregator_ref = aggregator
super().__init__(app, aggregator=aggregator, config=config)
atexit.register(self._cleanup)
def _cleanup(self) -> None:
"""Flush buffer and close DB — safe to call multiple times (via atexit or shutdown)."""
if self._stopped:
return
self._stopped = True
try:
self._aggregator_ref.stop()
except Exception:
pass
if self._db:
try:
self._db.close()
except Exception:
pass
def shutdown(self) -> None:
"""Flush remaining buffer, stop dashboard, and release all resources."""
self._cleanup()
if self._dashboard_server:
try:
self._dashboard_server.shutdown()
self._dashboard_server.server_close()
except Exception:
pass
self._dashboard_server = None