Skip to content

Commit 86ae1ba

Browse files
api: add metrics endpoint (#630)
Adds a metrics endpoint that computes updated metric values on each request. On each request we: - fetch runner groups from Redis - get all runners for each group - update a Gauge for the runners_count on each group - render out the prometheus metrics in standard format Resolves #625
1 parent 94e45c1 commit 86ae1ba

File tree

5 files changed

+113
-3
lines changed

5 files changed

+113
-3
lines changed

poetry.lock

Lines changed: 16 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ rq-scheduler = "^0.13.1"
2727
pyvmomi = "^8.0.2.0.1"
2828
vapi-runtime = { url = "https://raw.githubusercontent.com/vmware/vsphere-automation-sdk-python/v8.0.1.0/lib/vapi-runtime/vapi_runtime-2.40.0-py2.py3-none-any.whl" }
2929
vcenter-bindings = { url = "https://raw.githubusercontent.com/vmware/vsphere-automation-sdk-python/v8.0.1.0/lib/vcenter-bindings/vcenter_bindings-4.1.0-py2.py3-none-any.whl" }
30+
prometheus-client = "^0.20.0"
3031

3132

3233
[tool.poetry.group.docs]

runner_manager/main.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,14 @@
88
from runner_manager import Runner, RunnerGroup, Settings, log
99
from runner_manager.dependencies import get_queue, get_redis, get_settings
1010
from runner_manager.jobs.startup import startup
11-
from runner_manager.routers import _health, private, public, runner_groups, webhook
11+
from runner_manager.routers import (
12+
_health,
13+
metrics,
14+
private,
15+
public,
16+
runner_groups,
17+
webhook,
18+
)
1219

1320
settings = get_settings()
1421
log.setLevel(settings.log_level)
@@ -39,6 +46,7 @@ async def lifespan(app: FastAPI):
3946
app.include_router(private.router)
4047
app.include_router(public.router)
4148
app.include_router(runner_groups.router)
49+
app.include_router(metrics.router)
4250

4351

4452
@app.get("/")

runner_manager/routers/metrics.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from typing import List
2+
3+
from fastapi import APIRouter
4+
from fastapi.responses import PlainTextResponse
5+
from prometheus_client import Gauge, generate_latest
6+
7+
from runner_manager import RunnerGroup
8+
from runner_manager.models.runner import Runner
9+
10+
router = APIRouter(prefix="/metrics")
11+
12+
runners_count = Gauge("runners_count", "Number of runners", ["runner_group"])
13+
14+
15+
@router.get("/", response_class=PlainTextResponse)
16+
def compute_metrics() -> PlainTextResponse:
17+
groups: List[RunnerGroup] = RunnerGroup.find().all()
18+
for group in groups:
19+
runners: List[Runner] = group.get_runners()
20+
runners_count.labels(runner_group=group.name).set(len(runners))
21+
metrics = generate_latest().decode()
22+
return PlainTextResponse(content=metrics)

tests/api/test_metrics.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
from fastapi.testclient import TestClient
2+
3+
from runner_manager import RunnerGroup
4+
from runner_manager.clients.github import GitHub
5+
6+
7+
def test_metrics_endpoint(client: TestClient, runner_group: RunnerGroup):
8+
runner_group.save()
9+
response = client.get("/metrics")
10+
assert response.status_code == 200
11+
runner_lines = [
12+
line for line in response.text.splitlines() if line.startswith("runners_")
13+
]
14+
assert f'runners_count{{runner_group="{runner_group.name}"}} 0.0' in runner_lines
15+
16+
17+
def test_runner_count(client: TestClient, runner_group: RunnerGroup, github: GitHub):
18+
runner_group.save()
19+
want = len(runner_group.get_runners())
20+
21+
response = client.get("/metrics")
22+
assert response.status_code == 200
23+
got = [line for line in response.text.splitlines() if line.startswith("runners_")]
24+
print(want)
25+
assert f'runners_count{{runner_group="{runner_group.name}"}} {want:.1f}' in got
26+
27+
runner_group.max = 2
28+
runner_group.min = 1
29+
runner_group.save()
30+
31+
want = 0.0
32+
response = client.get("/metrics")
33+
assert response.status_code == 200
34+
before = [
35+
line for line in response.text.splitlines() if line.startswith("runners_")
36+
]
37+
assert f'runners_count{{runner_group="{runner_group.name}"}} {want:.1f}' in before
38+
39+
runner = runner_group.create_runner(github)
40+
assert runner is not None
41+
42+
want = 1.0
43+
response = client.get("/metrics")
44+
assert response.status_code == 200
45+
after_create = [
46+
line for line in response.text.splitlines() if line.startswith("runners_")
47+
]
48+
assert (
49+
f'runners_count{{runner_group="{runner_group.name}"}} {want:.1f}'
50+
in after_create
51+
)
52+
53+
runner = runner_group.delete_runner(runner, github)
54+
assert runner is not None
55+
56+
want = 0.0
57+
response = client.get("/metrics")
58+
assert response.status_code == 200
59+
after_delete = [
60+
line for line in response.text.splitlines() if line.startswith("runners_")
61+
]
62+
assert (
63+
f'runners_count{{runner_group="{runner_group.name}"}} {want:.1f}'
64+
in after_delete
65+
)

0 commit comments

Comments
 (0)