Add detailed CPU usage stats (#5136)

* Add detailed CPU stats Admin API endpoint

* Add detailed CPU stats endpoint route

* Add Network & Memory stats and Vue admin dashboard

* CPU Stats Polish Pass

* Fix bottom margin of CPU & Memory/Disk Space cols

* Improve admin menu usability on mobile

* Update language on CPU help modal.

Co-authored-by: Buster "Silver Eagle" Neece <buster@busterneece.com>
This commit is contained in:
Vaalyn 2022-02-25 06:16:43 +01:00 committed by GitHub
parent aa9ecbbe15
commit c20bc4fd11
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 1366 additions and 537 deletions

8
composer.lock generated
View File

@ -9520,12 +9520,12 @@
"source": {
"type": "git",
"url": "https://github.com/zircote/swagger-php.git",
"reference": "bd5699cc1fcb5a68fdfca388703a6076e44ac920"
"reference": "902be534909324ebd72b90d1cff9d695dc886379"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/zircote/swagger-php/zipball/bd5699cc1fcb5a68fdfca388703a6076e44ac920",
"reference": "bd5699cc1fcb5a68fdfca388703a6076e44ac920",
"url": "https://api.github.com/repos/zircote/swagger-php/zipball/902be534909324ebd72b90d1cff9d695dc886379",
"reference": "902be534909324ebd72b90d1cff9d695dc886379",
"shasum": ""
},
"require": {
@ -9588,7 +9588,7 @@
"issues": "https://github.com/zircote/swagger-php/issues",
"source": "https://github.com/zircote/swagger-php/tree/master"
},
"time": "2022-01-20T09:06:32+00:00"
"time": "2022-02-21T20:52:03+00:00"
}
],
"packages-dev": [

View File

@ -55,6 +55,9 @@ return static function (RouteCollectorProxy $group) {
}
)->add(new Middleware\Permissions(GlobalPermissions::Backups));
$group->get('/server/stats', Controller\Api\Admin\ServerStatsController::class)
->setName('api:admin:server:stats');
$group->get('/permissions', Controller\Api\Admin\PermissionsController::class)
->add(new Middleware\Permissions(GlobalPermissions::All));

View File

