Skip to content

Commit

Permalink
Split system information by category on monitor page
Browse files Browse the repository at this point in the history
Limit uptime metrics to 2 identifiers in monitor page
Hide all monitor endpoints in schema page
  • Loading branch information
dormant-user committed Sep 15, 2024
1 parent 16a0c4b commit 47652fe
Show file tree
Hide file tree
Showing 16 changed files with 285 additions and 160 deletions.
8 changes: 4 additions & 4 deletions doc_gen/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -114,22 +114,22 @@ PyNinja - Monitor
=================

Authenticator
=============
-------------

.. automodule:: pyninja.monitor.authenticator

Configuration
=============
-------------

.. automodule:: pyninja.monitor.config

Routes
======
------

.. automodule:: pyninja.monitor.routes

Secure
======
------

.. automodule:: pyninja.monitor.secure

Expand Down
2 changes: 1 addition & 1 deletion docs/README.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="generator" content="Docutils 0.19: https://docutils.sourceforge.io/" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="viewport" content="width=device-width, initial-scale=1" />

<title>PyNinja &#8212; PyNinja documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
Expand Down
8 changes: 4 additions & 4 deletions docs/_sources/index.rst.txt
Original file line number Diff line number Diff line change
Expand Up @@ -114,22 +114,22 @@ PyNinja - Monitor
=================

Authenticator
=============
-------------

.. automodule:: pyninja.monitor.authenticator

Configuration
=============
-------------

.. automodule:: pyninja.monitor.config

Routes
======
------

.. automodule:: pyninja.monitor.routes

Secure
======
------

.. automodule:: pyninja.monitor.secure

Expand Down
2 changes: 2 additions & 0 deletions docs/genindex.html
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ <h2 id="C">C</h2>
<table style="width: 100%" class="indextable genindextable"><tr>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="index.html#pyninja.monitor.secure.calculate_hash">calculate_hash() (in module pyninja.monitor.secure)</a>
</li>
<li><a href="index.html#pyninja.monitor.config.capwords_filter">capwords_filter() (in module pyninja.monitor.config)</a>
</li>
<li><a href="index.html#pyninja.monitor.config.clear_session">clear_session() (in module pyninja.monitor.config)</a>
</li>
Expand Down
173 changes: 114 additions & 59 deletions docs/index.html

Large diffs are not rendered by default.

