diff --git a/PulumiWebServer/Nix/flake.lock b/PulumiWebServer/Nix/flake.lock index 680f625..d3a7991 100644 --- a/PulumiWebServer/Nix/flake.lock +++ b/PulumiWebServer/Nix/flake.lock @@ -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": { diff --git a/PulumiWebServer/Nix/grafana/grafana.nix b/PulumiWebServer/Nix/grafana/grafana.nix index e90feb5..0144b82 100644 --- a/PulumiWebServer/Nix/grafana/grafana.nix +++ b/PulumiWebServer/Nix/grafana/grafana.nix @@ -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 = { diff --git a/PulumiWebServer/Nix/grafana/puregym.json b/PulumiWebServer/Nix/grafana/puregym.json new file mode 100644 index 0000000..a4db00b --- /dev/null +++ b/PulumiWebServer/Nix/grafana/puregym.json @@ -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": "" +} diff --git a/PulumiWebServer/Nix/prometheus/prometheus.nix b/PulumiWebServer/Nix/prometheus/prometheus.nix index 0e8c872..1e7478b 100644 --- a/PulumiWebServer/Nix/prometheus/prometheus.nix +++ b/PulumiWebServer/Nix/prometheus/prometheus.nix @@ -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 = [ diff --git a/PulumiWebServer/Nix/puregym/puregym.py b/PulumiWebServer/Nix/puregym/puregym.py index b03546d..f3080b2 100644 --- a/PulumiWebServer/Nix/puregym/puregym.py +++ b/PulumiWebServer/Nix/puregym/puregym.py @@ -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): diff --git a/flake.lock b/flake.lock index 5b4c88d..2f80ed7 100644 --- a/flake.lock +++ b/flake.lock @@ -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": {