Skip to content

Commit 2e60db2

Browse files
committed
api: provide /metrics for prometheus export
Instead of inventing a new stats API, use a Prometheus compatible backend. This commits removes all /api/v1/stats calls and instead provides everything interesting over at /metrics. A single metric `builds` is provided with a bunch of tags. Those tags can be used to create graphs describing which branch/version/profile etc was build. While at it, switch testing to use a sync build queue, meaning jobs will actually run and don't just end up in a "202 ACCEPTED" state. No longer validate outgoing messages except during testing to speed up the server. Signed-off-by: Paul Spooren <mail@aparcar.org>
1 parent 56c50ed commit 2e60db2

25 files changed

+408
-728
lines changed

asu/api.py

Lines changed: 10 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from rq import Connection, Queue
55

66
from .build import build
7-
from .common import get_request_hash, stats_profiles, stats_versions
7+
from .common import get_request_hash
88

99
bp = Blueprint("api", __name__, url_prefix="/api")
1010

@@ -37,7 +37,9 @@ def get_queue() -> Queue:
3737
"""
3838
if "queue" not in g:
3939
with Connection():
40-
g.queue = Queue(connection=get_redis())
40+
g.queue = Queue(
41+
connection=get_redis(), is_async=current_app.config["ASYNC_QUEUE"]
42+
)
4143
return g.queue
4244

4345

@@ -57,77 +59,6 @@ def api_latest():
5759
return redirect("/json/v1/latest.json")
5860

5961

60-
def api_v1_stats_images():
61-
return jsonify(
62-
{
63-
"total": int((get_redis().get("stats-images") or b"0").decode("utf-8")),
64-
"custom": int(
65-
(get_redis().get("stats-images-custom") or b"0").decode("utf-8")
66-
),
67-
}
68-
)
69-
70-
71-
def api_v1_stats_versions():
72-
return jsonify({"versions": stats_versions()})
73-
74-
75-
def api_v1_stats_targets(branch="SNAPSHOT"):
76-
if branch not in current_app.config["BRANCHES"]:
77-
return "", 404
78-
79-
return jsonify(
80-
{
81-
"branch": branch,
82-
"targets": [
83-
(s, p.decode("utf-8"))
84-
for p, s in get_redis().zrevrange(
85-
f"stats-targets-{branch}", 0, -1, withscores=True
86-
)
87-
],
88-
}
89-
)
90-
91-
92-
@bp.route("/v1/stats/targets/")
93-
def api_v1_stats_targets_default():
94-
return redirect("/api/v1/stats/targets/SNAPSHOT")
95-
96-
97-
def api_v1_stats_packages(branch="SNAPSHOT"):
98-
if branch not in current_app.config["BRANCHES"]:
99-
return "", 404
100-
101-
return jsonify(
102-
{
103-
"branch": branch,
104-
"packages": [
105-
(s, p.decode("utf-8"))
106-
for p, s in get_redis().zrevrange(
107-
f"stats-packages-{branch}", 0, -1, withscores=True
108-
)
109-
],
110-
}
111-
)
112-
113-
114-
@bp.route("/v1/stats/packages/")
115-
def api_v1_stats_packages_default():
116-
return redirect("/api/v1/stats/packages/SNAPSHOT")
117-
118-
119-
def api_v1_stats_profiles(branch):
120-
if branch not in current_app.config["BRANCHES"]:
121-
return "", 404
122-
123-
return jsonify({"branch": branch, "profiles": stats_profiles(branch)})
124-
125-
126-
@bp.route("/v1/stats/profiles/")
127-
def api_v1_stats_profiles_default():
128-
return redirect("/api/v1/stats/profiles/SNAPSHOT")
129-
130-
13162
def validate_packages(req):
13263
if req.get("packages_versions") and not req.get("packages"):
13364
req["packages"] = req["packages_versions"].keys()
@@ -188,7 +119,7 @@ def validate_request(req):
188119

189120
if "defaults" in req and not current_app.config["ALLOW_DEFAULTS"]:
190121
return (
191-
{"detail": f"Handling `defaults` not enabled on server", "status": 400},
122+
{"detail": "Handling `defaults` not enabled on server", "status": 400},
192123
400,
193124
)
194125

@@ -272,7 +203,7 @@ def return_job_v1(job):
272203
response.update(job.meta)
273204

274205
if job.is_failed:
275-
response.update({"status": 500, "detail": job.exc_info.strip().split("\n")[-1]})
206+
response.update({"status": 500})
276207

277208
elif job.is_queued:
278209
response.update(
@@ -294,11 +225,13 @@ def return_job_v1(job):
294225
headers = {"X-Imagebuilder-Status": response.get("imagebuilder_status", "init")}
295226

296227
elif job.is_finished:
297-
response.update({"status": 200, "build_at": job.ended_at, **job.result})
228+
response.update({"status": 200, **job.result})
298229

299230
response["enqueued_at"] = job.enqueued_at
300231
response["request_hash"] = job.id
301232

233+
print(response)
234+
302235
current_app.logger.debug(response)
303236
return response, response["status"], headers
304237

@@ -367,7 +300,7 @@ def return_job(job):
367300
status = 500
368301

369302
if job.is_failed:
370-
response["message"] = job.exc_info.strip().split("\n")[-1]
303+
response["message"] = job.meta["detail"]
371304

372305
elif job.is_queued:
373306
status = 202

asu/asu.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import json
2+
3+
# from msilib.schema import Registry
24
from os import getenv
35
from pathlib import Path
46

57
import connexion
6-
from flask import Flask, redirect, render_template, send_from_directory
8+
from flask import Flask, render_template, send_from_directory
9+
from prometheus_client import CollectorRegistry, make_wsgi_app
710
from redis import Redis
11+
from werkzeug.middleware.dispatcher import DispatcherMiddleware
812

913
import asu.common
1014
from asu import __version__
@@ -26,6 +30,7 @@ def create_app(test_config: dict = None) -> Flask:
2630

2731
cnxn = connexion.FlaskApp(__name__)
2832
app = cnxn.app
33+
2934
app.config.from_mapping(
3035
JSON_PATH=Path.cwd() / "public/json/v1/",
3136
REDIS_CONN=Redis(host=redis_host, port=redis_port, password=redis_password),
@@ -34,6 +39,7 @@ def create_app(test_config: dict = None) -> Flask:
3439
UPSTREAM_URL="https://downloads.openwrt.org",
3540
BRANCHES={},
3641
ALLOW_DEFAULTS=False,
42+
ASYNC_QUEUE=True,
3743
)
3844

3945
if not test_config:
@@ -45,6 +51,7 @@ def create_app(test_config: dict = None) -> Flask:
4551
print(f"Loading {config_file}")
4652
app.config.from_pyfile(config_file)
4753
break
54+
app.config["REGISTRY"] = CollectorRegistry()
4855
else:
4956
app.config.from_mapping(test_config)
5057

@@ -53,6 +60,10 @@ def create_app(test_config: dict = None) -> Flask:
5360
app.config[option] = Path(value)
5461
app.config[option].mkdir(parents=True, exist_ok=True)
5562

63+
app.wsgi_app = DispatcherMiddleware(
64+
app.wsgi_app, {"/metrics": make_wsgi_app(app.config["REGISTRY"])}
65+
)
66+
5667
(Path().cwd()).mkdir(exist_ok=True, parents=True)
5768

5869
@app.route("/json/")
@@ -74,6 +85,10 @@ def store_path(path="index.html"):
7485

7586
app.register_blueprint(api.bp)
7687

88+
from . import metrics
89+
90+
app.config["REGISTRY"].register(metrics.BuildCollector(app.config["REDIS_CONN"]))
91+
7792
branches = dict(
7893
map(
7994
lambda b: (b["name"], b),
@@ -142,6 +157,6 @@ def stats():
142157
if not app.config["REDIS_CONN"].hexists("mapping-abi", package):
143158
app.config["REDIS_CONN"].hset("mapping-abi", package, source)
144159

145-
cnxn.add_api("openapi.yml", validate_responses=True)
160+
cnxn.add_api("openapi.yml", validate_responses=app.config["TESTING"])
146161

147162
return app

0 commit comments

Comments
 (0)