Add PureGym Prometheus stats (#28)

This commit is contained in:
Patrick Stevens
2024-02-12 22:52:07 +00:00
committed by GitHub
parent e879dbcd60
commit 6f4bc26313
6 changed files with 259 additions and 37 deletions

View File

@@ -156,11 +156,11 @@
]
},
"locked": {
"lastModified": 1706473109,
"narHash": "sha256-iyuAvpKTsq2u23Cr07RcV5XlfKExrG8gRpF75hf1uVc=",
"lastModified": 1707683400,
"narHash": "sha256-Zc+J3UO1Xpx+NL8UB6woPHyttEy9cXXtm+0uWwzuYDc=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "d634c3abafa454551f2083b054cd95c3f287be61",
"rev": "21b078306a2ab68748abf72650db313d646cf2ca",
"type": "github"
},
"original": {
@@ -226,11 +226,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1706373441,
"narHash": "sha256-S1hbgNbVYhuY2L05OANWqmRzj4cElcbLuIkXTb69xkk=",
"lastModified": 1707650010,
"narHash": "sha256-dOhphIA4MGrH4ElNCy/OlwmN24MsnEqFjRR6+RY7jZw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "56911ef3403a9318b7621ce745f5452fb9ef6867",
"rev": "809cca784b9f72a5ad4b991e0e7bcf8890f9c3a6",
"type": "github"
},
"original": {
@@ -242,16 +242,16 @@
},
"nixpkgs-stable": {
"locked": {
"lastModified": 1705957679,
"narHash": "sha256-Q8LJaVZGJ9wo33wBafvZSzapYsjOaNjP/pOnSiKVGHY=",
"lastModified": 1707603439,
"narHash": "sha256-LodBVZ3+ehJP2azM5oj+JrhfNAAzmTJ/OwAIOn0RfZ0=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "9a333eaa80901efe01df07eade2c16d183761fa3",
"rev": "d8cd80616c8800feec0cab64331d7c3d5a1a6d98",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "release-23.05",
"ref": "release-23.11",
"repo": "nixpkgs",
"type": "github"
}
@@ -273,11 +273,11 @@
},
"nixpkgs_3": {
"locked": {
"lastModified": 1706173671,
"narHash": "sha256-lciR7kQUK2FCAYuszyd7zyRRmTaXVeoZsCyK6QFpGdk=",
"lastModified": 1707451808,
"narHash": "sha256-UwDBUNHNRsYKFJzyTMVMTF5qS4xeJlWoeyJf+6vvamU=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "4fddc9be4eaf195d631333908f2a454b03628ee5",
"rev": "442d407992384ed9c0e6d352de75b69079904e4e",
"type": "github"
},
"original": {
@@ -380,11 +380,11 @@
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1706573865,
"narHash": "sha256-Gfk5mMz6gRLVwljXrPTfqAX7Bp8UQK4vKtXu9kYkzQM=",
"lastModified": 1707775682,
"narHash": "sha256-cLIgwrkNAkJpTgKdzU0qaWwy8rClqIBYYjOm/UHprcg=",
"ref": "refs/heads/main",
"rev": "e96ae78665db13cbf4745c04d634a8a878549768",
"revCount": 10,
"rev": "419f27053f92ad0f0e42874cdc584fb0cca534e3",
"revCount": 12,
"type": "git",
"url": "https://gitea.patrickstevens.co.uk/patrick/puregym-unofficial-dotnet"
},
@@ -469,11 +469,11 @@
"nixpkgs-stable": "nixpkgs-stable"
},
"locked": {
"lastModified": 1706410821,
"narHash": "sha256-iCfXspqUOPLwRobqQNAQeKzprEyVowLMn17QaRPQc+M=",
"lastModified": 1707748232,
"narHash": "sha256-o9L8jrOemQl/5cYp++0cWdfMLzVljCdHwPFF4N0KZeQ=",
"owner": "Mic92",
"repo": "sops-nix",
"rev": "73bf36912e31a6b21af6e0f39218e067283c67ef",
"rev": "695275c349bb27f91b2b06cb742510899c887b81",
"type": "github"
},
"original": {

View File

@@ -41,6 +41,13 @@
mode = "0440";
};
environment.etc."grafana-dashboards/puregym.json" = {
source = ./puregym.json;
group = "grafana";
user = "grafana";
mode = "0440";
};
services.grafana = {
enable = true;
settings = {

View File

@@ -0,0 +1,144 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"links": [],
"liveNow": false,
"panels": [
{
"datasource": {
"type": "prometheus",
"uid": "P40645DF18AF953B4"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"axisSoftMin": 0,
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 0
},
"id": 1,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"timezone": [
"Europe/London"
],
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "P40645DF18AF953B4"
},
"disableTextWrap": false,
"editorMode": "builder",
"expr": "fullness",
"fullMetaSearch": false,
"includeNullMetadata": true,
"instant": false,
"legendFormat": "{{label}}",
"range": true,
"refId": "A",
"useBackend": false
}
],
"title": "Gym occupancy",
"type": "timeseries"
}
],
"refresh": "",
"schemaVersion": 38,
"tags": [],
"templating": {
"list": []
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "PureGym",
"uid": "f1399663-bebe-42d8-9162-b046af9fb0c2",
"version": 3,
"weekStart": ""
}

View File

