├── src └── Recorders │ ├── server-stats-linux.sh │ └── RemoteServers.php ├── composer.json ├── LICENSE └── README.md /src/Recorders/server-stats-linux.sh: -------------------------------------------------------------------------------- 1 | cat /proc/meminfo | grep MemTotal | grep -E -o '[0-9]+' 2 | cat /proc/meminfo | grep MemAvailable | grep -E -o '[0-9]+' 3 | top -bn1 | grep -E '^(%Cpu|CPU)' | awk '{ print $2 + $4 }' 4 | df / | awk 'NR==2 {print $3 "\n" $4 }' -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wrklst/pulse-remote-server", 3 | "description": "A Laravel Pulse Recorder for Remote Servers", 4 | "keywords": ["laravel", "remote", "server"], 5 | "type": "library", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Tobias Vielmetter-Diekmann", 10 | "email": "tobias@wrklst.art" 11 | } 12 | ], 13 | "require": { 14 | "php": "^8.1", 15 | "illuminate/support": "*", 16 | "laravel/pulse": "^1.0.0@beta" 17 | }, 18 | "autoload": { 19 | "psr-4": { 20 | "WrkLst\\Pulse\\RemoteServer\\": "src/" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 WrkLst 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Remote Server for Laravel Pulse 2 | 3 | Enhance your server stats by adding remote Linux servers to the mix. This feature is designed to incorporate remote servers to Laravel Pulse that do not execute PHP, such as database or cache servers. Servers running PHP are recommended to install their own instance of [Laravel Pulse](https://pulse.laravel.com) instead. 4 | 5 | ## Installation 6 | 7 | Begin by installing the package via Composer: 8 | 9 | ```shell 10 | composer require wrklst/pulse-remote-server 11 | ``` 12 | 13 | ## Authentication 14 | 15 | Ensure SSH key authentication is set up for accessing the remote server. The Remote Server package assumes the remote server is running Linux. It is compatible with both Mac and Linux servers for your local Larvel Pulse installation. 16 | 17 | ## Register the Recorder 18 | 19 | In your `pulse.php` configuration file, incorporate the `\WrkLst\Pulse\RemoteServer\Recorders\RemoteServers` class with the desired settings: 20 | 21 | ```php 22 | return [ 23 | // Other configurations... 24 | 25 | 'recorders' => [ 26 | \WrkLst\Pulse\RemoteServer\Recorders\RemoteServers::class => [ 27 | [ 28 | 'server_name' => "database-server-1", 29 | 'server_ssh' => "ssh forge@1.2.3.4", 30 | 'query_interval' => 15, 31 | 'directories' => explode(':', env('PULSE_SERVER_DIRECTORIES', '/')), 32 | ], 33 | [ 34 | 'server_name' => "cache-server-1", 35 | 'server_ssh' => "ssh forge@1.2.3.5", 36 | 'query_interval' => 15, 37 | 'directories' => explode(':', env('PULSE_SERVER_DIRECTORIES', '/')), 38 | ] 39 | ], 40 | ] 41 | ] 42 | ``` 43 | 44 | Don't forget to run [the `pulse:check` command](https://laravel.com/docs/10.x/pulse#capturing-entries) to start recording. 45 | 46 | ## Configuration Notes 47 | 48 | - `server_name`: Specify the name of the server as it should appear in the server stats. 49 | - `server_ssh`: Enter the SSH command to connect to the server (`ssh user@ipaddress`). You can also include options like `-p 2222` for non-standard ports. 50 | - `query_interval`: Define the interval for querying the remote server's stats, in seconds. 51 | - `directories`: Specify the directories to check for used and available disk capacity. By default, this is set to "/", but you can add multiple directories or change the directory. Note that altering this configuration might impact query performance. For specialized setups, consider forking the repository and adjusting the shell script accordingly. 52 | 53 | And that's all there is to it! -------------------------------------------------------------------------------- /src/Recorders/RemoteServers.php: -------------------------------------------------------------------------------- 1 | config->get('pulse.recorders.' . self::class); 39 | 40 | if(isset($servers['server_name'])) { 41 | $this->recordServer($event, $servers); 42 | } else { 43 | foreach ($servers as $serverConfig) { 44 | $this->recordServer($event, $serverConfig); 45 | } 46 | } 47 | } 48 | 49 | public function recordServer(SharedBeat $event, array $serverConfig) 50 | { 51 | $query_interval = (int)($serverConfig['query_interval'] ?? 15); 52 | 53 | if ($event->time->second % $query_interval !== 0) { 54 | return; 55 | } 56 | 57 | $remote_ssh = $serverConfig['server_ssh']; 58 | $server = $serverConfig['server_name']; 59 | 60 | $slug = Str::slug($server); 61 | 62 | $dir = dirname(__FILE__); 63 | 64 | $remoteServerStats = match (PHP_OS_FAMILY) { 65 | 'Darwin' => (`$remote_ssh 'bash -s' < $dir/server-stats-linux.sh`), 66 | 'Linux' => (`$remote_ssh 'bash -s' < $dir/server-stats-linux.sh`), 67 | default => throw new RuntimeException('The pulse:check command does not currently support ' . PHP_OS_FAMILY), 68 | }; 69 | 70 | $remoteServerStats = explode("\n", $remoteServerStats); 71 | 72 | /* 73 | cat /proc/meminfo | grep MemTotal | grep -E -o '[0-9]+' 74 | [0] 19952552 75 | cat /proc/meminfo | grep MemAvailable | grep -E -o '[0-9]+' 76 | [1] 5534304 77 | top -bn1 | grep -E '^(%Cpu|CPU)' | awk '{ print $2 + $4 }' 78 | [2] 18 79 | df / | awk 'NR==2 {print $3 "\n" $4 }' 80 | [3] 34218600 81 | [4] 473695292 82 | */ 83 | 84 | $memoryTotal = (int)($remoteServerStats[0] / 1024); 85 | $memoryUsed = $memoryTotal - (int)($remoteServerStats[1] / 1024); 86 | $cpu = (int) $remoteServerStats[2]; 87 | 88 | $storageDirectories = $serverConfig['directories']; 89 | 90 | if (count($storageDirectories) == 1 && $storageDirectories[0] == "/") { 91 | $storage = [collect([ 92 | 'directory' => "/", 93 | 'total' => (round(((int)($remoteServerStats[3]) + (int)($remoteServerStats[4])) / 1024)), // MB 94 | 'used' => (round((int)($remoteServerStats[3]) / 1024)), // MB 95 | ])]; 96 | } else { 97 | $storage = collect($storageDirectories) 98 | ->map(function (string $directory) use ($remote_ssh, $remoteServerStats) { 99 | if ($directory == "/") { 100 | $storageTotal = (int)($remoteServerStats[3]) + (int)($remoteServerStats[4]); // used and availble 101 | $storageUsed = (int)($remoteServerStats[3]); // used 102 | } else { 103 | $storage = match (PHP_OS_FAMILY) { 104 | 'Darwin' => (`$remote_ssh 'df $directory' | awk 'NR==2 {print $3 "\n" $4 }'`), 105 | 'Linux' => (`$remote_ssh 'df $directory' | awk 'NR==2 {print $3 "\n" $4 }'`), 106 | default => throw new RuntimeException('The pulse:check command does not currently support ' . PHP_OS_FAMILY), 107 | }; 108 | $storage = explode("\n", $storage); // break in lines 109 | $storageTotal = (int)($storage[0]) + (int)($storage[1]); // used and availble 110 | $storageUsed = (int)($storage[0]); // used 111 | } 112 | 113 | return [ 114 | 'directory' => $directory, 115 | 'total' => (round($storageTotal / 1024)), // MB 116 | 'used' => (round($storageUsed / 1024)), // MB 117 | ]; 118 | }) 119 | ->all(); 120 | } 121 | 122 | $this->pulse->record('cpu', $slug, $cpu, $event->time)->avg()->onlyBuckets(); 123 | $this->pulse->record('memory', $slug, $memoryUsed, $event->time)->avg()->onlyBuckets(); 124 | $this->pulse->set('system', $slug, json_encode([ 125 | 'name' => $server, 126 | 'cpu' => $cpu, 127 | 'memory_used' => $memoryUsed, 128 | 'memory_total' => $memoryTotal, 129 | 'storage' => $storage, 130 | ], flags: JSON_THROW_ON_ERROR), $event->time); 131 | } 132 | } 133 | --------------------------------------------------------------------------------