@ -0,0 +1,359 @@
<template>
<div>
<h2 class="outside-card-header mb-1">
<translate key="lang_hdr_admin">Administration</translate>
</h2>
<b-row>
<b-col v-for="(panel, key) in adminPanels" :key="key" sm="12" lg="4" class="mb-4">
<b-card no-body>
<b-card-header header-bg-variant="primary-dark">
<h2 class="card-title flex-fill">
<icon :icon="panel.icon"></icon>
{{ panel.label }}
</h2>
</b-card-header>
<b-list-group>
<b-list-group-item v-for="(item, key) in panel.items" :key="key" :href="item.url">{{ item.label }}</b-list-group-item>
</b-list-group>
</b-card>
</b-col>
</b-row>
<h2 class="outside-card-header mb-1">
<translate key="lang_hdr_server_status">Server Status</translate>
</h2>
<b-row>
<b-col sm="12" lg="8" xl="6" class="mb-4">
<b-card no-body>
<b-card-header header-bg-variant="primary-dark" class="d-flex align-items-center">
<div class="flex-fill">
<h2 class="card-title">
<translate key="lang_hdr_cpu_load">CPU Load</translate>
</h2>
</div>
<div class="flex-shrink-0">
<b-button variant="outline-light" size="sm" class="py-2" @click.prevent="showCpuStatsHelpModal">
<icon icon="help_outline"></icon>
</b-button>
</div>
</b-card-header>
<b-card-body>
<h5 class="mb-1 text-center">{{ formatCpuName(stats.cpu.total.name) }}</h5>
<b-progress max="100" :label="formatPercentageString(stats.cpu.total.usage)" class="h-20 mb-3 mt-2">
<b-progress-bar variant="danger" :value="stats.cpu.total.steal"></b-progress-bar>
<b-progress-bar variant="warning" :value="stats.cpu.total.io_wait"></b-progress-bar>
<b-progress-bar variant="primary" :value="stats.cpu.total.usage"></b-progress-bar>
</b-progress>
<b-row>
<b-col>
<b-badge pill variant="danger">&nbsp;&nbsp;</b-badge>&nbsp;
<translate key="lang_cpu_steal">Steal</translate>
: {{ stats.cpu.total.steal }}%
</b-col>
<b-col>
<b-badge pill variant="warning">&nbsp;&nbsp;</b-badge>&nbsp;
<translate key="lang_cpu_wait">Wait</translate>
: {{ stats.cpu.total.io_wait }}%
</b-col>
<b-col>
<b-badge pill variant="primary">&nbsp;&nbsp;</b-badge>&nbsp;
<translate key="lang_cpu_use">Use</translate>
: {{ stats.cpu.total.usage }}%
</b-col>
</b-row>
<hr>
<b-row>
<b-col v-for="core in stats.cpu.cores" :key="core.name" lg="6">
<h6 class="mb-1 text-center">{{ formatCpuName(core.name) }}</h6>
<b-progress max="100" :label="formatPercentageString(core.usage)" class="h-20">
<b-progress-bar variant="danger" :value="core.steal"></b-progress-bar>
<b-progress-bar variant="warning" :value="core.io_wait"></b-progress-bar>
<b-progress-bar variant="primary" :value="core.usage"></b-progress-bar>
</b-progress>
<b-row no-gutters class="mb-2 mt-1">
<b-col>
St: {{ core.steal }}%
</b-col>
<b-col>
Wa: {{ core.io_wait }}%
</b-col>
<b-col>
Us: {{ core.usage }}%
</b-col>
</b-row>
</b-col>
</b-row>
</b-card-body>
<b-card-footer>
<h6 class="mb-1 text-center">
<translate key="lang_cpu_average">Load Average</translate>
</h6>
<b-row class="text-center" no-gutters>
<b-col>
<h6>1-Min</h6>
{{ stats.cpu.load[0] }}
</b-col>
<b-col>
<h6>5-Min</h6>
{{ stats.cpu.load[1] }}
</b-col>
<b-col>
<h6>15-Min</h6>
{{ stats.cpu.load[2] }}
</b-col>
</b-row>
</b-card-footer>
</b-card>
</b-col>
<b-col sm="12" lg="4" xl="6" class="mb-4">
<b-row class="mb-4">
<b-col>
<b-card no-body>
<b-card-header header-bg-variant="primary-dark" class="d-flex align-items-center">
<div class="flex-fill">
<h2 class="card-title">
<translate key="lang_hdr_memory">Memory</translate>
</h2>
</div>
<div class="flex-shrink-0">
<b-button variant="outline-light" size="sm" class="py-2" @click.prevent="showMemoryStatsHelpModal">
<icon icon="help_outline"></icon>
</b-button>
</div>
</b-card-header>
<b-card-body>
<h6 class="mb-1 text-center">
<translate key="lang_disk_header">Total RAM</translate>
:
{{ stats.memory.readable.total }}
</h6>
<b-progress :max="stats.memory.bytes.total" :label="stats.memory.readable.used" class="h-20 mb-3 mt-2">
<b-progress-bar variant="primary" :value="stats.memory.bytes.used"></b-progress-bar>
<b-progress-bar variant="warning" :value="stats.memory.bytes.cached"></b-progress-bar>
</b-progress>
<b-row>
<b-col>
<b-badge pill variant="primary">&nbsp;&nbsp;</b-badge>&nbsp;
<translate key="lang_memory_used">Used</translate>
: {{ stats.memory.readable.used }}
</b-col>
<b-col>
<b-badge pill variant="warning">&nbsp;&nbsp;</b-badge>&nbsp;
<translate key="lang_memory_cached">Cached</translate>
: {{ stats.memory.readable.cached }}
</b-col>
</b-row>
</b-card-body>
</b-card>
</b-col>
</b-row>
<b-row>
<b-col>
<b-card no-body>
<b-card-header header-bg-variant="primary-dark">
<h2 class="card-title">
<translate key="lang_hdr_disk_space">Disk Space</translate>
</h2>
</b-card-header>
<b-card-body>
<h6 class="mb-1 text-center">
<translate key="lang_total_disk_space">Total Disk Space</translate>
:
{{ stats.disk.readable.total }}
</h6>
<b-progress :max="stats.disk.bytes.total" :label="stats.disk.readable.used" class="h-20 mb-3 mt-2">
<b-progress-bar variant="primary" :value="stats.disk.bytes.used"></b-progress-bar>
</b-progress>
<b-row>
<b-col>
<b-badge pill variant="primary">&nbsp;&nbsp;</b-badge>&nbsp;
<translate key="lang_used_disk_space">Used</translate>
:
{{ stats.disk.readable.used }}
</b-col>
</b-row>
</b-card-body>
</b-card>
</b-col>
</b-row>
</b-col>
</b-row>
<b-row>
<b-col>
<b-card no-body>
<b-card-header header-bg-variant="primary-dark">
<h2 class="card-title">
<translate key="lang_hdr_network_interfaces">Network Interfaces</translate>
</h2>
</b-card-header>
<b-tabs content-class="mt-3">
<b-tab v-for="netInterface in stats.network" :key="netInterface.interface_name" :title="netInterface.interface_name" pills card>
<b-row class="mb-3">
<b-col class="mb-3">
<h5 class="mb-1 text-center">
<translate key="lang_net_received">Received</translate>
</h5>
<b-table striped responsive
:items="getNetworkInterfaceTableItems(netInterface.received)"
:fields="getNetworkInterfaceTableFields(netInterface.received)">
</b-table>
</b-col>
<b-col>
<h5 class="mb-1 text-center">
<translate key="lang_net_transmitted">Transmitted</translate>
</h5>
<b-table striped responsive
:items="getNetworkInterfaceTableItems(netInterface.transmitted)"
:fields="getNetworkInterfaceTableFields(netInterface.transmitted)">
</b-table>
</b-col>
</b-row>
</b-tab>
</b-tabs>
</b-card>
</b-col>
</b-row>
<cpu-stats-help-modal ref="cpuStatsHelpModal"></cpu-stats-help-modal>
<memory-stats-help-modal ref="memoryStatsHelpModal"></memory-stats-help-modal>
</div>
</template>
<script>
import Icon from '~/components/Common/Icon';
import InfoCard from '~/components/Common/InfoCard';
import CpuStatsHelpModal from "./Index/CpuStatsHelpModal";
import MemoryStatsHelpModal from "./Index/MemoryStatsHelpModal";
import _ from 'lodash';
export default {
name: 'AdminIndex',
components: {InfoCard, CpuStatsHelpModal, MemoryStatsHelpModal, Icon},
props: {
adminPanels: Array,
statsUrl: String,
},
data() {
return {
stats: {
cpu: {
total: {
name: 'Total',
steal: 0,
io_wait: 0,
usage: 0
},
cores: [],
load: [
0,
0,
0
]
},
memory: {
bytes: {
total: 0,
used: 0,
cached: 0
},
readable: {
total: '',
used: '',
cached: ''
}
},
disk: {
bytes: {
total: 0,
used: 0
},
readable: {
total: '',
used: ''
}
},
network: []
}
};
},
created () {
this.updateStats();
},
methods: {
formatCpuName(cpuName) {
return _.upperFirst(cpuName);
},
formatPercentageString(value) {
return value + '%';
},
getNetworkInterfaceTableFields(interfaceData) {
let fields = [];
Object.keys(interfaceData).forEach((key) => {
fields.push({
key: key,
sortable: false
});
});
return fields;
},
getNetworkInterfaceTableItems(interfaceData) {
let item = {};
Object.entries(interfaceData).forEach((data) => {
let key = data[0];
let value = data[1];
if (_.isObject(value)) {
value = value.readable + '/s';
}
item[key] = value;
});
return [item];
},
updateStats () {
this.axios.get(this.statsUrl).then((response) => {
this.stats = response.data;
setTimeout(this.updateStats, (!document.hidden) ? 1000 : 5000);
}).catch((error) => {
if (!error.response || error.response.data.code !== 403) {
setTimeout(this.updateStats, (!document.hidden) ? 5000 : 10000);
}
});
},
showCpuStatsHelpModal() {
this.$refs.cpuStatsHelpModal.create();
},
showMemoryStatsHelpModal() {
this.$refs.memoryStatsHelpModal.create();
},
}
};
</script>

View File

