Skip to content

Commit

Permalink
Include an option to get CPU load averages
Browse files Browse the repository at this point in the history
Add CPU load avg plugin to the monitoring page UI
  • Loading branch information
dormant-user committed Sep 10, 2024
1 parent dd330c3 commit 8e8f664
Show file tree
Hide file tree
Showing 9 changed files with 144 additions and 34 deletions.
2 changes: 2 additions & 0 deletions docs/genindex.html
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,8 @@ <h2 id="G">G</h2>
<li><a href="index.html#pyninja.dockerized.get_all_volumes">get_all_volumes() (in module pyninja.dockerized)</a>
</li>
<li><a href="index.html#pyninja.dockerized.get_container_status">get_container_status() (in module pyninja.dockerized)</a>
</li>
<li><a href="index.html#pyninja.routers.get_cpu_load_avg">get_cpu_load_avg() (in module pyninja.routers)</a>
</li>
<li><a href="index.html#pyninja.routers.get_cpu_utilization">get_cpu_utilization() (in module pyninja.routers)</a>
</li>
Expand Down
16 changes: 16 additions & 0 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,22 @@ <h1>Welcome to PyNinja’s documentation!<a class="headerlink" href="#welcome-to
</div></blockquote>
</dd></dl>

<dl class="py function">
<dt class="sig sig-object py" id="pyninja.routers.get_cpu_load_avg">
<em class="property"><span class="k"><span class="pre">async</span></span><span class="w"> </span></em><span class="sig-prename descclassname"><span class="pre">pyninja.routers.</span></span><span class="sig-name descname"><span class="pre">get_cpu_load_avg</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">request</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">Request</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">apikey</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">HTTPAuthorizationCredentials</span></span><span class="w"> </span><span class="o"><span class="pre">=</span></span><span class="w"> </span><span class="default_value"><span class="pre">Depends(HTTPBearer)</span></span></em><span class="sig-paren">)</span><a class="headerlink" href="#pyninja.routers.get_cpu_load_avg" title="Permalink to this definition"></a></dt>
<dd><p><strong>Get the number of processes in the system run queue averaged over the last 1, 5, and 15 minutes respectively.</strong></p>
<p><strong>Args:</strong></p>
<blockquote>
<div><p>request: Reference to the FastAPI request object.
apikey: API Key to authenticate the request.</p>
</div></blockquote>
<p><strong>Raises:</strong></p>
<blockquote>
<div><p>APIResponse:
Raises the HTTPStatus object with a status code and CPU usage as response.</p>
</div></blockquote>
</dd></dl>

<dl class="py function">
<dt class="sig sig-object py" id="pyninja.routers.get_disk_utilization">
<em class="property"><span class="k"><span class="pre">async</span></span><span class="w"> </span></em><span class="sig-prename descclassname"><span class="pre">pyninja.routers.</span></span><span class="sig-name descname"><span class="pre">get_disk_utilization</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">request</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">Request</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">apikey</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">HTTPAuthorizationCredentials</span></span><span class="w"> </span><span class="o"><span class="pre">=</span></span><span class="w"> </span><span class="default_value"><span class="pre">Depends(HTTPBearer)</span></span></em><span class="sig-paren">)</span><a class="headerlink" href="#pyninja.routers.get_disk_utilization" title="Permalink to this definition"></a></dt>
Expand Down
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.

