Compare commits

..

9 Commits
1.3.0 ... 1.4.0

Author SHA1 Message Date
e2dd9177f7 fix number format 2024-04-17 22:08:19 +02:00
5a9e8d6799 fix monitoring numbers 2024-04-17 21:09:09 +02:00
868b70f530 add cron to docker 2024-04-17 17:14:12 +02:00
d07e9bcad2 remote monitor (#167) 2024-04-17 16:03:06 +02:00
0cd815cce6 ui fix and build 2024-04-14 18:17:44 +02:00
5ab6617b5d fix read file 2024-04-14 17:53:08 +02:00
72b37c56fd ui fix 2024-04-14 14:53:58 +02:00
8a4ef66946 update Feature/add remote server logs (#166) 2024-04-14 14:41:00 +02:00
4517ca7d2a Feature/add remote server logs (#159) 2024-04-14 14:34:47 +02:00
60 changed files with 720 additions and 115 deletions

View File

@ -0,0 +1,35 @@
<?php
namespace App\Actions\Server;
use App\Models\Server;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
class CreateServerLog
{
/**
* @throws ValidationException
*/
public function create(Server $server, array $input): void
{
$this->validate($input);
$server->logs()->create([
'is_remote' => true,
'name' => $input['path'],
'type' => 'remote',
'disk' => 'ssh',
]);
}
/**
* @throws ValidationException
*/
protected function validate(array $input): void
{
Validator::make($input, [
'path' => 'required',
])->validate();
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace App\Console\Commands;
use App\Models\Server;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Builder;
class GetMetricsCommand extends Command
{
protected $signature = 'metrics:get';
protected $description = 'Get server metrics';
public function handle(): void
{
$checkedMetrics = 0;
Server::query()->whereHas('services', function (Builder $query) {
$query->where('type', 'monitoring')
->where('name', 'remote-monitor');
})->chunk(10, function ($servers) use (&$checkedMetrics) {
/** @var Server $server */
foreach ($servers as $server) {
$info = $server->os()->resourceInfo();
$server->metrics()->create(array_merge($info, ['server_id' => $server->id]));
$checkedMetrics++;
}
});
$this->info("Checked $checkedMetrics metrics");
}
}

View File

@ -17,6 +17,7 @@ protected function schedule(Schedule $schedule): void
$schedule->command('backups:run "0 0 * * 0"')->weekly();
$schedule->command('backups:run "0 0 1 * *"')->monthly();
$schedule->command('metrics:delete-older-metrics')->daily();
$schedule->command('metrics:get')->everyMinute();
}
/**

View File

@ -54,7 +54,6 @@ public function show(Server $server): View
{
return view('servers.show', [
'server' => $server,
'logs' => $server->logs()->latest()->limit(10)->get(),
]);
}

View File

@ -2,10 +2,13 @@
namespace App\Http\Controllers;
use App\Actions\Server\CreateServerLog;
use App\Facades\Toast;
use App\Models\Server;
use App\Models\ServerLog;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class ServerLogController extends Controller
{
@ -13,6 +16,7 @@ public function index(Server $server): View
{
return view('server-logs.index', [
'server' => $server,
'pageTitle' => __('Vito Logs'),
]);
}
@ -26,4 +30,31 @@ public function show(Server $server, ServerLog $serverLog): RedirectResponse
'content' => $serverLog->getContent(),
]);
}
public function remote(Server $server): View
{
return view('server-logs.remote-logs', [
'server' => $server,
'remote' => true,
'pageTitle' => __('Remote Logs'),
]);
}
public function store(Server $server, Request $request): \App\Helpers\HtmxResponse
{
app(CreateServerLog::class)->create($server, $request->input());
Toast::success('Log added successfully.');
return htmx()->redirect(route('servers.logs.remote', ['server' => $server]));
}
public function destroy(Server $server, ServerLog $serverLog): RedirectResponse
{
$serverLog->delete();
Toast::success('Remote log deleted successfully.');
return redirect()->route('servers.logs.remote', ['server' => $server]);
}
}

View File

@ -13,6 +13,7 @@ public function index(Server $server, Site $site): View
return view('site-logs.index', [
'server' => $server,
'site' => $site,
'pageTitle' => __('Vito Logs'),
]);
}
}

View File

@ -16,6 +16,7 @@
* @property string $disk
* @property Server $server
* @property ?Site $site
* @property bool $is_remote
*/
class ServerLog extends AbstractModel
{
@ -27,11 +28,13 @@ class ServerLog extends AbstractModel
'type',
'name',
'disk',
'is_remote',
];
protected $casts = [
'server_id' => 'integer',
'site_id' => 'integer',
'is_remote' => 'boolean',
];
public static function boot(): void
@ -64,6 +67,17 @@ public function site(): BelongsTo
return $this->belongsTo(Site::class);
}
public static function getRemote($query, bool $active = true, ?Site $site = null)
{
$query->where('is_remote', $active);
if ($site) {
$query->where('name', 'like', $site->path.'%');
}
return $query;
}
public function write($buf): void
{
if (Str::contains($buf, 'VITO_SSH_ERROR')) {
@ -78,6 +92,10 @@ public function write($buf): void
public function getContent(): ?string
{
if ($this->is_remote) {
return $this->server->os()->tail($this->name, 150);
}
if (Storage::disk($this->disk)->exists($this->name)) {
return Storage::disk($this->disk)->get($this->name);
}

View File

@ -4,6 +4,7 @@
use App\Exceptions\SourceControlIsNotConnected;
use App\SiteTypes\SiteType;
use App\SSH\Services\Webserver\Webserver;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -185,7 +186,9 @@ public function php(): ?Service
public function changePHPVersion($version): void
{
$this->server->webserver()->handler()->changePHPVersion($this, $version);
/** @var Webserver $handler */
$handler = $this->server->webserver()->handler();
$handler->changePHPVersion($this, $version);
$this->php_version = $version;
$this->save();
}

View File

@ -117,6 +117,16 @@ public function readFile(string $path): string
);
}
public function tail(string $path, int $lines): string
{
return $this->server->ssh()->exec(
$this->getScript('tail.sh', [
'path' => $path,
'lines' => $lines,
])
);
}
public function runScript(string $path, string $script, ?int $siteId = null): ServerLog
{
$ssh = $this->server->ssh();
@ -156,4 +166,21 @@ public function cleanup(): void
'cleanup'
);
}
public function resourceInfo(): array
{
$info = $this->server->ssh()->exec(
$this->getScript('resource-info.sh'),
);
return [
'load' => str($info)->after('load:')->before(PHP_EOL)->toString(),
'memory_total' => str($info)->after('memory_total:')->before(PHP_EOL)->toString(),
'memory_used' => str($info)->after('memory_used:')->before(PHP_EOL)->toString(),
'memory_free' => str($info)->after('memory_free:')->before(PHP_EOL)->toString(),
'disk_total' => str($info)->after('disk_total:')->before(PHP_EOL)->toString(),
'disk_used' => str($info)->after('disk_used:')->before(PHP_EOL)->toString(),
'disk_free' => str($info)->after('disk_free:')->before(PHP_EOL)->toString(),
];
}
}

View File

@ -1 +1 @@
[ -f __path__ ] && cat __path__
[ -f __path__ ] && sudo cat __path__

View File

@ -0,0 +1,7 @@
echo "load:$(uptime | awk -F'load average:' '{print $2}' | awk -F, '{print $1}' | tr -d ' ')"
echo "memory_total:$(free -k | awk 'NR==2{print $2}')"
echo "memory_used:$(free -k | awk 'NR==2{print $3}')"
echo "memory_free:$(free -k | awk 'NR==2{print $7}')"
echo "disk_total:$(df -BM / | awk 'NR==2{print $2}' | sed 's/M//')"
echo "disk_used:$(df -BM / | awk 'NR==2{print $3}' | sed 's/M//')"
echo "disk_free:$(df -BM / | awk 'NR==2{print $4}' | sed 's/M//')"

View File

@ -0,0 +1 @@
sudo tail -n __lines__ __path__

View File

@ -0,0 +1,53 @@
<?php
namespace App\SSH\Services\Monitoring\RemoteMonitor;
use App\Models\Metric;
use App\SSH\Services\AbstractService;
use Closure;
use Illuminate\Validation\Rule;
class RemoteMonitor extends AbstractService
{
public function creationRules(array $input): array
{
return [
'type' => [
function (string $attribute, mixed $value, Closure $fail) {
$monitoringExists = $this->service->server->monitoring();
if ($monitoringExists) {
$fail('You already have a monitoring service on the server.');
}
},
],
'version' => [
'required',
Rule::in(['latest']),
],
];
}
public function creationData(array $input): array
{
return [
'data_retention' => 10,
];
}
public function data(): array
{
return [
'data_retention' => $this->service->type_data['data_retention'] ?? 10,
];
}
public function install(): void
{
//
}
public function uninstall(): void
{
Metric::where('server_id', $this->service->server_id)->delete();
}
}

View File

@ -1,6 +1,6 @@
<?php
namespace App\SSH\Services\VitoAgent;
namespace App\SSH\Services\Monitoring\VitoAgent;
use App\Models\Metric;
use App\SSH\HasScripts;

View File

@ -32,7 +32,12 @@ function htmx(): HtmxResponse
function vito_version(): string
{
return exec('git describe --tags');
$version = exec('git describe --tags');
if (str($version)->contains('-')) {
return str($version)->before('-').' (dev)';
}
return $version;
}
function convert_time_format($string): string

View File

@ -142,6 +142,7 @@
'ufw' => 'firewall',
'supervisor' => 'process_manager',
'vito-agent' => 'monitoring',
'remote-monitor' => 'monitoring',
],
'service_handlers' => [
'nginx' => \App\SSH\Services\Webserver\Nginx::class,
@ -152,7 +153,8 @@
'php' => \App\SSH\Services\PHP\PHP::class,
'ufw' => \App\SSH\Services\Firewall\Ufw::class,
'supervisor' => \App\SSH\Services\ProcessManager\Supervisor::class,
'vito-agent' => \App\SSH\Services\VitoAgent\VitoAgent::class,
'vito-agent' => \App\SSH\Services\Monitoring\VitoAgent\VitoAgent::class,
'remote-monitor' => \App\SSH\Services\Monitoring\RemoteMonitor\RemoteMonitor::class,
],
'service_units' => [
'nginx' => [

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('server_logs', function (Blueprint $table) {
$table->boolean('is_remote')->default(false);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('server_logs', function (Blueprint $table) {
$table->dropColumn('is_remote');
});
}
};

View File

@ -19,7 +19,7 @@ RUN apt-get install -y nginx
# php
RUN apt-get update \
&& apt-get install -y gnupg gosu curl ca-certificates zip unzip git supervisor libcap2-bin libpng-dev \
&& apt-get install -y cron gnupg gosu curl ca-certificates zip unzip git supervisor libcap2-bin libpng-dev \
python2 dnsutils librsvg2-bin fswatch wget \
&& add-apt-repository ppa:ondrej/php -y \
&& apt-get update \
@ -44,6 +44,8 @@ RUN rm /etc/nginx/sites-enabled/default
COPY docker/nginx.conf /etc/nginx/sites-available/default
RUN ln -s /etc/nginx/sites-available/default /etc/nginx/sites-enabled/default
RUN echo "* * * * * cd /var/www/html && php artisan schedule:run >> /var/log/cron.log 2>&1" | crontab -
# supervisord
COPY docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf

View File

@ -37,6 +37,8 @@ php /var/www/html/artisan view:cache
php /var/www/html/artisan user:create "$NAME" "$EMAIL" "$PASSWORD"
cron
echo "Vito is running! 🚀"
/usr/bin/supervisord

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
{
"resources/css/app.css": {
"file": "assets/app-53e4d707.css",
"file": "assets/app-f65997bb.css",
"isEntry": true,
"src": "resources/css/app.css"
},

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 -5.23 70 70" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg">
<metadata>
<rdf:RDF>
<cc:Work>
<dc:subject>
Miscellaneous
</dc:subject>
<dc:identifier>
health-monitoring
</dc:identifier>
<dc:title>
Health Monitoring
</dc:title>
<dc:format>
image/svg+xml
</dc:format>
<dc:publisher>
Amido Limited
</dc:publisher>
<dc:creator>
Richard Slater
</dc:creator>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
</cc:Work>
</rdf:RDF>
</metadata>
<path d="m -633.94123,753.25889 c -6.59942,-3.2916 -17.80605,-13.7307 -24.90952,-23.2035 l -0.38611,-0.5149 4.90905,0 c 3.3284,0 5.08031,-0.051 5.44092,-0.1594 0.95525,-0.2862 1.50799,-0.9179 2.58607,-2.9554 1.97619,-3.735 2.24879,-4.2224 2.2879,-4.0904 0.0218,0.074 0.44604,4.3009 0.94276,9.3939 0.87326,8.9538 0.91529,9.2823 1.27154,9.9368 0.62081,1.1407 1.47439,1.6301 2.85312,1.6359 1.01617,0 1.76269,-0.3415 2.41627,-1.1191 0.25355,-0.3016 1.82033,-3.2056 3.48173,-6.4532 l 3.02073,-5.9047 10.36659,-0.039 c 10.32236,-0.039 10.36894,-0.041 10.91581,-0.3356 1.1802,-0.6369 1.77594,-1.6202 1.77528,-2.9304 -6.9e-4,-1.3721 -0.67396,-2.4208 -1.91258,-2.9791 -0.5125,-0.231 -1.30161,-0.2501 -11.80218,-0.2858 -7.69785,-0.026 -11.47959,0.01 -11.97032,0.1108 -1.27206,0.264 -1.77303,0.7868 -3.0106,3.1416 l -1.08999,2.0739 -0.1043,-0.5158 c -0.0574,-0.2837 -0.47667,-4.3775 -0.9318,-9.0974 -0.45513,-4.7199 -0.88563,-8.7992 -0.95668,-9.0652 -0.36496,-1.3662 -1.62876,-2.2659 -3.16688,-2.2544 -1.04822,0.01 -1.94772,0.4395 -2.48617,1.1931 -0.17485,0.2447 -1.92936,3.5346 -3.8989,7.311 l -3.581,6.866 -5.76782,0.036 -5.76783,0.036 -0.83086,-1.6834 c -2.06318,-4.1804 -2.89449,-7.6097 -2.738,-11.2949 0.12425,-2.9261 0.69392,-5.0125 2.04328,-7.4832 1.10812,-2.029 3.06519,-4.3559 4.69277,-5.5795 1.78333,-1.3407 4.15216,-2.2461 6.64618,-2.5403 2.10735,-0.2485 4.60651,0.089 7.37391,0.9964 1.2153,0.3984 4.21499,1.9073 5.62954,2.8318 2.45012,1.6012 5.68511,4.4633 7.84072,6.9369 l 0.80955,0.929 0.94007,-1.2397 c 1.88483,-2.4857 4.78785,-5.1075 7.55221,-6.8208 5.19337,-3.2187 11.05786,-4.2791 15.6703,-2.8335 3.74959,1.1752 6.7744,3.9944 8.98105,8.3706 2.19828,4.3596 2.39398,9.8576 0.53892,15.1404 -1.06649,3.0372 -2.39805,5.6594 -4.46756,8.7979 -2.55838,3.88 -4.87538,6.6471 -9.08862,10.8542 -5.31708,5.3093 -11.00984,9.9038 -16.48777,13.3068 -1.60577,0.9976 -3.84246,2.2037 -4.0818,2.201 -0.0583,0 -0.75536,-0.325 -1.54898,-0.7208 z" fill="#00bcf2" transform="translate(667.003 -694.43)"/>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -6,8 +6,9 @@
hx-swap="outerHTML"
hx-select="#deploy"
>
<x-primary-button hx-disable>
<x-primary-button class="flex items-center justify-between" :active="true" hx-disable>
{{ __("Deploy") }}
<x-heroicon name="o-play-circle" class="ml-1 h-5 w-5" />
</x-primary-button>
</form>
@endif

View File

@ -1,10 +1,3 @@
@php
$class = "mx-auto px-4 sm:px-6 lg:px-8";
if (! str($attributes->get("class"))->contains("max-w-")) {
$class .= " max-w-7xl";
}
@endphp
<div {!! $attributes->merge(["class" => $class]) !!}>
<div {!! $attributes->merge(["class" => "max-w-5xl mx-auto"]) !!}>
{{ $slot }}
</div>

View File

@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
{{ $attributes }}
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m5.231 13.481L15 17.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v16.5c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Zm3.75 11.625a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z"
/>
</svg>

After

Width:  |  Height:  |  Size: 599 B

View File

@ -0,0 +1,15 @@
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
{{ $attributes }}
>
<path stroke-linecap="round" stroke-linejoin="round" d="M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.91 11.672a.375.375 0 0 1 0 .656l-5.603 3.113a.375.375 0 0 1-.557-.328V8.887c0-.286.307-.466.557-.327l5.603 3.112Z"
/>
</svg>

After

Width:  |  Height:  |  Size: 476 B

View File

@ -1,5 +1,5 @@
<td
{!! $attributes->merge(["class" => "whitespace-nowrap border-t border-gray-200 px-6 py-4 text-gray-700 dark:border-gray-700 dark:text-gray-300 w-1"]) !!}
{!! $attributes->merge(["class" => "whitespace-nowrap px-6 py-4 text-gray-700 dark:text-gray-300 w-1"]) !!}
>
{{ $slot }}
</td>

View File

@ -56,7 +56,7 @@ class="ml-1"
-
@endif
</x-td>
<x-td class="flex w-full justify-end">
<x-td class="text-right">
@if (in_array($file->status, [\App\Enums\BackupFileStatus::CREATED, \App\Enums\BackupFileStatus::RESTORED, \App\Enums\BackupFileStatus::RESTORE_FAILED]))
<x-icon-button
x-on:click="restoreAction = '{{ route('servers.databases.backups.files.restore', ['server' => $server, 'backup' => $backup, 'backupFile' => $file]) }}'; $dispatch('open-modal', 'restore-backup')"

View File

@ -32,7 +32,7 @@
@include("databases.partials.backup-status", ["status" => $backup->status])
</div>
</x-td>
<x-td class="flex w-full justify-end">
<x-td class="text-right">
<x-icon-button
:href="route('servers.databases.backups', ['server' => $server, 'backup' => $backup])"
>

View File

@ -33,7 +33,7 @@
@include("databases.partials.database-status", ["status" => $database->status])
</div>
</x-td>
<x-td class="flex w-full justify-end">
<x-td class="text-right">
<x-icon-button
x-on:click="deleteAction = '{{ route('servers.databases.destroy', ['server' => $server, 'database' => $database]) }}'; $dispatch('open-modal', 'delete-database')"
>

View File

@ -42,7 +42,7 @@
@include("databases.partials.database-user-status", ["status" => $databaseUser->status])
</div>
</x-td>
<x-td class="flex w-full justify-end">
<x-td class="text-right">
<x-icon-button
x-on:click="$dispatch('open-modal', 'database-user-password'); document.getElementById('txt-database-user-password').value = 'Loading...';"
hx-post="{{ route('servers.databases.users.password', ['server' => $server, 'databaseUser' => $databaseUser]) }}"

View File

@ -4,7 +4,16 @@
@endif
<x-slot name="header">
<h2 class="text-lg font-semibold">{{ $server->name }}</h2>
@if (isset($header))
<header class="flex-grow border-b border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800">
<div class="mx-auto flex h-20 w-full max-w-full items-center justify-between">
{{ $header }}
</div>
</header>
@else
<h2 class="text-lg font-semibold">{{ $server->name }}</h2>
@endif
<div class="flex flex-col items-end">
@include("servers.partials.server-status")
<x-input-label class="mt-1 cursor-pointer" x-data="{ copied: false }">

View File

@ -155,7 +155,7 @@ class="fixed left-0 top-0 z-40 h-screen w-64 -translate-x-full border-r border-g
<li>
<x-sidebar-link
:href="route('servers.logs', ['server' => $server])"
:active="request()->routeIs('servers.logs')"
:active="request()->routeIs('servers.logs*')"
>
<x-heroicon name="o-square-3-stack-3d" class="h-6 w-6" />
<span class="ml-2">

View File

@ -50,7 +50,7 @@ class="mr-1"
<x-tab-item
class="mr-1"
:href="route('servers.sites.logs', ['server' => $site->server, 'site' => $site])"
:active="request()->routeIs('servers.sites.logs')"
:active="request()->routeIs('servers.sites.logs*')"
>
<x-heroicon name="o-square-3-stack-3d" class="h-5 w-5" />
<span class="ml-2 hidden xl:block">Logs</span>
@ -105,7 +105,7 @@ class="flex w-full cursor-pointer items-center rounded-md border border-gray-300
</x-dropdown-link>
<x-dropdown-link
:href="route('servers.sites.logs', ['server' => $site->server, 'site' => $site])"
:active="request()->routeIs('servers.sites.logs')"
:active="request()->routeIs('servers.sites.logs*')"
>
<x-heroicon name="o-square-3-stack-3d" class="h-5 w-5" />
<span class="ml-2">Logs</span>

View File

@ -63,19 +63,19 @@ class="h-[200px] !p-0"
<x-simple-card class="grid grid-cols-1 gap-4 lg:grid-cols-2">
<span class="text-center lg:text-left">Total Memory</span>
<div class="text-center text-xl font-bold text-gray-600 dark:text-gray-400 lg:text-right">
{{ $lastMetric ? $lastMetric->memory_total : "-" }} MB
{{ $lastMetric ? number_format((int) ($lastMetric->memory_total / 1024)) : "-" }} MB
</div>
</x-simple-card>
<x-simple-card class="grid grid-cols-1 gap-4 lg:grid-cols-2">
<span class="text-center lg:text-left">Used Memory</span>
<div class="text-center text-xl font-bold text-gray-600 dark:text-gray-400 lg:text-right">
{{ $lastMetric ? $lastMetric->memory_used : "-" }} MB
{{ $lastMetric ? number_format((int) ($lastMetric->memory_used / 1024)) : "-" }} MB
</div>
</x-simple-card>
<x-simple-card class="grid grid-cols-1 gap-4 lg:grid-cols-2">
<span class="text-center lg:text-left">Free Memory</span>
<div class="text-center text-xl font-bold text-gray-600 dark:text-gray-400 lg:text-right">
{{ $lastMetric ? $lastMetric->memory_free : "-" }} MB
{{ $lastMetric ? number_format((int) ($lastMetric->memory_free / 1024)) : "-" }} MB
</div>
</x-simple-card>
</div>
@ -83,19 +83,19 @@ class="h-[200px] !p-0"
<x-simple-card class="grid grid-cols-1 gap-4 lg:grid-cols-2">
<span class="text-center lg:text-left">Total Space</span>
<div class="text-center text-xl font-bold text-gray-600 dark:text-gray-400 lg:text-right">
{{ $lastMetric ? $lastMetric->disk_total : "-" }} MB
{{ $lastMetric ? number_format($lastMetric->disk_total) : "-" }} MB
</div>
</x-simple-card>
<x-simple-card class="grid grid-cols-1 gap-4 lg:grid-cols-2">
<span class="text-center lg:text-left">Used Space</span>
<div class="text-center text-xl font-bold text-gray-600 dark:text-gray-400 lg:text-right">
{{ $lastMetric ? $lastMetric->disk_used : "-" }} MB
{{ $lastMetric ? number_format($lastMetric->disk_used) : "-" }} MB
</div>
</x-simple-card>
<x-simple-card class="grid grid-cols-1 gap-4 lg:grid-cols-2">
<span class="text-center lg:text-left">Free Space</span>
<div class="text-center text-xl font-bold text-gray-600 dark:text-gray-400 lg:text-right">
{{ $lastMetric ? $lastMetric->disk_free : "-" }} MB
{{ $lastMetric ? number_format($lastMetric->disk_free) : "-" }} MB
</div>
</x-simple-card>
</div>

View File

@ -22,7 +22,12 @@ class="p-6"
<x-input-label for="data_retention" value="Delete metrics older than" />
<x-select-input id="data_retention" name="data_retention" class="mt-1 w-full">
@foreach (config("core.metrics_data_retention") as $item)
<option value="{{ $item }}">{{ $item }} Days</option>
<option
value="{{ $item }}"
@if($server->monitoring()->handler()->data()['data_retention'] == $item) selected @endif
>
{{ $item }} Days
</option>
@endforeach
</x-select-input>
@error("data_retention")

View File

@ -1,5 +1,8 @@
<x-server-layout :server="$server">
<x-slot name="pageTitle">{{ $server->name }} Logs</x-slot>
@if (isset($pageTitle))
<x-slot name="pageTitle">{{ $pageTitle }}</x-slot>
@endif
@include("server-logs.partials.logs-list")
@include("server-logs.partials.header")
@include("server-logs.partials.logs-list-live")
</x-server-layout>

View File

@ -0,0 +1,59 @@
<div x-data="">
<x-card-header>
<x-slot name="title">{{ __("Manage your remote logs") }}</x-slot>
<x-slot name="description">
{{ __("Here you can add new logs") }}
</x-slot>
<x-slot name="aside">
<div class="flex flex-col items-end lg:flex-row lg:items-center">
<x-primary-button class="cursor-pointer" x-on:click="$dispatch('open-modal', 'add-log')">
{{ __("Add Remote Log") }}
</x-primary-button>
<x-modal name="add-log">
<form
id="add-log-form"
hx-post="{{ route("servers.logs.remote.store", ["server" => $server]) }}"
hx-select="#add-log-form"
hx-swap="outerHTML"
class="p-6"
>
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
{{ __("Add Remote Log") }}
</h2>
<div class="mt-6">
<x-input-label for="path" :value="__('Please introduce the full path to the log file.')" />
<x-text-input
list="sites"
value="{{ old('path') }}"
id="path"
name="path"
type="text"
class="mt-1 w-full"
/>
<datalist id="sites">
@foreach ($server->sites as $site)
<option>{{ str($site->path)->append("/") }}</option>
@endforeach
</datalist>
@error("path")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div class="mt-6 flex items-center justify-end">
<x-secondary-button type="button" x-on:click="$dispatch('close')">
{{ __("Cancel") }}
</x-secondary-button>
<x-primary-button class="ml-3" hx-disable>
{{ __("Save") }}
</x-primary-button>
</div>
</form>
</x-modal>
</div>
</x-slot>
</x-card-header>
</div>

View File

@ -0,0 +1,54 @@
@if (isset($pageTitle))
<x-slot name="pageTitle">{{ $pageTitle }}</x-slot>
@endif
<x-slot name="header">
<div class="hidden md:flex md:items-center md:justify-start">
<x-tab-item
class="mr-1"
:href="route('servers.logs', ['server' => $server])"
:active="request()->routeIs('servers.logs')"
>
<x-heroicon name="o-square-3-stack-3d" class="h-5 w-5" />
<span class="ml-2 hidden xl:block">{{ __("Vito Logs") }}</span>
</x-tab-item>
<x-tab-item
class="mr-1"
:href="route('servers.logs.remote', ['server' => $server])"
:active="request()->routeIs('servers.logs.remote')"
>
<x-heroicon name="o-document-magnifying-glass" class="h-5 w-5" />
<span class="ml-2 hidden xl:block">{{ __("Remote Logs") }}</span>
</x-tab-item>
</div>
<div class="md:hidden">
<x-dropdown align="left">
<x-slot name="trigger">
<div
class="flex w-full cursor-pointer items-center rounded-md border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-primary-500 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-primary-500 dark:focus:ring-primary-500"
>
Select
<button type="button" class="ml-2">
<x-heroicon name="o-chevron-down" class="h-4 w-4 text-gray-400" />
</button>
</div>
</x-slot>
<x-slot name="content">
<x-dropdown-link
:href="route('servers.logs', ['server' => $server])"
:active="request()->routeIs('servers.logs')"
>
<x-heroicon name="o-cog-6-tooth" class="h-5 w-5" />
<span class="ml-2">{{ __("Vito Logs") }}</span>
</x-dropdown-link>
<x-dropdown-link
:href="route('servers.logs.remote', ['server' => $server])"
:active="request()->routeIs('servers.logs.remote')"
>
<x-heroicon name="o-document-magnifying-glass" class="h-5 w-5" />
<span class="ml-2">{{ __("Remote Logs") }}</span>
</x-dropdown-link>
</x-slot>
</x-dropdown>
</div>
</x-slot>

View File

@ -0,0 +1,107 @@
@php
if (isset($site)) {
$logs = $site
->logs()
->latest()
->paginate(10);
} else {
$logs = $server
->logs()
->where("is_remote", isset($remote) ? 1 : 0)
->latest()
->paginate(10);
}
@endphp
<div x-data="{
deleteAction: '',
}">
<x-card-header>
<x-slot name="title">
{{ $pageTitle ?? "Logs" }}
</x-slot>
</x-card-header>
<x-live id="live-server-logs">
<x-table>
<x-thead>
<x-tr>
<x-th>
@isset($remote)
{{ __("Path") }}
@else
{{ __("Event") }}
@endisset
</x-th>
<x-th>{{ __("Date") }}</x-th>
<x-th></x-th>
</x-tr>
</x-thead>
<x-tbody>
@foreach ($logs as $log)
<x-tr>
<x-td>
@isset($remote)
{{ $log->name }}
@else
{{ $log->type }}
@endif
</x-td>
<x-td>
<x-datetime :value="$log->created_at" />
</x-td>
<x-td class="text-right">
<x-icon-button
x-on:click="$dispatch('open-modal', 'show-log'); document.getElementById('log-content').firstChild.innerHTML = '';"
hx-get="{{ route('servers.logs.show', ['server' => $server, 'serverLog' => $log->id]) }}"
hx-target="#log-content"
hx-select="#log-content"
>
<x-heroicon name="o-eye" class="h-5 w-5" />
</x-icon-button>
@if ($log->is_remote)
<x-icon-button
x-on:click="deleteAction = '{{ route('servers.logs.remote.destroy', ['server' => $server, 'serverLog' => $log->id]) }}'; $dispatch('open-modal', 'delete-remote-log')"
>
<x-heroicon name="o-trash" class="h-5 w-5" />
</x-icon-button>
@endif
</x-td>
</x-tr>
@endforeach
</x-tbody>
</x-table>
@if ($logs instanceof \Illuminate\Pagination\LengthAwarePaginator)
<div class="mt-5">
{{ $logs->withQueryString()->links() }}
</div>
@endif
</x-live>
<div id="delete"></div>
<x-modal name="show-log" max-width="4xl">
<div class="p-6">
<h2 class="mb-5 text-lg font-medium text-gray-900 dark:text-gray-100">
{{ __("View Log") }}
</h2>
<div id="log-content">
<x-console-view>
@if (session()->has("content"))
{{ session("content") }}
@endif
</x-console-view>
</div>
<div class="mt-6 flex justify-end">
<x-secondary-button type="button" x-on:click="$dispatch('close')">
{{ __("Close") }}
</x-secondary-button>
</div>
</div>
</x-modal>
<x-confirmation-modal
name="delete-remote-log"
title="Confirm"
description="Are you sure that you want to delete this log?"
method="delete"
x-bind:action="deleteAction"
/>
</div>

View File

@ -1,70 +0,0 @@
@php
if (isset($site)) {
$logs = $site
->logs()
->latest()
->paginate(10);
} else {
$logs = $server
->logs()
->latest()
->paginate(10);
}
@endphp
<div x-data="">
<x-card-header>
<x-slot name="title">{{ __("Logs") }}</x-slot>
</x-card-header>
<x-live id="live-server-logs">
<x-table>
<x-tr>
<x-th>{{ __("Event") }}</x-th>
<x-th>{{ __("Date") }}</x-th>
<x-th></x-th>
</x-tr>
@foreach ($logs as $log)
<x-tr>
<x-td>{{ $log->type }}</x-td>
<x-td>
<x-datetime :value="$log->created_at" />
</x-td>
<x-td>
<x-icon-button
x-on:click="$dispatch('open-modal', 'show-log'); document.getElementById('log-content').firstChild.innerHTML = '';"
hx-get="{{ route('servers.logs.show', ['server' => $server, 'serverLog' => $log->id]) }}"
hx-target="#log-content"
hx-select="#log-content"
>
<x-heroicon name="o-eye" class="h-5 w-5" />
</x-icon-button>
</x-td>
</x-tr>
@endforeach
</x-table>
@if ($logs instanceof \Illuminate\Pagination\LengthAwarePaginator)
<div class="mt-5">
{{ $logs->withQueryString()->links() }}
</div>
@endif
</x-live>
<x-modal name="show-log" max-width="4xl">
<div class="p-6">
<h2 class="mb-5 text-lg font-medium text-gray-900 dark:text-gray-100">
{{ __("View Log") }}
</h2>
<div id="log-content">
<x-console-view>
@if (session()->has("content"))
{{ session("content") }}
@endif
</x-console-view>
</div>
<div class="mt-6 flex justify-end">
<x-secondary-button type="button" x-on:click="$dispatch('close')">
{{ __("Close") }}
</x-secondary-button>
</div>
</div>
</x-modal>
</div>

View File

@ -0,0 +1,6 @@
<x-server-layout :server="$server">
@include("server-logs.partials.header")
@include("server-logs.partials.add-log")
@include("server-logs.partials.logs-list-live")
</x-server-layout>

View File

@ -15,5 +15,5 @@
@endif
</x-live>
@include("server-logs.partials.logs-list")
@include("server-logs.partials.logs-list-live", ["pageTitle" => "Logs"])
</div>

View File

@ -0,0 +1,6 @@
@include("services.partials.unit-actions.restart", ["disabled" => true])
@include("services.partials.unit-actions.start", ["disabled" => true])
@include("services.partials.unit-actions.stop", ["disabled" => true])
@include("services.partials.unit-actions.enable", ["disabled" => true])
@include("services.partials.unit-actions.disable", ["disabled" => true])
@include("services.partials.unit-actions.uninstall")

View File

@ -0,0 +1,37 @@
<x-secondary-button class="!w-full" x-on:click="$dispatch('open-modal', 'install-remote-monitor')">
Install
</x-secondary-button>
@push("modals")
<x-modal name="install-remote-monitor">
<form
id="install-remote-monitor-form"
hx-post="{{ route("servers.services.install", ["server" => $server]) }}"
hx-swap="outerHTML"
hx-select="#install-remote-monitor-form"
class="p-6"
>
@csrf
<input type="hidden" name="name" value="remote-monitor" />
<input type="hidden" name="type" value="monitoring" />
<input type="hidden" name="version" value="latest" />
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
{{ __("Install Remote Monitor") }}
</h2>
@error("type")
<x-input-error class="mt-2" :messages="$message" />
@enderror
<div class="mt-6 flex justify-end">
<x-secondary-button type="button" x-on:click="$dispatch('close')">
{{ __("Cancel") }}
</x-secondary-button>
<x-primary-button id="btn-remote-monitor" hx-disable class="ml-3">
{{ __("Install") }}
</x-primary-button>
</div>
</form>
</x-modal>
@endpush

View File

@ -21,7 +21,8 @@ class="p-6"
<div class="mt-6">
<x-alert-warning>
Vito Agent is only works if you are running your Vito instance on a cloud not local!
Vito Agent is only works if you are running your Vito instance on a cloud not local! Consider
installing remote-monitor instead.
</x-alert-warning>
</div>

View File

@ -1,4 +1,5 @@
<x-icon-button
:disabled="isset($disabled) ? $disabled : false"
data-tooltip="Disable Service"
class="cursor-pointer"
href="{{ route('servers.services.disable', ['server' => $server, 'service' => $service]) }}"

View File

@ -1,5 +1,5 @@
<x-icon-button
:disabled="$service->status != \App\Enums\ServiceStatus::DISABLED"
:disabled="isset($disabled) ? $disabled : $service->status != \App\Enums\ServiceStatus::DISABLED"
data-tooltip="Enable Service"
class="cursor-pointer"
href="{{ route('servers.services.enable', ['server' => $server, 'service' => $service]) }}"

View File

@ -1,4 +1,5 @@
<x-icon-button
:disabled="isset($disabled) ? $disabled : false"
data-tooltip="Restart Service"
class="cursor-pointer"
href="{{ route('servers.services.restart', ['server' => $server, 'service' => $service]) }}"

View File

@ -1,5 +1,5 @@
<x-icon-button
:disabled="$service->status != \App\Enums\ServiceStatus::STOPPED"
:disabled="isset($disabled) ? $disabled : $service->status != \App\Enums\ServiceStatus::STOPPED"
data-tooltip="Start Service"
class="cursor-pointer"
href="{{ route('servers.services.start', ['server' => $server, 'service' => $service]) }}"

View File

@ -1,6 +1,6 @@
<x-icon-button
:disabled="isset($disabled) ? $disabled : $service->status != \App\Enums\ServiceStatus::READY"
data-tooltip="Stop Service"
:disabled="$service->status != \App\Enums\ServiceStatus::READY"
class="cursor-pointer"
href="{{ route('servers.services.stop', ['server' => $server, 'service' => $service]) }}"
>

View File

@ -1,5 +1,5 @@
<x-site-layout :site="$site">
<x-slot name="pageTitle">{{ __("Logs") }}</x-slot>
<x-slot name="pageTitle">{{ $pageTitle }}</x-slot>
@include("server-logs.partials.logs-list", ["server" => $site->server, "site" => $site])
@include("server-logs.partials.logs-list-live", ["server" => $site->server, "site" => $site])
</x-site-layout>

View File

@ -11,5 +11,5 @@
@endif
</x-live>
@include("server-logs.partials.logs-list", ["server" => $site->server, "site" => $site])
@include("server-logs.partials.logs-list-live", ["server" => $site->server, "site" => $site])
</x-site-layout>

View File

@ -150,6 +150,9 @@
// logs
Route::prefix('/{server}/logs')->group(function () {
Route::get('/', [ServerLogController::class, 'index'])->name('servers.logs');
Route::get('/remote', [ServerLogController::class, 'remote'])->name('servers.logs.remote');
Route::post('/remote', [ServerLogController::class, 'store'])->name('servers.logs.remote.store');
Route::delete('/remote/{serverLog}', [ServerLogController::class, 'destroy'])->name('servers.logs.remote.destroy');
Route::get('/{serverLog}', [ServerLogController::class, 'show'])->name('servers.logs.show');
});
});

View File

@ -23,4 +23,37 @@ public function test_see_logs()
->assertSuccessful()
->assertSeeText($log->type);
}
public function test_see_logs_remote()
{
$this->actingAs($this->user);
/** @var ServerLog $log */
$log = ServerLog::factory()->create([
'server_id' => $this->server->id,
'is_remote' => true,
'type' => 'remote',
'name' => 'see-remote-log',
]);
$this->get(route('servers.logs.remote', $this->server))
->assertSuccessful()
->assertSeeText('see-remote-log');
}
public function test_create_remote_log()
{
$this->actingAs($this->user);
$this->post(route('servers.logs.remote.store', [
'server' => $this->server->id,
]), [
'path' => 'test-path',
])->assertOk();
$this->assertDatabaseHas('server_logs', [
'is_remote' => true,
'name' => 'test-path',
]);
}
}

View File

@ -282,6 +282,6 @@ public function test_see_logs(): void
'site' => $this->site,
]))
->assertSuccessful()
->assertSee('Logs');
->assertSee('Vito Logs');
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace Tests\Unit\Commands;
use App\Enums\ServiceStatus;
use App\Facades\SSH;
use App\Models\Service;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class GetMetricsCommandTest extends TestCase
{
use RefreshDatabase;
public function test_get_metrics(): void
{
SSH::fake(<<<'EOF'
load:1
memory_total:1
memory_used:1
memory_free:1
disk_total:1
disk_used:1
disk_free:1
EOF);
Service::factory()->create([
'server_id' => $this->server->id,
'name' => 'remote-monitor',
'type' => 'monitoring',
'type_data' => [
'data_retention' => 7,
],
'version' => 'latest',
'status' => ServiceStatus::READY,
]);
$this->artisan('metrics:get')
->expectsOutput('Checked 1 metrics');
$this->assertDatabaseHas('metrics', [
'server_id' => $this->server->id,
'load' => 1,
'memory_total' => 1,
'memory_used' => 1,
'memory_free' => 1,
'disk_total' => 1,
'disk_used' => 1,
'disk_free' => 1,
]);
}
}