Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add qsa-cli stats command #13

Merged
merged 4 commits into from
Apr 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/src/qsa-cli/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
13 changes: 13 additions & 0 deletions qsa-api/qsa_api/api/instances.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,16 @@ def instances_logs(instance):
return {"error": "QGIS Server instance is not available"}, 415

return monitor.conns[instance].logs


@instances.get("/<instance>/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
10 changes: 10 additions & 0 deletions qsa-api/qsa_api/monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
64 changes: 64 additions & 0 deletions qsa-cli/qsa/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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/")
Expand Down Expand Up @@ -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
52 changes: 50 additions & 2 deletions qsa-plugin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand All @@ -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)))
Expand All @@ -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)
2 changes: 2 additions & 0 deletions sandbox/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading