diff --git a/README.md b/README.md index c320c72..a050ea3 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,9 @@ Components: -* [QSA REST API](https://pblottiere.github.io/QSA/qsa-api/): Flask web server with a REST API for administrating QGIS Server -* [QSA plugin](https://pblottiere.github.io/QSA/qsa-plugin/): QGIS Server plugin for introspection -* [QSA cli](https://pblottiere.github.io/QSA/qsa-cli/): Command line tool +* `qsa-api`: Flask web server with a REST API for administrating QGIS Server +* `qsa-plugin`: QGIS Server plugin for introspection +* `qsa-cli`: Command line tool Main features: * Create and manage QGIS projects stored on filesystem diff --git a/docs/src/qsa-cli/commands.md b/docs/src/qsa-cli/commands.md index 1a0c3a7..ba87366 100644 --- a/docs/src/qsa-cli/commands.md +++ b/docs/src/qsa-cli/commands.md @@ -13,6 +13,7 @@ Commands: inspect Returns metadata about a specific QGIS Server instance logs Returns logs of a specific QGIS Server instance ps List QGIS Server instances + stats Returns stats of QGIS Server instances ```` Examples: diff --git a/qsa-api/qsa_api/api/instances.py b/qsa-api/qsa_api/api/instances.py index 2e756ab..91d0b63 100644 --- a/qsa-api/qsa_api/api/instances.py +++ b/qsa-api/qsa_api/api/instances.py @@ -50,3 +50,16 @@ def instances_logs(instance): return {"error": "QGIS Server instance is not available"}, 415 return monitor.conns[instance].logs + + +@instances.get("//stats") +def instances_stats(instance): + monitor = current_app.config["MONITOR"] + + if not monitor: + return {"error": "QGIS Server monitoring is not activated"}, 415 + + if instance not in monitor.conns: + return {"error": "QGIS Server instance is not available"}, 415 + + return monitor.conns[instance].stats diff --git a/qsa-api/qsa_api/monitor.py b/qsa-api/qsa_api/monitor.py index f2b8c5a..ab5fb31 100644 --- a/qsa-api/qsa_api/monitor.py +++ b/qsa-api/qsa_api/monitor.py @@ -56,6 +56,16 @@ def logs(self) -> dict: print(e, file=sys.stderr) return {} + @property + def stats(self) -> dict: + self.response = None + try: + self.con.send(b"stats") + return self._wait_recv() + except Exception as e: + print(e, file=sys.stderr) + return {} + def _wait_recv(self): it = 0 diff --git a/qsa-cli/qsa/cli.py b/qsa-cli/qsa/cli.py index 0128490..39a9d85 100644 --- a/qsa-cli/qsa/cli.py +++ b/qsa-cli/qsa/cli.py @@ -2,8 +2,10 @@ import os import json +import time import click import requests +from pathlib import Path from tabulate import tabulate QSA_URL = os.environ.get("QSA_SERVER_URL", "http://localhost:5000/") @@ -61,3 +63,65 @@ def logs(id): data = requests.get(url) print(data.json()["logs"]) + + +@cli.command() +@click.argument("id", required=False) +def stats(id): + """ + Returns stats of QGIS Server instances + """ + + ids = [] + if id: + ids.append(id) + else: + url = f"{QSA_URL}/api/instances" + data = requests.get(url) + for s in data.json()["servers"]: + ids.append(s["id"]) + + headers = [ + "INSTANCE ID", + "COUNT", + "TIME ", + "SERVICE", + "REQUEST", + "PROJECT", + ] + + try: + while 1: + table = [] + for i in ids: + url = f"{QSA_URL}/api/instances/{i}/stats" + task = requests.get(url).json() + + if "error" in task: + continue + + t = [] + t.append(i) + t.append(task["count"]) + + if "service" in task: + t.append(f"{task['duration']} ms") + t.append(task["service"]) + t.append(task["request"]) + p = Path(task["project"]).name + t.append(p) + else: + t.append("") + t.append("") + t.append("") + t.append("") + + table.append(t) + + s = tabulate(table, headers=headers) + os.system("cls" if os.name == "nt" else "clear") + print(s) + + time.sleep(0.25) + except: + pass diff --git a/qsa-plugin/__init__.py b/qsa-plugin/__init__.py index 8ff23ba..d21ceb4 100644 --- a/qsa-plugin/__init__.py +++ b/qsa-plugin/__init__.py @@ -13,19 +13,59 @@ from datetime import datetime from qgis import PyQt -from qgis.server import QgsConfigCache from qgis.utils import server_active_plugins +from qgis.server import QgsConfigCache, QgsServerFilter from qgis.core import Qgis, QgsProviderRegistry, QgsApplication LOG_MESSAGES = [] +class ProbeFilter(QgsServerFilter): + def __init__(self, iface, task): + super().__init__(iface) + self.task = task + + def onRequestReady(self) -> bool: + request = self.serverInterface().requestHandler() + params = request.parameterMap() + + self.task["project"] = params.get("MAP", "") + self.task["service"] = params.get("SERVICE", "") + self.task["request"] = params.get("REQUEST", "") + self.task["start"] = datetime.now() + self.task["count"] += 1 + + return True + + def onResponseComplete(self) -> bool: + self._clear_task() + return True + + def onSendResponse(self) -> bool: + self._clear_task() + return True + + def _clear_task(self): + count = self.task["count"] + self.task.clear() + self.task["count"] = count + + def log_messages(): m = {} m["logs"] = "\n".join(LOG_MESSAGES) return m +def stats(task): + s = task + if "start" in s: + s["duration"] = int( + (datetime.now() - s["start"]).total_seconds() * 1000 + ) + return s + + def metadata(iface) -> dict: m = {} m["plugins"] = server_active_plugins @@ -61,7 +101,7 @@ def auto_connect(s: socket.socket, host: str, port: int) -> socket.socket: return s -def f(iface, host: str, port: int) -> None: +def f(iface, host: str, port: int, task: dict) -> None: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s = auto_connect(s, host, port) @@ -74,6 +114,8 @@ def f(iface, host: str, port: int) -> None: payload = metadata(iface) elif b"logs" in data: payload = log_messages() + elif b"stats" in data: + payload = stats(task) ser = pickle.dumps(payload) s.sendall(struct.pack(">I", len(ser))) @@ -95,12 +137,18 @@ def serverClassFactory(iface): host = str(os.environ.get("QSA_HOST", "localhost")) port = int(os.environ.get("QSA_PORT", 9999)) + task = {} + task["count"] = 0 + t = Thread( target=f, args=( iface, host.replace('"', ""), port, + task, ), ) t.start() + + iface.registerFilter(ProbeFilter(iface, task), 100) diff --git a/sandbox/docker-compose.yml b/sandbox/docker-compose.yml index cec4912..e529383 100644 --- a/sandbox/docker-compose.yml +++ b/sandbox/docker-compose.yml @@ -12,6 +12,8 @@ services: - QGIS_SERVER_LOG_LEVEL=0 - QSA_HOST="qsa" - QSA_PORT=9999 + ports: + - "8080:80" qsa: image: pblottiere/qsa entrypoint: "qsa"