Skip to content

Commit

Permalink
api: provide /metrics for prometheus export
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
aparcar committed Mar 14, 2022
1 parent 56c50ed commit 2e60db2
Show file tree
Hide file tree
Showing 25 changed files with 408 additions and 728 deletions.
87 changes: 10 additions & 77 deletions asu/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from rq import Connection, Queue

from .build import build
from .common import get_request_hash, stats_profiles, stats_versions
from .common import get_request_hash

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

Expand Down Expand Up @@ -37,7 +37,9 @@ def get_queue() -> Queue:
"""
if "queue" not in g:
with Connection():
g.queue = Queue(connection=get_redis())
g.queue = Queue(
connection=get_redis(), is_async=current_app.config["ASYNC_QUEUE"]
)
return g.queue


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


def api_v1_stats_images():
return jsonify(
{
"total": int((get_redis().get("stats-images") or b"0").decode("utf-8")),
"custom": int(
(get_redis().get("stats-images-custom") or b"0").decode("utf-8")
),
}
)


def api_v1_stats_versions():
return jsonify({"versions": stats_versions()})


def api_v1_stats_targets(branch="SNAPSHOT"):
if branch not in current_app.config["BRANCHES"]:
return "", 404

return jsonify(
{
"branch": branch,
"targets": [
(s, p.decode("utf-8"))
for p, s in get_redis().zrevrange(
f"stats-targets-{branch}", 0, -1, withscores=True
)
],
}
)


@bp.route("/v1/stats/targets/")
def api_v1_stats_targets_default():
return redirect("/api/v1/stats/targets/SNAPSHOT")


def api_v1_stats_packages(branch="SNAPSHOT"):
if branch not in current_app.config["BRANCHES"]:
return "", 404

return jsonify(
{
"branch": branch,
"packages": [
(s, p.decode("utf-8"))
for p, s in get_redis().zrevrange(
f"stats-packages-{branch}", 0, -1, withscores=True
)
],
}
)


@bp.route("/v1/stats/packages/")
def api_v1_stats_packages_default():
return redirect("/api/v1/stats/packages/SNAPSHOT")


def api_v1_stats_profiles(branch):
if branch not in current_app.config["BRANCHES"]:
return "", 404

return jsonify({"branch": branch, "profiles": stats_profiles(branch)})


@bp.route("/v1/stats/profiles/")
def api_v1_stats_profiles_default():
return redirect("/api/v1/stats/profiles/SNAPSHOT")


def validate_packages(req):
if req.get("packages_versions") and not req.get("packages"):
req["packages"] = req["packages_versions"].keys()
Expand Down Expand Up @@ -188,7 +119,7 @@ def validate_request(req):

if "defaults" in req and not current_app.config["ALLOW_DEFAULTS"]:
return (
{"detail": f"Handling `defaults` not enabled on server", "status": 400},
{"detail": "Handling `defaults` not enabled on server", "status": 400},
400,
)

Expand Down Expand Up @@ -272,7 +203,7 @@ def return_job_v1(job):
response.update(job.meta)

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

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

elif job.is_finished:
response.update({"status": 200, "build_at": job.ended_at, **job.result})
response.update({"status": 200, **job.result})

response["enqueued_at"] = job.enqueued_at
response["request_hash"] = job.id

print(response)

current_app.logger.debug(response)
return response, response["status"], headers

Expand Down Expand Up @@ -367,7 +300,7 @@ def return_job(job):
status = 500

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

elif job.is_queued:
status = 202
Expand Down
19 changes: 17 additions & 2 deletions asu/asu.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import json

# from msilib.schema import Registry
from os import getenv
from pathlib import Path

import connexion
from flask import Flask, redirect, render_template, send_from_directory
from flask import Flask, render_template, send_from_directory
from prometheus_client import CollectorRegistry, make_wsgi_app
from redis import Redis
from werkzeug.middleware.dispatcher import DispatcherMiddleware

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

cnxn = connexion.FlaskApp(__name__)
app = cnxn.app

app.config.from_mapping(
JSON_PATH=Path.cwd() / "public/json/v1/",
REDIS_CONN=Redis(host=redis_host, port=redis_port, password=redis_password),
Expand All @@ -34,6 +39,7 @@ def create_app(test_config: dict = None) -> Flask:
UPSTREAM_URL="https://downloads.openwrt.org",
BRANCHES={},
ALLOW_DEFAULTS=False,
ASYNC_QUEUE=True,
)

if not test_config:
Expand All @@ -45,6 +51,7 @@ def create_app(test_config: dict = None) -> Flask:
print(f"Loading {config_file}")
app.config.from_pyfile(config_file)
break
app.config["REGISTRY"] = CollectorRegistry()
else:
app.config.from_mapping(test_config)

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

app.wsgi_app = DispatcherMiddleware(
app.wsgi_app, {"/metrics": make_wsgi_app(app.config["REGISTRY"])}
)

(Path().cwd()).mkdir(exist_ok=True, parents=True)

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

app.register_blueprint(api.bp)

from . import metrics

app.config["REGISTRY"].register(metrics.BuildCollector(app.config["REDIS_CONN"]))

branches = dict(
map(
lambda b: (b["name"], b),
Expand Down Expand Up @@ -142,6 +157,6 @@ def stats():
if not app.config["REDIS_CONN"].hexists("mapping-abi", package):
app.config["REDIS_CONN"].hset("mapping-abi", package, source)

cnxn.add_api("openapi.yml", validate_responses=True)
cnxn.add_api("openapi.yml", validate_responses=app.config["TESTING"])

return app
Loading

0 comments on commit 2e60db2

Please sign in to comment.