@@ -52,6 +52,18 @@
};
scrapeConfigs = [
{
job_name = "gym-fullness";
static_configs = [
{
# Gym 19 is London Oval
targets = ["localhost:${toString config.services.puregym-config.port}"];
}
];
params = { gym_id = ["19"]; };
metrics_path = "/fullness-prometheus";
scrape_interval = "5m";
}
{
job_name = "node";
static_configs = [

View File

@@ -13,12 +13,49 @@ class MyHandler(BaseHTTPRequestHandler):
_last_accessed_by_id = defaultdict(lambda: datetime.min)
_last_accessed_by_name = defaultdict(lambda: datetime.min)
_all_gyms = {}
_last_refreshed_gyms = datetime.min
def _bad_request(self, text: str, code: int = 400) -> None:
self.send_response(code)
self.send_header('Content-type', 'text/plain; charset=utf-8')
self.end_headers()
self.wfile.write(text.encode('utf-8'))
def _get_fullness(self, gym_id: int) -> bytes:
if abs(datetime.now() - self._last_accessed_by_id[gym_id]) > timedelta(seconds=30):
token = subprocess.check_output(['cat', '/tmp/puregym_token']).strip()
output = subprocess.check_output(
[puregym, 'fullness', '--bearer-token', token, '--gym-id', str(gym_id)], text=True,
encoding='utf-8')
output = output.encode('utf-8')
self._cache_result_by_id[gym_id] = output
self._last_accessed_by_id[gym_id] = datetime.now()
else:
output = self._cache_result_by_id[gym_id]
return output
def _refresh_gyms(self) -> None:
if self._last_refreshed_gyms < datetime.now() - timedelta(days=1):
token = subprocess.check_output(['cat', '/tmp/puregym_token']).strip()
output = subprocess.check_output(
[puregym, 'all-gyms', '--bearer-token', token, '--terse', 'true'], text=True,
encoding='utf-8')
new_gyms = {}
for line in output.splitlines():
gym_id, gym_name = line.split(',')
new_gyms[int(gym_id)] = gym_name
self._all_gyms = new_gyms
self._last_refreshed_gyms = datetime.now()
def get_all_gyms(self, _query: dict[AnyStr, list[AnyStr]]) -> None:
self._refresh_gyms()
self.send_response(200)
self.send_header('Content-type', 'text/plain')
self.end_headers()
for gym_id, gym_name in self._all_gyms.items():
self.wfile.write(f'{gym_id}: {gym_name}\n'.encode())
def get_fullness(self, query: dict[AnyStr, list[AnyStr]]) -> None:
desired_gym_name = None
query_gym = query.get("gym_name", None)
@@ -48,16 +85,7 @@ class MyHandler(BaseHTTPRequestHandler):
desired_gym_id = None
if desired_gym_id is not None:
if abs(datetime.now() - self._last_accessed_by_id[desired_gym_id]) > timedelta(seconds=30):
token = subprocess.check_output(['cat', '/tmp/puregym_token']).strip()
output = subprocess.check_output(
[puregym, 'fullness', '--bearer-token', token, '--gym-id', str(desired_gym_id)], text=True,
encoding='utf-8')
output = output.encode('utf-8')
self._cache_result_by_id[desired_gym_id] = output
self._last_accessed_by_id[desired_gym_id] = datetime.now()
else:
output = self._cache_result_by_id[desired_gym_id]
output = self._get_fullness(desired_gym_id)
elif desired_gym_name is not None:
if abs(datetime.now() - self._last_accessed_by_name[desired_gym_name]) > timedelta(seconds=30):
token = subprocess.check_output(['cat', '/tmp/puregym_token']).strip()
@@ -79,8 +107,39 @@ class MyHandler(BaseHTTPRequestHandler):
self.end_headers()
self.wfile.write(output)
def get_prometheus(self, query: dict[AnyStr, list[AnyStr]]) -> None:
query_gym = query.get("gym_id", None)
if query_gym is None:
self._bad_request('Must supply gym_id')
return
try:
gym_id = [int(i) for i in query_gym]
except ValueError:
self._bad_request('at least one gym_id did not parse as an int')
return
if not gym_id:
self._bad_request('supply at least one gym_id')
return
try:
fullness = [(i, int(self._get_fullness(i).split(b' ')[0])) for i in gym_id]
except ValueError:
self._bad_request('at least one fullness did not yield an int', 500)
return
self.send_response(200)
self.send_header('Content-type', 'text/plain')
self.end_headers()
self._refresh_gyms()
for gym_id, fullness in fullness:
gym_name = ''.join(c for c in self._all_gyms[gym_id] if c == ' ' or str.isalnum(c))
self.wfile.write(f'fullness{{label="{gym_name}"}} {fullness}\n'.encode())
_handlers: dict[str, Callable[["MyHandler", dict[AnyStr, list[AnyStr]]], None]] = {
"/fullness": get_fullness
"/fullness": get_fullness,
"/fullness-prometheus": get_prometheus,
"/gym-mapping": get_all_gyms,
}
def do_GET(self):

12
flake.lock generated
View File

@@ -5,11 +5,11 @@
"systems": "systems"
},
"locked": {
"lastModified": 1701680307,
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
"lastModified": 1705309234,
"narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
"rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
"type": "github"
},
"original": {
@@ -20,11 +20,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1703637592,
"narHash": "sha256-8MXjxU0RfFfzl57Zy3OfXCITS0qWDNLzlBAdwxGZwfY=",
"lastModified": 1706732774,
"narHash": "sha256-hqJlyJk4MRpcItGYMF+3uHe8HvxNETWvlGtLuVpqLU0=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "cfc3698c31b1fb9cdcf10f36c9643460264d0ca8",
"rev": "b8b232ae7b8b144397fdb12d20f592e5e7c1a64d",
"type": "github"
},
"original": {