16 changes: 12 additions & 4 deletions pyninja/monitor/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ async def websocket_endpoint(websocket: WebSocket, session_token: str = Cookie(N
session_token: Session token set after verifying username and password.
"""
await websocket.accept()
# Validate session before starting the websocket connection
try:
await monitor.authenticator.validate_session(
websocket.client.host, session_token
Expand All @@ -185,6 +186,9 @@ async def websocket_endpoint(websocket: WebSocket, session_token: str = Cookie(N
data = squire.system_resources(models.ws_settings.cpu_interval)
task = asyncio.create_task(asyncio.sleep(0.1))
while True:
# Validate session asynchronously (non-blocking)
# This way of handling session validation is more efficient than using a blocking call
# This might slip through one iteration even after the session has expired, but it just means one more iteration
try:
if task.done():
await task
Expand Down Expand Up @@ -264,8 +268,12 @@ async def websocket_endpoint(websocket: WebSocket, session_token: str = Cookie(N
break
try:
if task.done():
await task
except exceptions.SessionError as error:
await asyncio.wait_for(task, timeout=1)
else:
task.cancel()
except (
exceptions.SessionError,
asyncio.TimeoutError,
asyncio.CancelledError,
) as error:
LOGGER.warning(error)
await websocket.send_text(error.__str__())
await websocket.close()
72 changes: 71 additions & 1 deletion pyninja/monitor/templates/main.html
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,11 @@
right: 2%;
font-size: 12px;
}

.graph-canvas {
max-width: 600px;
}

</style>
</head>
<div class="corner">
Expand Down Expand Up @@ -182,6 +187,7 @@ <h1>PyNinja - System Monitor</h1>
</details>
</div>
<div class="container">
<!-- Box to display utilization per CPU -->
<div class="box">
<h3>CPU Usage</h3>
<div class="cpu-box" id="cpuUsageContainer">
Expand All @@ -200,25 +206,34 @@ <h3>Set Refresh Interval (seconds)</h3>
<button class="tooltip-button" title="Frequency to query system resources" id="setRefreshIntervalBtn">Update
</button>
</div>
<!-- Box to display Memory, Swap and Disk usage along with CPU load avg -->
<div class="box">
<h3>Memory Usage</h3>
<div class="progress">
<div id="memoryUsage" class="progress-bar"></div>
</div>
<p id="memoryUsageText">Memory: 0%</p>

{% if swap %}
<h3>Swap Usage</h3>
<div class="progress">
<div id="swapUsage" class="progress-bar"></div>
</div>
<p id="swapUsageText">Swap: 0%</p>
{% endif %}

<h3>Disk Usage</h3>
<div class="progress">
<div id="diskUsage" class="progress-bar"></div>
</div>
<p id="diskUsageText">Disk: 0%</p>

<div class="graph">
<h3>CPU Load Averages</h3>
<canvas class="graph-canvas" id="loadChart" width="400" height="200"></canvas>
</div>
</div>
<!-- Box to display Memory, Swap and Disk usage as Pie charts -->
<div class="box">
<h3>Memory Usage</h3>
<h5 id="memoryTotal"></h5>
Expand Down Expand Up @@ -249,6 +264,7 @@ <h5 id="diskTotal"></h5>
let memoryChartInstance = null;
let swapChartInstance = null;
let diskChartInstance = null;
let loadChartInstance = null;

ws.onmessage = function (event) {
const date = new Date();
Expand All @@ -259,7 +275,7 @@ <h5 id="diskTotal"></h5>
} catch (error) {
console.warn('Error parsing JSON data:', error);
alert(event.data);
window.location.href = `${window.location.origin}/logout`;
logOut();
return;
}
// Update CPU usage
Expand Down Expand Up @@ -301,6 +317,60 @@ <h5 id="diskTotal"></h5>
document.getElementById('diskUsageText').innerText = `Disk: ${diskUsage.toFixed(2)}%`;
updateProgressBar('diskUsage', diskUsage);

// CPU Load Avg Graph
const loadAverages = data.load_averages;
if (loadChartInstance) {
loadChartInstance.data.datasets[0].data = [loadAverages["1m"], loadAverages["5m"], loadAverages["15m"]];
loadChartInstance.update();
} else {
const ctx = document.getElementById('loadChart').getContext('2d');
loadChartInstance = new Chart(ctx, {
type: 'bar',
data: {
labels: ['1 minute', '5 minutes', '15 minutes'],
datasets: [{
label: 'Load Average',
data: [loadAverages["1m"], loadAverages["5m"], loadAverages["15m"]],
backgroundColor: [
'rgba(75, 192, 192, 0.2)',
'rgba(153, 102, 255, 0.2)',
'rgba(255, 159, 64, 0.2)'
],
borderColor: [
'rgba(75, 192, 192, 1)',
'rgba(153, 102, 255, 1)',
'rgba(255, 159, 64, 1)'
],
borderWidth: 1
}]
},
options: {
plugins: {
// Hide the legend
legend: {
display: false
}
},
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: 'Number of Processes'
},
ticks: {
// Set integer step size
stepSize: 1,
callback: function (value) {
return Number.isInteger(value) ? value : '';
}
}
}
}
}
});
}

// Memory Chart
document.getElementById("memoryTotal").innerText = `Total: ${formatBytes(memoryInfo.total)}`;
if (memoryChartInstance) {
Expand Down
30 changes: 30 additions & 0 deletions pyninja/routers.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,30 @@ async def get_memory_utilization(
)


async def get_cpu_load_avg(
request: Request,
apikey: HTTPAuthorizationCredentials = Depends(BEARER_AUTH),
):
"""**Get the number of processes in the system run queue averaged over the last 1, 5, and 15 minutes respectively.**
**Args:**
request: Reference to the FastAPI request object.
apikey: API Key to authenticate the request.
**Raises:**
APIResponse:
Raises the HTTPStatus object with a status code and CPU usage as response.
"""
await auth.level_1(request, apikey)
m1, m5, m15 = psutil.getloadavg() or (None, None, None)
raise exceptions.APIResponse(
status_code=HTTPStatus.OK.real,
detail={"1m": m1, "5m": m5, "15m": m15},
)


async def get_disk_utilization(
request: Request,
apikey: HTTPAuthorizationCredentials = Depends(BEARER_AUTH),
Expand Down Expand Up @@ -402,6 +426,12 @@ def get_all_routes(dependencies: List[Depends]) -> List[APIRoute]:
methods=["GET"],
dependencies=dependencies,
),
APIRoute(
path="/get-cpu-load",
endpoint=get_cpu_load_avg,
methods=["GET"],
dependencies=dependencies,
),
APIRoute(
path="/get-processor",
endpoint=get_processor_name,
Expand Down
2 changes: 2 additions & 0 deletions pyninja/squire.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,13 @@ def system_resources(cpu_interval: int) -> Dict[str, dict]:
Dict[str, dict]:
Returns a nested dictionary.
"""
m1, m5, m15 = os.getloadavg() or (None, None, None)
return dict(
cpu_usage=psutil.cpu_percent(interval=cpu_interval, percpu=True),
memory_info=psutil.virtual_memory()._asdict(),
swap_info=psutil.swap_memory()._asdict(),
disk_info=shutil.disk_usage("/")._asdict(),
load_averages={"1m": m1, "5m": m5, "15m": m15},
)


Expand Down
38 changes: 10 additions & 28 deletions release_notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,58 +3,40 @@ Release Notes

v0.0.7 (09/09/2024)
-------------------
- Includes a new feature to monitor disk utilization and get process name
- Bug fix on uncaught errors during server shutdown
- **Full Changelog**: https://github.com/thevickypedia/PyNinja/compare/v0.0.6...v0.0.7
- Release `v0.0.7`

v0.0.6 (09/09/2024)
-------------------
- Includes an option to limit maximum number of WebSocket sessions
- Includes a logout functionality for the monitoring page
- Uses bearer auth for the monitoring page
- Redefines progress bars with newer color schemes
- **Full Changelog**: https://github.com/thevickypedia/PyNinja/compare/v0.0.5...v0.0.6
- Release `v0.0.6`

v0.0.6a (09/07/2024)
--------------------
- Includes an option to limit max number of concurrent sessions for monitoring page
- **Full Changelog**: https://github.com/thevickypedia/PyNinja/compare/v0.0.5...v0.0.6a
- Release `v0.0.6a`

v0.0.5 (09/07/2024)
-------------------
- Packs an entirely new UI and authentication mechanism for monitoring tool
- Includes speed, stability and security improvements for monitoring feature
- Adds night mode option for monitoring UI
- **Full Changelog**: https://github.com/thevickypedia/PyNinja/compare/v0.0.4...v0.0.5
- Release `v0.0.5`

v0.0.4 (09/06/2024)
-------------------
- Includes an option to monitor system resources via `WebSockets`
- **Full Changelog**: https://github.com/thevickypedia/PyNinja/compare/v0.0.3...v0.0.4
- Include an option to monitor system resources via websockets

v0.0.3 (08/16/2024)
-------------------
- Allows env vars to be sourced from both ``env_file`` and ``kwargs``
- **Full Changelog**: https://github.com/thevickypedia/PyNinja/compare/v0.0.2...v0.0.3
- Release `v0.0.3`

v0.0.2 (08/16/2024)
-------------------
- Includes added support for custom log configuration
- **Full Changelog**: https://github.com/thevickypedia/PyNinja/compare/v0.0.1...v0.0.2
- Release `v0.0.2`

v0.0.1 (08/11/2024)
-------------------
- Includes a process monitor and remote command execution functionality
- Security improvements including brute force protection and rate limiting
- Accepts ``JSON`` and ``YAML`` files for env config
- Supports custom worker count for ``uvicorn`` server
- Allows custom logging using ``logging.ini``
- Includes an option to set the ``apikey`` via commandline
- **Full Changelog**: https://github.com/thevickypedia/PyNinja/compare/v0.0.0...v0.0.1
- Release `v0.0.1`

v0.0.0 (08/11/2024)
-------------------
- Release first stable version
- Implement concurrency for validating process health
- Update logger names across the module and README.md

0.0.0-a (08/10/2024)
--------------------
Expand Down

0 comments on commit 8e8f664

Please sign in to comment.