@ -0,0 +1,79 @@
<template>
<b-modal size="lg" centered id="cpu_stats_help_modal" ref="modal" :title="langTitle">
<div class="mb-2">
<h6>
<b-badge pill variant="danger">&nbsp;&nbsp;</b-badge>&nbsp;
<translate key="lang_steal">Steal (St)</translate>
:
<translate
key="lang_steal_1">Time stolen by other virtual machines on the same physical server.</translate>
</h6>
<div class="ml-4">
<p>
<translate key="lang_steal_2">Most hosting providers will put more Virtual Machines (VPSes) on a server than the hardware can handle when each VM is running at full CPU load. This is called over-provisioning, which can lead to other VMs on the server "stealing" CPU time from your VM and vice-versa.</translate>
</p>
<p>
<translate key="lang_steal_3">To alleviate this potential problem with shared CPU resources, hosts assign "credits" to a VPS which are used up according to an algorithm based on the CPU load as well as the time over which the CPU load is generated. If your VM's assigned credit is used up, they will take CPU time from your VM and assign it to other VMs on the machine. This is seen as the "Steal" or "St" value.</translate>
</p>
<p>
<translate key="lang_steal_4">Audio transcoding applications like Liquidsoap use a consistent amount of CPU over time, which gradually drains this available credit. If you regularly see stolen CPU time, you should consider migrating to a VM that has CPU resources dedicated to your instance.</translate>
</p>
</div>
</div>
<div class="mb-2">
<h6>
<b-badge pill variant="warning">&nbsp;&nbsp;</b-badge>&nbsp;
<translate key="lang_wait">Wait (Wa)</translate>
:
<translate key="lang_wait_1">Time spent waiting for disk I/O to be completed.</translate>
</h6>
<div class="ml-4">
<p>
<translate key="lang_wait_2">The I/O Wait is the percentage of time that the CPU is waiting for disk access before it can continue the work that depends on the result of this.</translate>
</p>
<p>
<translate key="lang_wait_3">High I/O Wait can indicate a bottleneck with the server's hard disk, a potentially failing hard disk, or heavy load on the hard disk.</translate>
</p>
<p>
<translate key="lang_wait_4">One important note on I/O Wait is that it can indicate a bottleneck or problem but also may be completely meaningless, depending on the workload and general available resources. A constantly high I/O Wait should prompt further investigation with more sophisticated tools.</translate>
</p>
</div>
</div>
<div class="mb-1">
<h6>
<b-badge pill variant="primary">&nbsp;&nbsp;</b-badge>&nbsp;
<translate key="lang_use">Use (Us)</translate>
:
<translate key="lang_use_1">The current CPU usage including I/O Wait and Steal.</translate>
</h6>
</div>
<template #modal-footer>
<slot name="modal-footer">
<b-button variant="default" type="button" @click="close">
<translate key="lang_btn_close">Close</translate>
</b-button>
</slot>
</template>
</b-modal>
</template>
<script>
export default {
name: 'CpuStatsHelpModal',
computed: {
langTitle() {
return this.$gettext('CPU Stats Help');
}
},
methods: {
create() {
this.$refs.modal.show();
},
close() {
this.$refs.modal.hide();
}
}
};
</script>

View File

@ -0,0 +1,58 @@
<template>
<b-modal size="lg" centered id="cpu_stats_help_modal" ref="modal" :title="langTitle">
<div class="mb-2">
<h6>
<b-badge pill variant="danger">&nbsp;&nbsp;</b-badge>&nbsp;
<translate key="lang_cached">Cached</translate>
:
<translate
key="lang_cached_1">The amount of memory Linux is using for disk caching.</translate>
</h6>
<div class="ml-4">
<p>
<translate key="lang_cached_2">This can make it look like your memory is low while it actually is not. Some monitoring solutions/panels include cached memory in their used memory statistics without indicating this.</translate>
</p>
<p>
<translate key="lang_cached_3">Disk caching makes a system much faster and more responsive in general. It does not take memory away from applications in any way since it will automatically be released by the operating system when needed.</translate>
</p>
</div>
</div>
<div class="mb-2">
<h6>
<b-badge pill variant="primary">&nbsp;&nbsp;</b-badge>&nbsp;
<translate key="lang_used">Used</translate>
:
<translate key="lang_used_1">The current Memory usage excluding cached memory.</translate>
</h6>
</div>
<template #modal-footer>
<slot name="modal-footer">
<b-button variant="default" type="button" @click="close">
<translate key="lang_btn_close">Close</translate>
</b-button>
</slot>
</template>
</b-modal>
</template>
<script>
export default {
name: 'MemoryStatsHelpModal',
computed: {
langTitle() {
return this.$gettext('Memory Stats Help');
}
},
methods: {
create() {
this.$refs.modal.show();
},
close() {
this.$refs.modal.hide();
}
}
};
</script>

View File

@ -0,0 +1,8 @@
import initBase from '~/base.js';
import '~/vendor/bootstrapVue.js';
import '~/vendor/sweetalert.js';
import AdminIndex from '~/components/Admin/Index.vue';
export default initBase(AdminIndex);

View File

