-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcloud_transport.py
More file actions
101 lines (92 loc) · 3.75 KB
/
Copy pathcloud_transport.py
File metadata and controls
101 lines (92 loc) · 3.75 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
import json
import time
import threading
import urllib.request
import urllib.error
from datetime import datetime, timezone
_CIRCUIT_OPEN_S = 60
_FAILURE_THRESHOLD = 5
class CloudTransport:
"""Sends aggregated metrics to the APIForge SaaS ingest endpoint."""
def __init__(self, cloud_url: str, api_key: str, service: str):
self._url = cloud_url.rstrip("/") + "/ingest"
self._api_key = api_key
self._service = service
self._failures = 0
self._open_until = 0.0
self._lock = threading.Lock()
def write_routes(self, routes: list[dict]) -> None:
if not routes:
return
payload = json.dumps({
"routes": [
{"route": r["route"], "method": r["method"], "service": self._service}
for r in routes
]
}).encode()
req = urllib.request.Request(
self._url + "/routes",
data=payload,
headers={"Content-Type": "application/json", "X-API-Key": self._api_key},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=10):
pass
except (urllib.error.URLError, OSError) as exc:
print(f"[apiforgepy] Failed to sync route registry: {exc}")
def write(self, rows: list[dict]) -> None:
if not rows:
return
if time.monotonic() < self._open_until:
return
metrics = [
{
"route": r["route"],
"method": r["method"],
"service": self._service,
"env": r["env"],
"release": r.get("release_tag"),
"time": datetime.fromtimestamp(r["bucket_ts"], tz=timezone.utc).strftime('%Y-%m-%dT%H:%M:%S.000Z'),
"calls_total": r["total_calls"],
"calls_2xx": r["status_2xx"],
"calls_3xx": r.get("status_3xx", 0),
"calls_4xx": r["status_4xx"],
"calls_5xx": r["status_5xx"],
"status_dist": r.get("status_dist"),
"lat_p50": r.get("lat_p50"),
"lat_p90": r.get("lat_p90"),
"lat_p99": r.get("lat_p99"),
"lat_avg": r.get("lat_avg"),
"lat_ttfb_p50": r.get("lat_ttfb_p50"),
"lat_ttfb_p90": r.get("lat_ttfb_p90"),
"lat_ttfb_p99": r.get("lat_ttfb_p99"),
"bytes_avg": r.get("bytes_avg"),
"request_size_avg": r.get("request_size_avg"),
"inflight_avg": r.get("inflight_avg"),
"inflight_max": r.get("inflight_max"),
"is_ghost": bool(r.get("is_ghost", 0)),
}
for r in rows
]
payload = json.dumps({"metrics": metrics}).encode()
req = urllib.request.Request(
self._url,
data=payload,
headers={"Content-Type": "application/json", "X-API-Key": self._api_key},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=10):
with self._lock:
self._failures = 0
except (urllib.error.URLError, OSError) as exc:
with self._lock:
self._failures += 1
if self._failures >= _FAILURE_THRESHOLD:
self._open_until = time.monotonic() + _CIRCUIT_OPEN_S
self._failures = 0
print(
f"[apiforgepy] Cloud flush failures — pausing for {_CIRCUIT_OPEN_S}s. "
f"Error: {exc}"
)