Binary file modified docs/objects.inv
Binary file not shown.
2 changes: 1 addition & 1 deletion docs/searchindex.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions pyninja/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,10 @@ async def level_2(


async def incrementer(attempt: int) -> int:
"""Increments block time for a host address based on the number of failed attempts.
"""Increments block time for a host address based on the number of failed login attempts.
Args:
attempt: Number of failed attempts.
attempt: Number of failed login attempts.
Returns:
int:
Expand Down
3 changes: 1 addition & 2 deletions pyninja/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,7 @@ def raise_os_error() -> NoReturn:
"""Raises a custom exception for unsupported OS.
Raises:
ValidationError:
Overridden exception from ``pydantic.ValidationError`` for unsupported OS.
ValidationError: Overridden exception from ``pydantic.ValidationError`` for unsupported OS.
"""
# https://docs.pydantic.dev/latest/errors/validation_errors/#model_type
raise ValidationError.from_exception_data(
Expand Down
14 changes: 11 additions & 3 deletions pyninja/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ def get_desc(remote_flag: bool, monitor_flag: bool) -> str:
monitor_fl = "Disabled"
description = "**Lightweight OS-agnostic service monitoring API**"
description += (
"\n\nIn addition to monitoring services, processes and containers, "
"PyNinja API also allows you to execute remote commands and host a monitoring page for "
"system resources. 🚀"
"\n\nIn addition to monitoring services, processes, and containers,"
"the PyNinja API provides optional features for executing remote commands "
"and hosting a real-time system resource monitoring page. 🚀"
)
description += "\n\n#### Basic Features"
description += "\n- <a href='/docs#/default/get_ip_get_ip_get'>/get-ip</a><br>"
Expand All @@ -66,6 +66,14 @@ def get_desc(remote_flag: bool, monitor_flag: bool) -> str:
description += "\n\n#### Current State"
description += f"\n- **Remote Execution:** {remote_fl}"
description += f"\n- **Monitoring Page:** {monitor_fl}"
description += "\n\n#### Links"
description += (
"\n- <a href='https://pypi.org/project/PyNinja/'>PyPi Repository</a><br>"
)
description += (
"\n- <a href='https://github.com/thevickypedia/PyNinja'>GitHub Homepage</a><br>"
)
description += "\n- <a href='https://thevickypedia.github.io/PyNinja/'>Sphinx Documentation</a><br>"
return description


Expand Down
4 changes: 4 additions & 0 deletions pyninja/monitor/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,24 +24,28 @@ def get_all_monitor_routes(
endpoint=routes.login_endpoint,
methods=["POST"],
dependencies=dependencies,
include_in_schema=False,
),
APIRoute(
path="/error",
endpoint=routes.error_endpoint,
methods=["GET"],
dependencies=dependencies,
include_in_schema=False,
),
APIRoute(
path="/monitor",
endpoint=routes.monitor_endpoint,
methods=["GET"],
dependencies=dependencies,
include_in_schema=False,
),
APIRoute(
path="/logout",
endpoint=routes.logout_endpoint,
methods=["GET"],
dependencies=dependencies,
include_in_schema=False,
),
APIWebSocketRoute(path="/ws/system", endpoint=routes.websocket_endpoint),
]
26 changes: 26 additions & 0 deletions pyninja/monitor/config.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,38 @@
import os
import string
import time

from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates


def capwords_filter(value: str) -> str:
"""Capitalizes a string.
Args:
value: String value to be capitalized.
See Also:
This function is added as a filter to Jinja2 templates.
Returns:
str:
Returns the capitalized string.
"""
if value.endswith("_raw"):
parts = value.split("_")
return " ".join(parts[:-1])
if value.endswith("_cap"):
parts = value.split("_")
return parts[0].upper() + " " + " ".join(parts[1:-1])
return string.capwords(value).replace("_", " ")


templates = Jinja2Templates(
directory=os.path.join(os.path.dirname(__file__), "templates")
)
# Add custom filter to Jinja2 environment
templates.env.filters["capwords"] = capwords_filter


async def clear_session(response: HTMLResponse) -> HTMLResponse:
Expand Down
50 changes: 29 additions & 21 deletions pyninja/monitor/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,21 +123,32 @@ async def monitor_endpoint(request: Request, session_token: str = Cookie(None)):
)
else:
uname = platform.uname()
ctx = dict(
request=request,
default_cpu_interval=models.ws_settings.cpu_interval,
default_refresh_interval=models.ws_settings.refresh_interval,
sys_info_basic = dict(
node=uname.node,
system=uname.system,
machine=uname.machine,
cores=psutil.cpu_count(logical=True),
memory=squire.size_converter(psutil.virtual_memory().total),
storage=squire.size_converter(shutil.disk_usage("/").total),
swap=squire.size_converter(psutil.swap_memory().total),
logout="/logout",
architecture=uname.machine,
cpu_cores_cap=psutil.cpu_count(logical=True),
uptime=squire.format_timedelta(
timedelta(seconds=time.time() - psutil.boot_time())
),
)
sys_info_mem_storage = dict(
memory=squire.size_converter(psutil.virtual_memory().total),
swap=squire.size_converter(psutil.swap_memory().total),
storage=squire.size_converter(shutil.disk_usage("/").total),
)
sys_info_network = dict(
Private_IP_address_raw=squire.private_ip_address(),
Public_IP_address_raw=squire.public_ip_address(),
)
ctx = dict(
request=request,
default_cpu_interval=models.ws_settings.cpu_interval,
default_refresh_interval=models.ws_settings.refresh_interval,
logout="/logout",
sys_info_basic=sys_info_basic,
sys_info_mem_storage=sys_info_mem_storage,
sys_info_network=sys_info_network,
version=version.__version__,
)
if processor_name := processor.get_name():
Expand Down Expand Up @@ -177,13 +188,11 @@ async def websocket_endpoint(websocket: WebSocket, session_token: str = Cookie(N
session_timestamp = models.ws_session.client_auth.get(websocket.client.host).get(
"timestamp"
)
refresh_time = time.time()
LOGGER.info(
"Intervals: {'CPU': %s, 'refresh': %s}",
models.ws_settings.cpu_interval,
models.ws_settings.refresh_interval,
)
data = squire.system_resources(models.ws_settings.cpu_interval)
task = asyncio.create_task(asyncio.sleep(0.1))
while True:
# Validate session asynchronously (non-blocking)
Expand All @@ -210,6 +219,7 @@ async def websocket_endpoint(websocket: WebSocket, session_token: str = Cookie(N
if websocket.application_state == WebSocketState.CONNECTED:
try:
msg = await asyncio.wait_for(websocket.receive_text(), timeout=1)
# todo: Check if this is session locked or updated globally
if msg.startswith("refresh_interval:"):
if refresh_interval := squire.dynamic_numbers(
msg.split(":")[1].strip()
Expand Down Expand Up @@ -249,17 +259,15 @@ async def websocket_endpoint(websocket: WebSocket, session_token: str = Cookie(N
await websocket.send_text("Session Expired")
await websocket.close()
break
if now - refresh_time > models.ws_settings.refresh_interval:
refresh_time = time.time()
LOGGER.debug("Fetching new charts")
data = squire.system_resources(models.ws_settings.cpu_interval)
# This can be gathered in the background
# but it will no longer be realtime data
# making it useless for long intervals
data = squire.system_resources(models.ws_settings.cpu_interval)
try:
await websocket.send_json(data)
await asyncio.sleep(
min(
models.ws_settings.refresh_interval, models.ws_settings.cpu_interval
)
)
# There is no point in gathering data, when it is not propagated
# So instead of repeat iterations, simply sleep until next refresh
await asyncio.sleep(models.ws_settings.refresh_interval)
except WebSocketDisconnect:
break
except KeyboardInterrupt:
Expand Down
50 changes: 32 additions & 18 deletions pyninja/monitor/templates/main.html
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@
<i>
<span id="span"></span><br>
Last status update: <span id="lastUpdate"></span><br>
Next update: <span id="nextUpdate"></span><br>
Generated by <a href="https://github.com/thevickypedia/PyNinja/releases/tag/v{{ version }}">PyNinja - v{{
version }}</a>
</i>
Expand All @@ -169,21 +170,27 @@
<h1>PyNinja - System Monitor</h1>
<div class="center-container">
<details>
<summary><strong>System Information</strong></summary>
<summary><strong>System Information (Basic)</strong></summary>
<br>
<strong>Node: </strong>{{ node }}<br>
<strong>Machine: </strong>{{ system }}<br>
<strong>Architecture: </strong>{{ machine }}<br>
{% if processor %}
<strong>Processor: </strong>{{ processor }}<br>
{% endif %}
<strong>CPU Cores: </strong>{{ cores }}<br>
<strong>RAM Capacity: </strong>{{ memory }}<br>
<strong>Disk Capacity: </strong>{{ storage }}<br>
{% if swap %}
<strong>Swap Capacity: </strong>{{ swap }}<br>
{% endif %}
<strong>Uptime: </strong>{{ uptime }}<br>
{% for key, value in sys_info_basic.items() %}
<strong>{{ key|capwords }}: </strong>{{ value }}<br>
{% endfor %}
</details>
<br>
<details>
<summary><strong>System Information (Memory & Storage)</strong></summary>
<br>
{% for key, value in sys_info_mem_storage.items() %}
<strong>{{ key|capwords }}: </strong>{{ value }}<br>
{% endfor %}
</details>
<br>
<details>
<summary><strong>System Information (Network)</strong></summary>
<br>
{% for key, value in sys_info_network.items() %}
<strong>{{ key|capwords }}: </strong>{{ value }}<br>
{% endfor %}
</details>
</div>
<div class="container">
Expand Down Expand Up @@ -214,7 +221,7 @@ <h3>Memory Usage</h3>
</div>
<p id="memoryUsageText">Memory: 0%</p>

{% if swap %}
{% if 'swap' in sys_info_mem_storage.keys() %}
<h3>Swap Usage</h3>
<div class="progress">
<div id="swapUsage" class="progress-bar"></div>
Expand All @@ -240,7 +247,7 @@ <h5 id="memoryTotal"></h5>
<div class="chart-container">
<canvas id="memoryChart"></canvas>
</div>
{% if swap %}
{% if 'swap' in sys_info_mem_storage.keys() %}
<h3>Swap Usage</h3>
<h5 id="swapTotal"></h5>
<div class="chart-container">
Expand All @@ -267,8 +274,6 @@ <h5 id="diskTotal"></h5>
let loadChartInstance = null;

ws.onmessage = function (event) {
const date = new Date();
document.getElementById('lastUpdate').innerText = date.toLocaleString();
let data;
try {
data = JSON.parse(event.data);
Expand All @@ -278,6 +283,15 @@ <h5 id="diskTotal"></h5>
logOut();
return;
}

// Floating info on top right of the page
const date = new Date();
document.getElementById('lastUpdate').innerText = date.toLocaleString();
// Convert seconds to milliseconds
const refreshInterval = data.refresh_interval * 1000;
const nextUpdateTime = new Date(date.getTime() + refreshInterval);
document.getElementById('nextUpdate').innerText = nextUpdateTime.toLocaleString();

// Update CPU usage
const cpuUsage = data.cpu_usage;
const cpuContainer = document.getElementById('cpuUsageContainer');
Expand Down
Loading

0 comments on commit 47652fe

Please sign in to comment.