@ -14,6 +14,7 @@ module.exports = {
AdminBranding: '~/pages/Admin/Branding.js',
AdminCustomFields: '~/pages/Admin/CustomFields.js',
AdminGeoLite: '~/pages/Admin/GeoLite.js',
AdminIndex: '~/pages/Admin/Index.js',
AdminPermissions: '~/pages/Admin/Permissions.js',
AdminSettings: '~/pages/Admin/Settings.js',
AdminShoutcast: '~/pages/Admin/Shoutcast.js',

View File

@ -1,410 +1,10 @@
parameters:
ignoreErrors:
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Delete constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Admin/CustomFieldsController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Get constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 2
path: src/Controller/Api/Admin/CustomFieldsController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Post constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Admin/CustomFieldsController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Put constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Admin/CustomFieldsController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Get constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Admin/PermissionsController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Delete constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Admin/RolesController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Get constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 2
path: src/Controller/Api/Admin/RolesController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Post constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Admin/RolesController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Put constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Admin/RolesController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Get constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Admin/SettingsController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Put constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Admin/SettingsController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Delete constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Admin/StationsController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Get constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 2
path: src/Controller/Api/Admin/StationsController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Post constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Admin/StationsController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Put constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Admin/StationsController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Delete constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Admin/StorageLocationsController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Get constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 2
path: src/Controller/Api/Admin/StorageLocationsController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Post constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Admin/StorageLocationsController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Put constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Admin/StorageLocationsController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Delete constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Admin/UsersController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Get constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 2
path: src/Controller/Api/Admin/UsersController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Post constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Admin/UsersController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Put constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Admin/UsersController.php
-
message: "#^Parameter \\#2 \\$record of method App\\\\Controller\\\\Api\\\\AbstractApiCrudController\\<TEntity of App\\\\Entity\\\\StationPlaylist\\|App\\\\Entity\\\\StationStreamer\\>\\:\\:fromArray\\(\\) expects \\(TEntity of App\\\\Entity\\\\StationPlaylist\\|App\\\\Entity\\\\StationStreamer\\)\\|null, \\(TEntity of App\\\\Entity\\\\StationPlaylist\\|App\\\\Entity\\\\StationStreamer\\)\\|null given\\.$#"
count: 1
path: src/Controller/Api/Stations/AbstractScheduledEntityController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Delete constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Stations/Art/DeleteArtAction.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Post constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Stations/Art/PostArtAction.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Put constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Stations/Automation/RunAction.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Delete constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Stations/FilesController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Get constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 2
path: src/Controller/Api/Stations/FilesController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Post constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Stations/FilesController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Put constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Stations/FilesController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Get constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Stations/HistoryController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Get constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Stations/ListenersAction.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Delete constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Stations/Mounts/Intro/DeleteIntroAction.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Get constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Stations/Mounts/Intro/GetIntroAction.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Post constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Stations/Mounts/Intro/PostIntroAction.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Delete constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Stations/MountsController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Get constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 2
path: src/Controller/Api/Stations/MountsController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Post constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Stations/MountsController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Put constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Stations/MountsController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Delete constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Stations/PlaylistsController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Get constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 2
path: src/Controller/Api/Stations/PlaylistsController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Post constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Stations/PlaylistsController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Put constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Stations/PlaylistsController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Delete constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Stations/PodcastEpisodesController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Get constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 2
path: src/Controller/Api/Stations/PodcastEpisodesController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Post constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Stations/PodcastEpisodesController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Put constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Stations/PodcastEpisodesController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Delete constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Stations/Podcasts/Art/DeleteArtAction.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Get constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Stations/Podcasts/Art/GetArtAction.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Post constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Stations/Podcasts/Art/PostArtAction.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Delete constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Stations/Podcasts/Episodes/Art/DeleteArtAction.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Get constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Stations/Podcasts/Episodes/Art/GetArtAction.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Post constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Stations/Podcasts/Episodes/Art/PostArtAction.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Delete constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Stations/Podcasts/Episodes/Media/DeleteMediaAction.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Get constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Stations/Podcasts/Episodes/Media/GetMediaAction.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Post constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Stations/Podcasts/Episodes/Media/PostMediaAction.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Delete constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Stations/PodcastsController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Get constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 2
path: src/Controller/Api/Stations/PodcastsController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Post constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Stations/PodcastsController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Put constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Stations/PodcastsController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Delete constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Stations/QueueController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Get constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 2
path: src/Controller/Api/Stations/QueueController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Delete constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Stations/RemotesController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Get constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 2
path: src/Controller/Api/Stations/RemotesController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Post constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Stations/RemotesController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Put constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Stations/RemotesController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Get constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Stations/ServicesController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Post constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 3
path: src/Controller/Api/Stations/ServicesController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Delete constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Stations/SftpUsersController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Get constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 2
path: src/Controller/Api/Stations/SftpUsersController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Post constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Stations/SftpUsersController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Put constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Stations/SftpUsersController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Delete constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Stations/StreamersController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Get constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 2
path: src/Controller/Api/Stations/StreamersController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Post constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Stations/StreamersController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Put constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Stations/StreamersController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Delete constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Stations/WebhooksController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Get constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 2
path: src/Controller/Api/Stations/WebhooksController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Post constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Stations/WebhooksController.php
-
message: "#^Parameter \\$security of attribute class OpenApi\\\\Attributes\\\\Put constructor expects array\\<string\\>\\|null, array\\<int, array\\<string, array\\>\\> given\\.$#"
count: 1
path: src/Controller/Api/Stations/WebhooksController.php
-
message: "#^Parameter \\#3 \\$criteria of method Doctrine\\\\DBAL\\\\Connection\\:\\:update\\(\\) expects array\\<string, mixed\\>, array\\<int, int\\> given\\.$#"
count: 1

View File

@ -4,64 +4,33 @@ declare(strict_types=1);
namespace App\Controller\Admin;
use App\Environment;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Radio\Quota;
use Brick\Math\BigInteger;
use Psr\Http\Message\ResponseInterface;
class IndexAction
{
public function __invoke(
ServerRequest $request,
Response $response,
Environment $environment
Response $response
): ResponseInterface {
$router = $request->getRouter();
$view = $request->getView();
// Remove the sidebar on the homepage.
$view->addData(['sidebar' => null]);
$stationsBaseDir = $environment->getStationDirectory();
$view = $request->getView();
$viewData = $view->getData();
$spaceTotalFloat = disk_total_space($stationsBaseDir);
$spaceTotal = (is_float($spaceTotalFloat))
? BigInteger::of($spaceTotalFloat)
: BigInteger::zero();
$spaceFreeFloat = disk_free_space($stationsBaseDir);
$spaceFree = (is_float($spaceFreeFloat))
? BigInteger::of($spaceFreeFloat)
: BigInteger::zero();
$spaceUsed = $spaceTotal->minus($spaceFree);
// Get memory info.
$meminfoRaw = file("/proc/meminfo", FILE_IGNORE_NEW_LINES) ?: [];
$meminfo = [];
foreach ($meminfoRaw as $line) {
if (str_contains($line, ':')) {
[$key, $val] = explode(":", $line);
$meminfo[$key] = trim($val);
}
}
$memoryTotal = Quota::convertFromReadableSize($meminfo['MemTotal']) ?? BigInteger::zero();
$memoryFree = Quota::convertFromReadableSize($meminfo['MemAvailable']) ?? BigInteger::zero();
$memoryUsed = $memoryTotal->minus($memoryFree);
return $view->renderToResponse(
$response,
'admin/index/index',
[
'load' => sys_getloadavg(),
'space_percent' => Quota::getPercentage($spaceUsed, $spaceTotal),
'space_used' => Quota::getReadableSize($spaceUsed),
'space_total' => Quota::getReadableSize($spaceTotal),
'memory_percent' => Quota::getPercentage($memoryUsed, $memoryTotal),
'memory_used' => Quota::getReadableSize($memoryUsed),
'memory_total' => Quota::getReadableSize($memoryTotal),
return $view->renderVuePage(
response: $response,
component: 'Vue_AdminIndex',
id: 'admin-index',
title: __('Administration'),
props: [
'adminPanels' => $viewData['admin_panels'] ?? [],
'statsUrl' => (string)$router->named('api:admin:server:stats'),
]
);
}

View File

@ -0,0 +1,189 @@
<?php
declare(strict_types=1);
namespace App\Controller\Api\Admin;
use App\Environment;
use App\Http\Response;
use App\Http\ServerRequest;
use App\OpenApi;
use App\Radio\Quota;
use App\Service\CpuStats;
use App\Service\MemoryStats;
use App\Service\NetworkStats;
use Brick\Math\BigInteger;
use Brick\Math\RoundingMode;
use OpenApi\Attributes as OA;
use Psr\Http\Message\ResponseInterface;
#[
OA\Get(
path: '/admin/cpu/stats',
operationId: 'getCpuStats',
description: 'Return a list of all CPU usage stats.',
security: OpenApi::API_KEY_SECURITY,
tags: ['Administration: CPU stats'],
responses: [
new OA\Response(
response: 200,
description: 'Success' // TODO: Response Body
),
new OA\Response(ref: OpenApi::REF_RESPONSE_ACCESS_DENIED, response: 403),
new OA\Response(ref: OpenApi::REF_RESPONSE_NOT_FOUND, response: 404),
new OA\Response(ref: OpenApi::REF_RESPONSE_GENERIC_ERROR, response: 500),
]
)
]
class ServerStatsController
{
public function __invoke(
ServerRequest $request,
Response $response,
Environment $environment
): ResponseInterface {
$firstCpuMeasurement = CpuStats::getCurrentLoad();
$firstNetworkMeasurement = NetworkStats::getNetworkUsage();
$measurementTime = 1;
sleep($measurementTime);
$secondCpuMeasurement = CpuStats::getCurrentLoad();
$secondNetworkMeasurement = NetworkStats::getNetworkUsage();
$cpuTotal = [];
$statsPerCore = [];
foreach ($secondCpuMeasurement as $index => $currentCoreData) {
$previousCpuData = $firstCpuMeasurement[$index];
$deltaCpuData = CpuStats::calculateDelta($currentCoreData, $previousCpuData);
$cpuStats = [
'name' => $deltaCpuData->name,
'usage' => CpuStats::getUsage($deltaCpuData),
'idle' => CpuStats::getIdle($deltaCpuData),
'io_wait' => CpuStats::getIoWait($deltaCpuData),
'steal' => CpuStats::getSteal($deltaCpuData),
];
if ($deltaCpuData->name === 'total') {
$cpuTotal = $cpuStats;
} else {
$statsPerCore[] = $cpuStats;
}
}
$networkInterfaces = [];
foreach ($secondNetworkMeasurement as $index => $currentNetworkMeasurement) {
$previousNetworkMeasurement = $firstNetworkMeasurement[$index];
$deltaNetworkData = NetworkStats::calculateDelta(
$currentNetworkMeasurement,
$previousNetworkMeasurement
);
$bytesPerTimeReceived = $deltaNetworkData->received->bytes
->dividedBy($measurementTime, RoundingMode::HALF_UP)
->toBigInteger();
$bytesPerTimeTransmitted = $deltaNetworkData->transmitted->bytes
->dividedBy($measurementTime, RoundingMode::HALF_UP)
->toBigInteger();
$networkInterfaceStats = [
'interface_name' => $deltaNetworkData->interfaceName,
'received' => [
'speed' => [
'bytes' => $bytesPerTimeReceived,
'readable' => Quota::getReadableSize($bytesPerTimeReceived),
],
'packets' => $deltaNetworkData->received->packets,
'errs' => $deltaNetworkData->received->errs,
'drop' => $deltaNetworkData->received->drop,
'fifo' => $deltaNetworkData->received->fifo,
'frame' => $deltaNetworkData->received->frame,
'compressed' => $deltaNetworkData->received->compressed,
'multicast' => $deltaNetworkData->received->multicast,
],
'transmitted' => [
'speed' => [
'bytes' => $bytesPerTimeTransmitted,
'readable' => Quota::getReadableSize($bytesPerTimeTransmitted),
],
'packets' => $deltaNetworkData->transmitted->packets,
'errs' => $deltaNetworkData->transmitted->errs,
'drop' => $deltaNetworkData->transmitted->drop,
'fifo' => $deltaNetworkData->transmitted->fifo,
'frame' => $deltaNetworkData->transmitted->colls,
'carrier' => $deltaNetworkData->transmitted->carrier,
'compressed' => $deltaNetworkData->transmitted->compressed,
],
];
$networkInterfaces[] = $networkInterfaceStats;
}
$memoryStats = MemoryStats::getMemoryUsage();
$spaceTotalFloat = disk_total_space($environment->getStationDirectory());
$spaceTotal = (is_float($spaceTotalFloat))
? BigInteger::of($spaceTotalFloat)
: BigInteger::zero();
$spaceFreeFloat = disk_free_space($environment->getStationDirectory());
$spaceFree = (is_float($spaceFreeFloat))
? BigInteger::of($spaceFreeFloat)
: BigInteger::zero();
$spaceUsed = $spaceTotal->minus($spaceFree);
$stats = [
'cpu' => [
'total' => $cpuTotal,
'cores' => $statsPerCore,
'load' => sys_getloadavg(),
],
'memory' => [
'bytes' => [
'total' => $memoryStats->memTotal,
'free' => $memoryStats->memFree,
'cached' => $memoryStats->cached,
'used' => $memoryStats->getUsedMemory(),
],
'readable' => [
'total' => Quota::getReadableSize($memoryStats->memTotal),
'free' => Quota::getReadableSize($memoryStats->memFree),
'cached' => Quota::getReadableSize($memoryStats->cached),
'used' => Quota::getReadableSize($memoryStats->getUsedMemory()),
],
],
'swap' => [
'bytes' => [
'total' => $memoryStats->swapTotal,
'free' => $memoryStats->swapFree,
'used' => $memoryStats->getUsedSwap(),
],
'readable' => [
'total' => Quota::getReadableSize($memoryStats->swapTotal),
'free' => Quota::getReadableSize($memoryStats->swapFree),
'used' => Quota::getReadableSize($memoryStats->getUsedSwap()),
],
],
'disk' => [
'bytes' => [
'total' => $spaceTotal,
'free' => $spaceFree,
'used' => $spaceUsed,
],
'readable' => [
'total' => Quota::getReadableSize($spaceTotal),
'free' => Quota::getReadableSize($spaceFree),
'used' => Quota::getReadableSize($spaceUsed),
],
],
'network' => $networkInterfaces,
];
return $response->withJson($stats);
}
}

124
src/Service/CpuStats.php Normal file
View File

@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
namespace App\Service;
use App\Service\CpuStats\CpuData;
use Brick\Math\BigDecimal;
use Brick\Math\RoundingMode;
class CpuStats
{
/**
* @return CpuData[]
*/
public static function getCurrentLoad(): array
{
$cpuStatsRaw = file('/proc/stat', FILE_IGNORE_NEW_LINES) ?: [];
$cpuCoreData = [];
foreach ($cpuStatsRaw as $statLine) {
$lineData = preg_split('/\s+/', $statLine) ?: [];
$lineName = array_shift($lineData) ?? '';
if ($lineName === 'cpu') {
$cpuCoreData[] = CpuData::fromCoreData('total', $lineData);
} elseif (str_starts_with($lineName, 'cpu')) {
$cpuCoreData[] = CpuData::fromCoreData($lineName, $lineData);
}
}
return $cpuCoreData;
}
public static function calculateDelta(CpuData $current, CpuData $previous): CpuData
{
$name = $current->name;
$user = $current->user - $previous->user;
$nice = $current->nice - $previous->nice;
$system = $current->system - $previous->system;
$idle = $current->idle - $previous->idle;
$iowait = null;
if ($current->iowait !== null && $previous->iowait !== null) {
$iowait = $current->iowait - $previous->iowait;
}
$irq = null;
if ($current->irq !== null && $previous->irq !== null) {
$irq = $current->irq - $previous->irq;
}
$softirq = null;
if ($current->softirq !== null && $previous->softirq !== null) {
$softirq = $current->softirq - $previous->softirq;
}
$steal = null;
if ($current->steal !== null && $previous->steal !== null) {
$steal = $current->steal - $previous->steal;
}
$guest = null;
if ($current->guest !== null && $previous->guest !== null) {
$guest = $current->guest - $previous->guest;
}
return new CpuData(
$name,
true,
$user,
$nice,
$system,
$idle,
$iowait,
$irq,
$softirq,
$steal,
$guest
);
}
public static function getUsage(CpuData $delta): BigDecimal
{
$usage = $delta->getTotalUsage();
$total = $delta->getTotal();
return BigDecimal::of($usage)
->multipliedBy(100)
->dividedBy($total, 2, RoundingMode::HALF_UP);
}
public static function getIdle(CpuData $delta): BigDecimal
{
$idle = $delta->idle;
$total = $delta->getTotal();
return BigDecimal::of($idle)
->multipliedBy(100)
->dividedBy($total, 2, RoundingMode::HALF_UP);
}
public static function getIoWait(CpuData $delta): BigDecimal
{
$ioWait = $delta->iowait;
$total = $delta->getTotal();
return BigDecimal::of($ioWait ?? 0)
->multipliedBy(100)
->dividedBy($total, 2, RoundingMode::HALF_UP);
}
public static function getSteal(CpuData $delta): BigDecimal
{
$steal = $delta->steal;
$total = $delta->getTotal();
return BigDecimal::of($steal ?? 0)
->multipliedBy(100)
->dividedBy($total, 2, RoundingMode::HALF_UP);
}
}

View File

@ -0,0 +1,171 @@
<?php
declare(strict_types=1);
namespace App\Service\CpuStats;
use Brick\Math\BigInteger;
class CpuData
{
public readonly string $name;
public readonly bool $isDelta;
/**
* Time spent with normal processing in user mode.
*/
public readonly int $user;
/**
* Time spent with niced processes in user mode.
*/
public readonly int $nice;
/**
* Time spent running in kernel mode.
*/
public readonly int $system;
/**
* Time spent in vacations twiddling thumbs.
*/
public readonly int $idle;
/**
* Time spent waiting for I/O to completed. This is considered idle time too.
*
* since Kernel 2.5.41
*/
public readonly ?int $iowait;
/**
* Time spent serving hardware interrupts. See the description of the intr line for more details.
*
* since 2.6.0
*/
public readonly ?int $irq;
/**
* Time spent serving software interrupts.
*
* since 2.6.0
*/
public readonly ?int $softirq;
/**
* Time stolen by other operating systems running in a virtual environment.
*
* since 2.6.11
*/
public readonly ?int $steal;
/**
* Time spent for running a virtual CPU or guest OS under the control of the kernel.
*
* since 2.6.24
*/
public readonly ?int $guest;
public function __construct(
string $name,
bool $isDelta,
int $user,
int $nice,
int $system,
int $idle,
?int $iowait = null,
?int $irq = null,
?int $softirq = null,
?int $steal = null,
?int $guest = null,
) {
$this->name = $name;
$this->isDelta = $isDelta;
$this->user = $user;
$this->nice = $nice;
$this->system = $system;
$this->idle = $idle;
$this->iowait = $iowait;
$this->irq = $irq;
$this->softirq = $softirq;
$this->steal = $steal;
$this->guest = $guest;
}
public static function fromCoreData(string $name, array $coreData): self
{
$user = (int) $coreData[0];
$nice = (int) $coreData[1];
$system = (int) $coreData[2];
$idle = (int) $coreData[3];
$iowait = null;
if (isset($coreData[4])) {
$iowait = (int) $coreData[4];
}
$irq = null;
if (isset($coreData[5])) {
$irq = (int) $coreData[5];
}
$softirq = null;
if (isset($coreData[6])) {
$softirq = (int) $coreData[6];
}
$steal = null;
if (isset($coreData[7])) {
$steal = (int) $coreData[7];
}
$guest = null;
if (isset($coreData[8])) {
$guest = (int) $coreData[8];
}
return new self(
$name,
false,
$user,
$nice,
$system,
$idle,
$iowait,
$irq,
$softirq,
$steal,
$guest
);
}
public function getTotalUsage(): BigInteger
{
return BigInteger::sum(
$this->user,
$this->nice,
$this->system,
$this->iowait ?? 0,
$this->irq ?? 0,
$this->softirq ?? 0,
$this->steal ?? 0,
$this->guest ?? 0
);
}
public function getTotal(): BigInteger
{
return BigInteger::sum(
$this->user,
$this->nice,
$this->system,
$this->idle,
$this->iowait ?? 0,
$this->irq ?? 0,
$this->softirq ?? 0,
$this->steal ?? 0,
$this->guest ?? 0
);
}
}

View File

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace App\Service;
use App\Service\MemoryStats\MemoryData;
class MemoryStats
{
public static function getMemoryUsage(): MemoryData
{
$meminfoRaw = file('/proc/meminfo', FILE_IGNORE_NEW_LINES) ?: [];
$meminfo = [];
foreach ($meminfoRaw as $line) {
if (!str_contains($line, ':')) {
continue;
}
[$key, $val] = explode(':', $line);
$meminfo[$key] = trim($val);
}
return MemoryData::fromMeminfo($meminfo);
}
}

View File

@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace App\Service\MemoryStats;
use App\Radio\Quota;
use Brick\Math\BigInteger;
class MemoryData
{
public function __construct(
public readonly BigInteger $memTotal,
public readonly BigInteger $memFree,
public readonly BigInteger $cached,
public readonly BigInteger $swapTotal,
public readonly BigInteger $swapFree,
) {
}
public static function fromMeminfo(array $meminfo): self
{
$memTotal = Quota::convertFromReadableSize($meminfo['MemTotal']) ?? BigInteger::zero();
$memFree = Quota::convertFromReadableSize($meminfo['MemFree']) ?? BigInteger::zero();
$cached = Quota::convertFromReadableSize($meminfo['Cached']) ?? BigInteger::zero();
$swapTotal = Quota::convertFromReadableSize($meminfo['SwapTotal']) ?? BigInteger::zero();
$swapFree = Quota::convertFromReadableSize($meminfo['SwapFree']) ?? BigInteger::zero();
return new self(
$memTotal,
$memFree,
$cached,
$swapTotal,
$swapFree
);
}
public function getUsedMemory(): BigInteger
{
return $this->memTotal
->minus($this->memFree)
->minus($this->cached);
}
public function getUsedSwap(): BigInteger
{
return $this->swapTotal->minus($this->swapFree);
}
}

View File

@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace App\Service;
use App\Service\NetworkStats\NetworkData;
use App\Service\NetworkStats\NetworkData\Received;
use App\Service\NetworkStats\NetworkData\Transmitted;
use Brick\Math\BigDecimal;
class NetworkStats
{
public static function getNetworkUsage(): array
{
$networkRaw = file('/proc/net/dev', FILE_IGNORE_NEW_LINES) ?: [];
$currentTimestamp = microtime(true);
$interfaces = [];
foreach ($networkRaw as $lineNumber => $line) {
if ($lineNumber <= 1) {
continue;
}
[$interfaceName, $interfaceData] = explode(':', $line);
$interfaceName = trim($interfaceName);
$interfaceData = preg_split('/\s+/', trim($interfaceData)) ?: [];
$interfaces[] = NetworkData::fromInterfaceData(
$interfaceName,
BigDecimal::of($currentTimestamp),
$interfaceData
);
}
return $interfaces;
}
public static function calculateDelta(NetworkData $current, NetworkData $previous): NetworkData
{
$interfaceName = $current->interfaceName;
$received = self::calculateReceivedDelta($current->received, $previous->received);
$transmitted = self::calculateTransmittedDelta($current->transmitted, $previous->transmitted);
return new NetworkData(
$interfaceName,
$current->time->minus($previous->time),
$received,
$transmitted,
true
);
}
public static function calculateReceivedDelta(Received $current, Received $previous): Received
{
return new Received(
$current->bytes->minus($previous->bytes),
$current->packets->minus($previous->packets),
$current->errs->minus($previous->errs),
$current->drop->minus($previous->drop),
$current->fifo->minus($previous->fifo),
$current->frame->minus($previous->frame),
$current->compressed->minus($previous->compressed),
$current->multicast->minus($previous->multicast)
);
}
public static function calculateTransmittedDelta(Transmitted $current, Transmitted $previous): Transmitted
{
return new Transmitted(
$current->bytes->minus($previous->bytes),
$current->packets->minus($previous->packets),
$current->errs->minus($previous->errs),
$current->drop->minus($previous->drop),
$current->fifo->minus($previous->fifo),
$current->colls->minus($previous->colls),
$current->carrier->minus($previous->carrier),
$current->compressed->minus($previous->compressed)
);
}
}

View File

@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace App\Service\NetworkStats;
use App\Service\NetworkStats\NetworkData\Received;
use App\Service\NetworkStats\NetworkData\Transmitted;
use Brick\Math\BigDecimal;
use Brick\Math\BigInteger;
class NetworkData
{
public function __construct(
public readonly string $interfaceName,
public readonly BigDecimal $time,
public readonly Received $received,
public readonly Transmitted $transmitted,
public readonly bool $isDelta = false
) {
}
public static function fromInterfaceData(
string $interfaceName,
BigDecimal $time,
array $interfaceData
): self {
$received = new Received(
BigInteger::of($interfaceData[0] ?? 0),
BigInteger::of($interfaceData[1] ?? 0),
BigInteger::of($interfaceData[2] ?? 0),
BigInteger::of($interfaceData[3] ?? 0),
BigInteger::of($interfaceData[4] ?? 0),
BigInteger::of($interfaceData[5] ?? 0),
BigInteger::of($interfaceData[6] ?? 0),
BigInteger::of($interfaceData[7] ?? 0)
);
$transmitted = new Transmitted(
BigInteger::of($interfaceData[8] ?? 0),
BigInteger::of($interfaceData[9] ?? 0),
BigInteger::of($interfaceData[10] ?? 0),
BigInteger::of($interfaceData[11] ?? 0),
BigInteger::of($interfaceData[12] ?? 0),
BigInteger::of($interfaceData[13] ?? 0),
BigInteger::of($interfaceData[14] ?? 0),
BigInteger::of($interfaceData[15] ?? 0)
);
return new self(
$interfaceName,
$time,
$received,
$transmitted
);
}
}

View File

@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace App\Service\NetworkStats\NetworkData;
use Brick\Math\BigInteger;
class Received
{
/**
* The total number of bytes of data received by the interface.
*/
public readonly BigInteger $bytes;
/**
* The total number of packets of data received by the interface.
*/
public readonly BigInteger $packets;
/**
* The total number of receive errors detected by the device driver.
*/
public readonly BigInteger $errs;
/**
* The total number of packets dropped by the device driver.
*/
public readonly BigInteger $drop;
/**
* The number of FIFO buffer errors.
*/
public readonly BigInteger $fifo;
/**
* The number of packet framing errors.
*/
public readonly BigInteger $frame;
/**
* The number of compressed packets received by the device driver.
*/
public readonly BigInteger $compressed;
/**
* The number of multicast frames received by the device driver.
*/
public readonly BigInteger $multicast;
public function __construct(
BigInteger $bytes,
BigInteger $packets,
BigInteger $errs,
BigInteger $drop,
BigInteger $fifo,
BigInteger $frame,
BigInteger $compressed,
BigInteger $multicast
) {
$this->bytes = $bytes;
$this->packets = $packets;
$this->errs = $errs;
$this->drop = $drop;
$this->fifo = $fifo;
$this->frame = $frame;
$this->compressed = $compressed;
$this->multicast = $multicast;
}
}

View File

@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace App\Service\NetworkStats\NetworkData;
use Brick\Math\BigInteger;
class Transmitted
{
/**
* The total number of bytes of data transmitted by the interface.
*/
public readonly BigInteger $bytes;
/**
* The total number of packets of data transmitted by the interface.
*/
public readonly BigInteger $packets;
/**
* The total number of transmit errors detected by the device driver.
*/
public readonly BigInteger $errs;
/**
* The total number of packets dropped by the device driver.
*/
public readonly BigInteger $drop;
/**
* The number of FIFO buffer errors.
*/
public readonly BigInteger $fifo;
/**
* The number of collisions detected on the interface.
*/
public readonly BigInteger $colls;
/**
* The number of carrier losses detected by the device driver.
*/
public readonly BigInteger $carrier;
/**
* The number of compressed packets transmitted by the device driver.
*/
public readonly BigInteger $compressed;
public function __construct(
BigInteger $bytes,
BigInteger $packets,
BigInteger $errs,
BigInteger $drop,
BigInteger $fifo,
BigInteger $colls,
BigInteger $carrier,
BigInteger $compressed
) {
$this->bytes = $bytes;
$this->packets = $packets;
$this->errs = $errs;
$this->drop = $drop;
$this->fifo = $fifo;
$this->colls = $colls;
$this->carrier = $carrier;
$this->compressed = $compressed;
}
}

View File

@ -1,85 +0,0 @@
<?php
/**
* @var \App\Assets $assets
* @var array $admin_panels
*/
$this->layout('main', [
'title' => 'Administration',
'manual' => true,
'page_class' => 'page-admin',
]);
?>
<h2 class="outside-card-header mb-1"><?=__('Administration')?></h2>
<div class="card-columns">
<?php foreach ($admin_panels as $category_id => $category): ?>
<section class="card" role="region">
<div class="card-header bg-primary-dark">
<h2 class="card-title"><?=$category['label']?></h2>
</div>
<div class="list-group list-group-flush">
<?php foreach ($category['items'] as $item): ?>
<a class="list-group-item list-group-item-action" href="<?=$item['url']?>" title="<?=($item['title'] ?? '')?>">
<?=$item['label']?>
</a>
<?php endforeach; ?>
</div>
</section>
<?php endforeach; ?>
</div>
<h2 class="outside-card-header mb-1"><?=__('Server Status')?></h2>
<div class="card-deck">
<div class="card" role="region">
<div class="card-header bg-primary-dark">
<h2 class="card-title"><?=__('CPU Load')?></h2>
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">
<p class="card-text mb-0">
<strong><?=round($load[0], 2)?></strong>
<small><?=__('Current')?></small>
</p>
</li>
<li class="list-group-item">
<p class="card-text mb-0">
<strong><?= round($load[2], 2) ?></strong>
<small><?= __('15-Minute Average') ?></small>
</p>
</li>
</ul>
</div>
<div class="card" role="region">
<div class="card-header bg-primary-dark">
<h2 class="card-title"><?= __('Memory') ?></h2>
</div>
<div class="card-body">
<div class="progress h-20 mb-1">
<div class="progress-bar" role="progressbar" aria-valuenow="<?= $memory_percent ?>" aria-valuemin="0"
aria-valuemax="100" style="width: <?= $memory_percent ?>%;">
<?= $memory_percent ?>%
</div>
</div>
<h3><small><?= __('%s of %s Used', $memory_used, $memory_total) ?></small></h3>
</div>
</div>
<div class="card" role="region">
<div class="card-header bg-primary-dark">
<h2 class="card-title"><?= __('Disk Space') ?></h2>
</div>
<div class="card-body">
<div class="progress h-20 mb-1">
<div class="progress-bar" role="progressbar" aria-valuenow="<?= $space_percent ?>" aria-valuemin="0"
aria-valuemax="100" style="width: <?= $space_percent ?>%;">
<?= $space_percent ?>%
</div>
</div>
<h3><small><?= __('%s of %s Used', $space_used, $space_total) ?></small></h3>
</div>
</div>
</div>

View File

@ -11,10 +11,7 @@ class Admin_IndexCest extends CestAbstract
$I->wantTo('See the administration homepage.');
$I->amOnPage('/admin');
$I->see('Administration');
$I->see('System Maintenance');
$I->see('Users');
$I->see('Stations');
$I->seeResponseCodeIs(200);
$I->seeInTitle('Administration');
}
}