├── .github ├── FUNDING.yml └── workflows │ └── tests.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── composer.json ├── config └── monitoring.php ├── database ├── factories │ ├── MonitoringAlertFactory.php │ └── MonitoringRecordFactory.php └── migrations │ ├── 2022_01_04_202625_create_monitoring_records_table.php │ └── 2022_01_04_202626_create_monitoring_alerts_table.php ├── demo.png ├── demo2.png ├── package-lock.json ├── package.json ├── phpunit.xml ├── public ├── css │ └── app.css ├── js │ ├── app.js │ └── app.js.LICENSE.txt └── mix-manifest.json ├── resources ├── css │ └── app.css ├── js │ ├── app.js │ └── components │ │ ├── Alert.vue │ │ ├── App.vue │ │ ├── Button.vue │ │ ├── DialogForm.vue │ │ ├── DialogModal.vue │ │ ├── IconButton.vue │ │ ├── Input.vue │ │ ├── InputError.vue │ │ ├── Label.vue │ │ ├── Loader.vue │ │ ├── Modal.vue │ │ ├── SecondaryButton.vue │ │ ├── Select.vue │ │ └── Usage.vue └── views │ ├── emails │ └── resource-usage.blade.php │ └── index.blade.php ├── scripts ├── cpu.sh ├── disk.sh └── memory.sh ├── src ├── Actions │ ├── CheckForAlerts.php │ ├── CreateAlert.php │ └── RecordUsage.php ├── Channels │ ├── BaseChannel.php │ ├── Channel.php │ ├── Discord.php │ ├── Email.php │ └── Slack.php ├── Commands │ ├── PublishCommand.php │ ├── PurgeCommand.php │ └── RecordCommand.php ├── Exceptions │ └── OSIsNotSupported.php ├── Facades │ └── Monitoring.php ├── HasAlerts.php ├── HasRecords.php ├── Http │ ├── Controller.php │ ├── MonitoringAlertController.php │ ├── MonitoringController.php │ ├── MonitoringRecordController.php │ └── routes.php ├── Mail │ └── ResourceUsageMail.php ├── Models │ ├── MonitoringAlert.php │ └── MonitoringRecord.php ├── Monitoring.php ├── MonitoringServiceProvider.php └── System │ ├── CPU.php │ ├── Disk.php │ ├── Memory.php │ └── SystemResource.php ├── tailwind.config.js ├── tests ├── Actions │ ├── CheckForAlertsTest.php │ ├── CreateAlertTest.php │ └── RecordUsageTest.php ├── Channels │ ├── DiscordTest.php │ ├── EmailTest.php │ └── SlackTest.php ├── Commands │ ├── PurgeCommandTest.php │ └── RecordCommandTest.php ├── Http │ ├── MonitoringAlertControllerTest.php │ └── MonitoringRecordControllerTest.php └── TestCase.php ├── webpack.config.js └── webpack.mix.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [saeedvaziry] 4 | ko_fi: saeedvaziry 5 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: '0 0 * * *' 8 | 9 | jobs: 10 | tests: 11 | runs-on: ubuntu-20.04 12 | 13 | services: 14 | mysql: 15 | image: mysql 16 | env: 17 | MYSQL_DATABASE: test_db 18 | MYSQL_USER: user 19 | MYSQL_PASSWORD: password 20 | MYSQL_ROOT_PASSWORD: rootpassword 21 | ports: 22 | - 3306:3306 23 | options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 24 | 25 | strategy: 26 | fail-fast: true 27 | matrix: 28 | php: [ 8.1, 8.2 ] 29 | laravel: [ ^8.0, ^9.0, ^10.0 ] 30 | 31 | steps: 32 | - uses: actions/checkout@v2 33 | 34 | - name: Setup PHP 35 | uses: shivammathur/setup-php@v2 36 | with: 37 | php-version: ${{ matrix.php }} 38 | 39 | - name: Cache Composer packages 40 | id: composer-cache 41 | uses: actions/cache@v2 42 | with: 43 | path: vendor 44 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} 45 | restore-keys: | 46 | ${{ runner.os }}-php- 47 | - name: Install dependencies 48 | if: steps.composer-cache.outputs.cache-hit != 'true' 49 | run: composer install --prefer-dist --no-progress --no-suggest 50 | 51 | - name: Run test suite 52 | run: vendor/bin/phpunit --verbose 53 | env: 54 | DB_HOST: 127.0.0.1 55 | DB_DATABASE: test_db 56 | DB_USERNAME: user 57 | DB_PASSWORD: password 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /node_modules 3 | 4 | .phpunit.result.cache 5 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Saeed Vaziry 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 13 | all 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 21 | THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Monitoring 2 | 3 | [![](https://img.shields.io/packagist/v/saeedvaziry/laravel-monitoring.svg?style=flat-square)](https://packagist.org/packages/saeedvaziry/laravel-monitoring) 4 | ![](https://github.com/saeedvaziry/laravel-monitoring/workflows/tests/badge.svg) 5 | 6 | Monitor your Laravel applications server with a beautiful dashboard and get notified if anything gets wrong! 7 | 8 | ![](demo.png) 9 | ![](demo2.png) 10 | 11 | ## Supported OS 12 | 13 | This package works only on Linux servers. 14 | 15 | ## Installation 16 | 17 | **1)** Install the latest version from composer 18 | 19 | For PHP >= 8.1 20 | 21 | ```shell 22 | composer require saeedvaziry/laravel-monitoring 23 | ``` 24 | 25 | For PHP <= 8.0 26 | 27 | ```shell 28 | composer require saeedvaziry/laravel-monitoring "1.4.2" 29 | ``` 30 | 31 | **2)** Publish vendors 32 | 33 | ```shell 34 | php artisan monitoring:publish 35 | ``` 36 | 37 | **3)** Run migrations 38 | 39 | ```shell 40 | php artisan migrate 41 | ``` 42 | 43 | **4)** Set up a cronjob to collect data 44 | 45 | ```shell 46 | * * * * * cd /path-to-your-project && php artisan monitoring:record 47 | ``` 48 | 49 | **5)** Visit `/monitoring` to see the statistics. 50 | 51 | ## Configuration 52 | 53 | You can find the configuration at `config/monitoring.php`. 54 | 55 | | Key | Description | 56 | |-----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 57 | | `instance_name` | This is your current server's name, And the data will be collected under this name. | 58 | | `routes` | You can change the URL prefix of the monitoring dashboard. Also, You can protect the route by applying middlewares to it. | 59 | | `models` | If you want to customize the models, define yours and update this config. | 60 | | `chart_colors` | Chart colors are customizable by this config. | 61 | | `notifications` | Currently, we support `Slack` and `Email` channels for notifications. However, You can add your custom channels. To add a custom channel, Create a class and implement it by `SaeedVaziry\Monitoring\Channels\Channel` and then add the class to `channels` under the `notifications` item. | 62 | 63 | 64 | ## Multi-Server support 65 | 66 | Sometimes your source code is deployed to multiple servers. 67 | 68 | For example, You have multiple webservers with a load balancer and another server for your Backoffice. 69 | 70 | In this case, you just need to set a unique name for `MONITORING_INSTANCE_NAME` environment variable on each server, Of course, assuming that you have one database in common with all the servers that you want to monitor. 71 | 72 | The result will be similar to the Demo picture. 73 | 74 | ## Command 75 | 76 | You can use `php artisan monitoring:record` command to collect the data manually. 77 | 78 | ## Facade 79 | 80 | Add the bellow line to your `config/app.php` file, Under the `allias`: 81 | 82 | ```php 83 | 'aliases' => [ 84 | ... 85 | 'Monitoring' => \SaeedVaziry\Monitoring\Facades\Monitoring::class 86 | ... 87 | ]; 88 | ``` 89 | 90 | With this Facade you can access the server's resource usages. 91 | 92 | Example usages: 93 | 94 | ```php 95 | Monitoring::cpu()->usage(); // returns CPU usage 96 | Monitoring::memory()->usage(); // returns Memory usage 97 | Monitoring::disk()->usage(); // returns Disk usage 98 | ``` 99 | 100 | ## Purge Records 101 | 102 | Without purging, the `monitoring_records` table can accumulate records very quickly. To mitigate this, you should schedule the monitoring:purge Artisan command to run daily or any time you wish. 103 | You can also, Set the `purge_before` configuration at `config/monitoring.php`. 104 | 105 | ```php 106 | $schedule->command('monitoring:purge')->daily(); 107 | ``` 108 | 109 | ## Contributing 110 | 111 | Please feel free to submit an issue or open a PR. 112 | 113 | ## Credits 114 | 115 | * [Laravel](https://laravel.com/) 116 | * [Vue.js](https://vuejs.org/) 117 | * [Tailwindcss](https://tailwindcss.com/) 118 | * [Fontawesome Icons](https://fontawesome.com/) 119 | 120 | ## License 121 | 122 | Laravel Monitoring is open-sourced software and licensed under the MIT License (MIT). 123 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "saeedvaziry/laravel-monitoring", 3 | "description": "Monitor Laravel Hosted Servers", 4 | "type": "library", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Saeed Vaziry", 9 | "email": "mr.saeedvaziry@gmail.com" 10 | } 11 | ], 12 | "scripts": { 13 | "test": "vendor/bin/phpunit" 14 | }, 15 | "require": { 16 | "php": "^7.1|^8.0", 17 | "laravel/framework": "^6.0|^7.0|^8.0|^9.0|^10.0" 18 | }, 19 | "require-dev": { 20 | "phpunit/phpunit": "^8.0|^9.0", 21 | "orchestra/testbench": "^4.0|^5.0|^6.0", 22 | "guzzlehttp/guzzle": "^6.5.5|^7.0" 23 | }, 24 | "autoload": { 25 | "psr-4": { 26 | "SaeedVaziry\\Monitoring\\": "src/", 27 | "SaeedVaziry\\Monitoring\\Database\\Factories\\": "database/factories/" 28 | } 29 | }, 30 | "autoload-dev": { 31 | "psr-4": { 32 | "SaeedVaziry\\Monitoring\\Tests\\": "tests/" 33 | } 34 | }, 35 | "extra": { 36 | "laravel": { 37 | "providers": [ 38 | "SaeedVaziry\\Monitoring\\MonitoringServiceProvider" 39 | ], 40 | "aliases": { 41 | "Monitoring": "SaeedVaziry\\Monitoring\\Monitoring" 42 | } 43 | } 44 | }, 45 | "minimum-stability": "dev", 46 | "prefer-stable": true 47 | } 48 | -------------------------------------------------------------------------------- /config/monitoring.php: -------------------------------------------------------------------------------- 1 | env('MONITORING_INSTANCE_NAME', env('APP_NAME')), 11 | 12 | /* 13 | * Route configurations 14 | */ 15 | 'routes' => [ 16 | 'prefix' => 'monitoring', 17 | 'middlewares' => ['web'], 18 | ], 19 | 20 | /* 21 | * Models 22 | */ 23 | 'models' => [ 24 | 'monitoring_record' => \SaeedVaziry\Monitoring\Models\MonitoringRecord::class, 25 | 'monitoring_alert' => \SaeedVaziry\Monitoring\Models\MonitoringAlert::class, 26 | ], 27 | 28 | /* 29 | * Chart colors 30 | */ 31 | 'chart_colors' => [ 32 | 'cpu' => [ 33 | 'border_color' => '#4f46e5', 34 | 'background_color' => '#a5b4fc', 35 | ], 36 | 'memory' => [ 37 | 'border_color' => '#e11d48', 38 | 'background_color' => '#fda4af', 39 | ], 40 | 'disk' => [ 41 | 'border_color' => '#9333ea', 42 | 'background_color' => '#d8b4fe', 43 | ], 44 | ], 45 | 46 | /* 47 | * Supported channels are Email and Slack, but you can add 48 | * your own channels class to the `channels` array. 49 | * Make sure that you implementing `\SaeedVaziry\Monitoring\Channels\Channel` 50 | * interface in your custom channels 51 | */ 52 | 'notifications' => [ 53 | 'channels' => [ 54 | \SaeedVaziry\Monitoring\Channels\Email::class, 55 | // \SaeedVaziry\Monitoring\Channels\Slack::class, 56 | // \SaeedVaziry\Monitoring\Channels\Discord::class, 57 | ], 58 | 59 | /* 60 | * Fill it if you want the Email channel 61 | */ 62 | 'email' => env('MONITORING_EMAIL_ADDRESS'), 63 | 64 | /* 65 | * Fill it if you want the Slack channel 66 | */ 67 | 'slack_webhook_url' => env('MONITORING_SLACK_WEBHOOK_URL'), 68 | 69 | /* 70 | * Fill it if you want the Discord channel 71 | */ 72 | 'discord_webhook_url' => env('MONITORING_DISCORD_WEBHOOK_URL'), 73 | ], 74 | 75 | /* 76 | * You can enable or disable migrations here 77 | */ 78 | 'migrations' => true, 79 | 80 | /* 81 | * Purge recorded data 82 | * Supports PHP strtotime options like: '-1 day', '-2 hours', ... 83 | */ 84 | 'purge_before' => '-1 day', 85 | ]; 86 | -------------------------------------------------------------------------------- /database/factories/MonitoringAlertFactory.php: -------------------------------------------------------------------------------- 1 | config('monitoring.instance_name'), 24 | 'cpu' => 40, 25 | 'memory' => 30, 26 | 'disk' => 20, 27 | 'occurred' => 0, 28 | ]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /database/factories/MonitoringRecordFactory.php: -------------------------------------------------------------------------------- 1 | config('monitoring.instance_name'), 24 | 'cpu' => 10, 25 | 'memory' => 20, 26 | 'disk' => 30, 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /database/migrations/2022_01_04_202625_create_monitoring_records_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->string('instance_name')->index(); 19 | $table->float('cpu')->nullable(); 20 | $table->float('memory')->nullable(); 21 | $table->float('disk')->nullable(); 22 | $table->timestamps(); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | * 29 | * @return void 30 | */ 31 | public function down() 32 | { 33 | Schema::dropIfExists('monitoring_records'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /database/migrations/2022_01_04_202626_create_monitoring_alerts_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->string('instance_name')->index(); 19 | $table->float('cpu')->nullable(); 20 | $table->float('memory')->nullable(); 21 | $table->float('disk')->nullable(); 22 | $table->unsignedBigInteger('occurred')->default(0); 23 | $table->timestamps(); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | * 30 | * @return void 31 | */ 32 | public function down() 33 | { 34 | Schema::dropIfExists('monitoring_alerts'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saeedvaziry/laravel-monitoring/cf8bc7c57235b5dac407e0c0de18dc8de0667c25/demo.png -------------------------------------------------------------------------------- /demo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saeedvaziry/laravel-monitoring/cf8bc7c57235b5dac407e0c0de18dc8de0667c25/demo2.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "npm run development", 5 | "development": "mix", 6 | "watch": "mix watch", 7 | "watch-poll": "mix watch -- --watch-options-poll=1000", 8 | "hot": "mix watch --hot", 9 | "prod": "npm run production", 10 | "production": "mix --production" 11 | }, 12 | "devDependencies": { 13 | "@vue/compiler-sfc": "^3.0.5", 14 | "axios": "^0.21", 15 | "chart.js": "^2.9.4", 16 | "laravel-mix": "^6.0.6", 17 | "lodash": "^4.17.19", 18 | "moment": "^2.10.6", 19 | "moment-timezone": "^0.5.21", 20 | "postcss-import": "^14.0.2", 21 | "resolve-url-loader": "^3.1.2", 22 | "tailwindcss": "^3.0.10", 23 | "vue": "^3.0.5", 24 | "vue-chartjs": "^3.5.1", 25 | "vue-loader": "^16.1.2" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./tests 6 | 7 | 8 | 9 | 10 | ./app 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /public/css/app.css: -------------------------------------------------------------------------------- 1 | /*! tailwindcss v3.0.10 | MIT License | https://tailwindcss.com*/*,:after,:before{border:0 solid;box-sizing:border-box}:after,:before{--tw-content:""}html{-webkit-text-size-adjust:100%;font-family:Nunito,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4}body{line-height:inherit;margin:0}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{color:inherit;font-family:inherit;font-size:100%;line-height:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{color:#94a3b8;opacity:1}input:-ms-input-placeholder,textarea:-ms-input-placeholder{color:#94a3b8;opacity:1}input::placeholder,textarea::placeholder{color:#94a3b8;opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[hidden]{display:none}*,:after,:before{--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-transform:translateX(var(--tw-translate-x)) translateY(var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));--tw-border-opacity:1;--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;border-color:rgb(226 232 240/var(--tw-border-opacity))}.fixed{position:fixed}.absolute{position:absolute}.inset-0{bottom:0;left:0;right:0;top:0}.z-50{z-index:50}.col-span-6{grid-column:span 6/span 6}.col-span-1{grid-column:span 1/span 1}.col-span-3{grid-column:span 3/span 3}.mx-auto{margin-left:auto;margin-right:auto}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.ml-2{margin-left:.5rem}.mr-2{margin-right:.5rem}.mb-10{margin-bottom:2.5rem}.mr-5{margin-right:1.25rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mb-6{margin-bottom:1.5rem}.block{display:block}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.hidden{display:none}.h-4{height:1rem}.h-8{height:2rem}.h-full{height:100%}.min-h-screen{min-height:100vh}.w-full{width:100%}.w-4{width:1rem}.w-8{width:2rem}.w-10{width:2.5rem}.w-max{width:-webkit-max-content;width:-moz-max-content;width:max-content}.min-w-max{min-width:-webkit-max-content;min-width:-moz-max-content;min-width:max-content}.max-w-5xl{max-width:64rem}.translate-y-4{--tw-translate-y:1rem;transform:var(--tw-transform)}.translate-y-0{--tw-translate-y:0px}.transform,.translate-y-0{transform:var(--tw-transform)}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-3{gap:.75rem}.gap-6{gap:1.5rem}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.rounded-md{border-radius:.375rem}.border{border-width:1px}.border-transparent{border-color:transparent}.border-gray-300{--tw-border-opacity:1;border-color:rgb(203 213 225/var(--tw-border-opacity))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(241 245 249/var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-indigo-600{--tw-bg-opacity:1;background-color:rgb(79 70 229/var(--tw-bg-opacity))}.bg-indigo-700{--tw-bg-opacity:1;background-color:rgb(67 56 202/var(--tw-bg-opacity))}.bg-gray-900{--tw-bg-opacity:1;background-color:rgb(15 23 42/var(--tw-bg-opacity))}.fill-current{fill:currentColor}.p-3{padding:.75rem}.p-2{padding:.5rem}.py-7{padding-bottom:1.75rem;padding-top:1.75rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-4{padding-left:1rem;padding-right:1rem}.py-1{padding-bottom:.25rem;padding-top:.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-4{padding-bottom:1rem;padding-top:1rem}.py-2{padding-bottom:.5rem;padding-top:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.py-6{padding-bottom:1.5rem;padding-top:1.5rem}.text-right{text-align:right}.text-2xl{font-size:1.5rem;line-height:2rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.font-bold{font-weight:700}.font-semibold{font-weight:600}.font-medium{font-weight:500}.capitalize{text-transform:capitalize}.text-gray-700{--tw-text-opacity:1;color:rgb(51 65 85/var(--tw-text-opacity))}.text-indigo-500{--tw-text-opacity:1;color:rgb(99 102 241/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(100 116 139/var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.text-indigo-600{--tw-text-opacity:1;color:rgb(79 70 229/var(--tw-text-opacity))}.text-red-600{--tw-text-opacity:1;color:rgb(220 38 38/var(--tw-text-opacity))}.text-green-600{--tw-text-opacity:1;color:rgb(22 163 74/var(--tw-text-opacity))}.text-yellow-600{--tw-text-opacity:1;color:rgb(202 138 4/var(--tw-text-opacity))}.opacity-25{opacity:.25}.opacity-50{opacity:.5}.opacity-0{opacity:0}.opacity-100{opacity:1}.opacity-75{opacity:.75}.shadow{--tw-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1);--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-sm{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.shadow-xl{--tw-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1);--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.transition{transition-duration:.15s;transition-property:color,background-color,border-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-text-decoration-color,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-text-decoration-color,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-all{transition-duration:.15s;transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1)}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.ease-in{transition-timing-function:cubic-bezier(.4,0,1,1)}.hover\:bg-indigo-700:hover{--tw-bg-opacity:1;background-color:rgb(67 56 202/var(--tw-bg-opacity))}.hover\:text-gray-500:hover{--tw-text-opacity:1;color:rgb(100 116 139/var(--tw-text-opacity))}.focus\:border-indigo-700:focus{--tw-border-opacity:1;border-color:rgb(67 56 202/var(--tw-border-opacity))}.focus\:border-gray-400:focus{--tw-border-opacity:1;border-color:rgb(148 163 184/var(--tw-border-opacity))}.focus\:border-indigo-300:focus{--tw-border-opacity:1;border-color:rgb(165 180 252/var(--tw-border-opacity))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-indigo-200:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(199 210 254/var(--tw-ring-opacity))}.focus\:ring-indigo-300:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(165 180 252/var(--tw-ring-opacity))}.focus\:ring-opacity-50:focus{--tw-ring-opacity:0.5}.active\:bg-indigo-700:active{--tw-bg-opacity:1;background-color:rgb(67 56 202/var(--tw-bg-opacity))}.disabled\:bg-gray-100:disabled{--tw-bg-opacity:1;background-color:rgb(241 245 249/var(--tw-bg-opacity))}.disabled\:opacity-25:disabled{opacity:.25}@media (prefers-color-scheme:dark){.dark\:border-gray-700{--tw-border-opacity:1;border-color:rgb(51 65 85/var(--tw-border-opacity))}.dark\:border-gray-800{--tw-border-opacity:1;border-color:rgb(30 41 59/var(--tw-border-opacity))}.dark\:border-transparent{border-color:transparent}.dark\:bg-gray-900{--tw-bg-opacity:1;background-color:rgb(15 23 42/var(--tw-bg-opacity))}.dark\:bg-gray-800{--tw-bg-opacity:1;background-color:rgb(30 41 59/var(--tw-bg-opacity))}.dark\:bg-gray-700{--tw-bg-opacity:1;background-color:rgb(51 65 85/var(--tw-bg-opacity))}.dark\:text-gray-300{--tw-text-opacity:1;color:rgb(203 213 225/var(--tw-text-opacity))}.dark\:disabled\:bg-gray-700:disabled{--tw-bg-opacity:1;background-color:rgb(51 65 85/var(--tw-bg-opacity))}}@media (min-width:640px){.sm\:mx-auto{margin-left:auto;margin-right:auto}.sm\:w-full{width:100%}.sm\:max-w-sm{max-width:24rem}.sm\:max-w-md{max-width:28rem}.sm\:max-w-lg{max-width:32rem}.sm\:max-w-xl{max-width:36rem}.sm\:max-w-2xl{max-width:42rem}.sm\:translate-y-0{--tw-translate-y:0px}.sm\:scale-95,.sm\:translate-y-0{transform:var(--tw-transform)}.sm\:scale-95{--tw-scale-x:.95;--tw-scale-y:.95}.sm\:scale-100{--tw-scale-x:1;--tw-scale-y:1;transform:var(--tw-transform)}.sm\:px-0{padding-left:0;padding-right:0}} 2 | -------------------------------------------------------------------------------- /public/js/app.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | //! Copyright (c) JS Foundation and other contributors 2 | 3 | //! github.com/moment/moment-timezone 4 | 5 | //! license : MIT 6 | 7 | //! moment-timezone.js 8 | 9 | //! moment.js 10 | 11 | //! moment.js locale configuration 12 | 13 | //! version : 0.5.34 14 | -------------------------------------------------------------------------------- /public/mix-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "/js/app.js": "/js/app.js?id=74f6221d994c8a941a42", 3 | "/css/app.css": "/css/app.css?id=72db21d04a6f1275979b" 4 | } 5 | -------------------------------------------------------------------------------- /resources/css/app.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /resources/js/app.js: -------------------------------------------------------------------------------- 1 | import {createApp} from 'vue'; 2 | import App from "@/components/App"; 3 | 4 | createApp(App).mount('#app') 5 | -------------------------------------------------------------------------------- /resources/js/components/Alert.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 103 | -------------------------------------------------------------------------------- /resources/js/components/App.vue: -------------------------------------------------------------------------------- 1 | 63 | 64 | 161 | -------------------------------------------------------------------------------- /resources/js/components/Button.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 22 | -------------------------------------------------------------------------------- /resources/js/components/DialogForm.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 56 | -------------------------------------------------------------------------------- /resources/js/components/DialogModal.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 53 | -------------------------------------------------------------------------------- /resources/js/components/IconButton.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 19 | -------------------------------------------------------------------------------- /resources/js/components/Input.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 26 | 27 | -------------------------------------------------------------------------------- /resources/js/components/InputError.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 16 | -------------------------------------------------------------------------------- /resources/js/components/Label.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 17 | -------------------------------------------------------------------------------- /resources/js/components/Loader.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 29 | -------------------------------------------------------------------------------- /resources/js/components/Modal.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 101 | -------------------------------------------------------------------------------- /resources/js/components/SecondaryButton.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 19 | -------------------------------------------------------------------------------- /resources/js/components/Select.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 22 | 23 | -------------------------------------------------------------------------------- /resources/js/components/Usage.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 20 | -------------------------------------------------------------------------------- /resources/views/emails/resource-usage.blade.php: -------------------------------------------------------------------------------- 1 | @component('mail::message') 2 | # {{ $subject }} 3 | 4 | **CPU:** {{ $record->cpu }} 5 | 6 | **Memory:** {{ $record->memory }} 7 | 8 | **Disk:** {{ $record->disk }} 9 | 10 | Regards,
11 | {{ config('app.name') }} 12 | @endcomponent 13 | -------------------------------------------------------------------------------- /resources/views/index.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ __('Monitoring') }} 8 | 9 | 10 | 15 | 16 | 17 | 18 |
19 | 20 |
21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /scripts/cpu.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo `top -b -n1 | grep "Cpu(s)" | awk '{print $2 + $4}'` 3 | -------------------------------------------------------------------------------- /scripts/disk.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo `df -lh | awk '{if ($6 == "/") { print $5 }}' | head -1 | cut -d'%' -f1` 3 | -------------------------------------------------------------------------------- /scripts/memory.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | FREE_DATA=`free -m | grep Mem` 3 | CURRENT=`echo $FREE_DATA | cut -f3 -d' '` 4 | TOTAL=`echo $FREE_DATA | cut -f2 -d' '` 5 | echo $(awk "BEGIN{print $CURRENT / $TOTAL * 100}") 6 | -------------------------------------------------------------------------------- /src/Actions/CheckForAlerts.php: -------------------------------------------------------------------------------- 1 | where('instance_name', $record->instance_name) 16 | ->where(function (Builder $query) use ($record) { 17 | $query->where('cpu', '<=', (float) $record->cpu) 18 | ->orWhere('memory', '<=', (float) $record->memory) 19 | ->orWhere('disk', '<=', (float) $record->disk); 20 | }) 21 | ->get(); 22 | foreach ($alerts as $alert) { 23 | $alert->occurred += 1; 24 | $alert->save(); 25 | foreach (config('monitoring.notifications.channels') as $channel) { 26 | app($channel)->send($record); 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Actions/CreateAlert.php: -------------------------------------------------------------------------------- 1 | validate($input); 20 | 21 | $alert = app(config('monitoring.models.monitoring_alert')) 22 | ->where('instance_name', $input['instance_name']) 23 | ->firstOrCreate([ 24 | 'instance_name' => $input['instance_name'], 25 | ], $input); 26 | $alert->update($input); 27 | 28 | return $alert; 29 | } 30 | 31 | /** 32 | * @param array $input 33 | * 34 | * @throws ValidationException 35 | * 36 | * @return void 37 | */ 38 | protected function validate(array $input) 39 | { 40 | $rules = [ 41 | 'instance_name' => 'required', 42 | ]; 43 | 44 | if (isset($input['cpu']) && !empty($input['cpu'])) { 45 | $rules['cpu'] = 'required|numeric|min:1|max:99'; 46 | } 47 | 48 | if (isset($input['memory']) && !empty($input['memory'])) { 49 | $rules['memory'] = 'required|numeric|min:1|max:99'; 50 | } 51 | 52 | if (isset($input['disk']) && !empty($input['disk'])) { 53 | $rules['disk'] = 'required|numeric|min:1|max:99'; 54 | } 55 | 56 | Validator::make($input, $rules)->validateWithBag('createAlert'); 57 | 58 | if ( 59 | (!isset($input['cpu']) && !isset($input['memory']) && !isset($input['disk'])) || 60 | (empty($input['cpu']) && empty($input['memory']) && empty($input['disk'])) 61 | ) { 62 | throw ValidationException::withMessages([ 63 | 'cpu' => __('You must fill at least one item'), 64 | 'memory' => __('You must fill at least one item'), 65 | 'disk' => __('You must fill at least one item'), 66 | ])->errorBag('createAlert'); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Actions/RecordUsage.php: -------------------------------------------------------------------------------- 1 | checkOS(); 20 | 21 | $model = config('monitoring.models.monitoring_record'); 22 | $record = new $model([ 23 | 'instance_name' => str_replace(' ', '', config('monitoring.instance_name')), 24 | 'cpu' => $resources['cpu'] ?? null, 25 | 'memory' => $resources['memory'] ?? null, 26 | 'disk' => $resources['disk'] ?? null, 27 | ]); 28 | $record->save(); 29 | 30 | return $record; 31 | } 32 | 33 | /** 34 | * @throws \Exception 35 | * 36 | * @return void 37 | */ 38 | protected function checkOS() 39 | { 40 | if (PHP_OS !== 'Linux' && app()->environment() !== 'testing') { 41 | throw new OSIsNotSupported(PHP_OS); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Channels/BaseChannel.php: -------------------------------------------------------------------------------- 1 | $record->instance_name]); 13 | } 14 | 15 | /** 16 | * @param $record 17 | * 18 | * @return string 19 | */ 20 | protected function message($record) 21 | { 22 | return __("CPU: :cpu\n Memory: :memory\n Disk: :disk", [ 23 | 'cpu' => $record->cpu, 24 | 'memory' => $record->memory, 25 | 'disk' => $record->disk, 26 | ]); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Channels/Channel.php: -------------------------------------------------------------------------------- 1 | '*'.$this->subject($record).'*'."\n".$this->message($record), 18 | ]); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Channels/Email.php: -------------------------------------------------------------------------------- 1 | send(new ResourceUsageMail( 18 | $this->subject($record), 19 | $record 20 | )); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Channels/Slack.php: -------------------------------------------------------------------------------- 1 | '*'.$this->subject($record).'*'."\n".$this->message($record), 18 | ]); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Commands/PublishCommand.php: -------------------------------------------------------------------------------- 1 | call('vendor:publish', [ 31 | '--tag' => 'monitoring-config', 32 | '--force' => $this->option('force'), 33 | ]); 34 | 35 | $this->call('vendor:publish', [ 36 | '--tag' => 'monitoring-assets', 37 | '--force' => true, 38 | ]); 39 | 40 | $this->call('vendor:publish', [ 41 | '--tag' => 'monitoring-migrations', 42 | '--force' => $this->option('force'), 43 | ]); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Commands/PurgeCommand.php: -------------------------------------------------------------------------------- 1 | where( 45 | 'created_at', 46 | '<', 47 | date('Y-m-d', strtotime(config('monitoring.purge_before', '-1 day'))) 48 | ) 49 | ->delete(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Commands/RecordCommand.php: -------------------------------------------------------------------------------- 1 | record([ 47 | 'cpu' => Monitoring::cpu()->usage(), 48 | 'memory' => Monitoring::memory()->usage(), 49 | 'disk' => Monitoring::disk()->usage(), 50 | ]), function ($record) { 51 | app(CheckForAlerts::class)->check($record); 52 | }); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Exceptions/OSIsNotSupported.php: -------------------------------------------------------------------------------- 1 | where('instance_name', $instance) 18 | ->first(); 19 | } 20 | 21 | return $alerts; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/HasRecords.php: -------------------------------------------------------------------------------- 1 | groupBy('instance_name') 14 | ->select('instance_name') 15 | ->get(); 16 | $result = []; 17 | foreach ($instances as $instance) { 18 | $result[] = $instance->instance_name; 19 | } 20 | 21 | return $result; 22 | } 23 | 24 | /** 25 | * @param array $instances 26 | * @param $duration 27 | * 28 | * @return array 29 | */ 30 | protected function getRecords(array $instances, $duration) 31 | { 32 | $records = []; 33 | foreach ($instances as $instance) { 34 | $records[$instance] = app(config('monitoring.models.monitoring_record')) 35 | ->where('instance_name', $instance) 36 | ->where('created_at', '>', $this->getDurationInTime($duration)) 37 | ->select(['created_at', 'cpu', 'memory', 'disk']) 38 | ->get() 39 | ->toArray(); 40 | } 41 | 42 | return $records; 43 | } 44 | 45 | /** 46 | * @param string|null $duration 47 | * 48 | * @return mixed 49 | */ 50 | private function getDurationInTime($duration = null) 51 | { 52 | switch ($duration) { 53 | case 'day': 54 | return now()->subDay(); 55 | default: 56 | return now()->subHour(); 57 | } 58 | } 59 | 60 | /** 61 | * @param array $instances 62 | * 63 | * @return array 64 | */ 65 | protected function getLastRecords(array $instances) 66 | { 67 | $records = []; 68 | foreach ($instances as $instance) { 69 | $records[$instance] = app(config('monitoring.models.monitoring_record')) 70 | ->where('instance_name', $instance) 71 | ->orderByDesc('id') 72 | ->first(); 73 | } 74 | 75 | return $records; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Http/Controller.php: -------------------------------------------------------------------------------- 1 | json([ 21 | 'alert' => app(CreateAlert::class)->create($request->only([ 22 | 'instance_name', 23 | 'cpu', 24 | 'memory', 25 | 'disk', 26 | ])), 27 | ]); 28 | } 29 | 30 | /** 31 | * @param $id 32 | * 33 | * @return \Illuminate\Http\JsonResponse 34 | */ 35 | public function destroy($id) 36 | { 37 | app(config('monitoring.models.monitoring_alert')) 38 | ->findOrFail($id) 39 | ->delete(); 40 | 41 | return response()->json(true); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Http/MonitoringController.php: -------------------------------------------------------------------------------- 1 | validate([ 22 | 'duration' => 'in:hour,day', 23 | ]); 24 | 25 | $instances = $this->getInstances(); 26 | $charts = $this->getRecords($instances, $request->duration); 27 | 28 | return response()->json([ 29 | 'instances' => $instances, 30 | 'records' => $this->getLastRecords($instances), 31 | 'charts' => $charts, 32 | 'alerts' => $this->getAlerts($instances), 33 | ]); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Http/routes.php: -------------------------------------------------------------------------------- 1 | config('monitoring.routes.middlewares'), 10 | 'prefix' => config('monitoring.routes.prefix'), 11 | ], function () { 12 | Route::get('/', [MonitoringController::class, 'index'])->name('monitoring'); 13 | Route::get('/records', [MonitoringRecordController::class, 'records'])->name('records'); 14 | Route::resource('alerts', MonitoringAlertController::class)->only([ 15 | 'store', 'destroy', 16 | ]); 17 | }); 18 | -------------------------------------------------------------------------------- /src/Mail/ResourceUsageMail.php: -------------------------------------------------------------------------------- 1 | subject = $subject; 25 | $this->record = $record; 26 | } 27 | 28 | /** 29 | * Build the message. 30 | * 31 | * @return $this 32 | */ 33 | public function build() 34 | { 35 | return $this->markdown('monitoring::emails.resource-usage', [ 36 | 'subject' => $this->subject, 37 | 'record' => $this->record, 38 | ]); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Models/MonitoringAlert.php: -------------------------------------------------------------------------------- 1 | 'float', 25 | 'memory' => 'float', 26 | 'disk' => 'float', 27 | 'occurred' => 'integer', 28 | ]; 29 | } 30 | -------------------------------------------------------------------------------- /src/Models/MonitoringRecord.php: -------------------------------------------------------------------------------- 1 | 'float', 24 | 'memory' => 'float', 25 | 'disk' => 'float', 26 | ]; 27 | } 28 | -------------------------------------------------------------------------------- /src/Monitoring.php: -------------------------------------------------------------------------------- 1 | app->bind('monitoring', function () { 21 | return new Monitoring(); 22 | }); 23 | 24 | // merge config file 25 | $this->mergeConfigFrom(__DIR__.'/../config/monitoring.php', 'monitoring'); 26 | } 27 | 28 | /** 29 | * Bootstrap services. 30 | * 31 | * @return void 32 | */ 33 | public function boot() 34 | { 35 | $this->loadMigrations(); 36 | $this->registerViews(); 37 | $this->registerRoute(); 38 | $this->registerCommands(); 39 | $this->registerPublishing(); 40 | } 41 | 42 | /** 43 | * Register publishing. 44 | * 45 | * @return void 46 | */ 47 | private function registerPublishing() 48 | { 49 | if ($this->app->runningInConsole()) { 50 | // publish config 51 | $this->publishes([ 52 | __DIR__.'/../config/monitoring.php' => config_path('monitoring.php'), 53 | ], ['monitoring-config', 'laravel-config']); 54 | 55 | // publish migrations 56 | $this->publishes([ 57 | __DIR__.'/../database/migrations/' => database_path('migrations'), 58 | ], ['monitoring-migrations', 'laravel-migrations']); 59 | 60 | // publish assets 61 | $this->publishes([ 62 | __DIR__.'/../public' => public_path('vendor/monitoring'), 63 | ], ['monitoring-assets', 'laravel-assets']); 64 | } 65 | } 66 | 67 | /** 68 | * Register commands. 69 | * 70 | * @return void 71 | */ 72 | private function registerCommands() 73 | { 74 | if ($this->app->runningInConsole()) { 75 | $this->commands([ 76 | RecordCommand::class, 77 | PurgeCommand::class, 78 | PublishCommand::class, 79 | ]); 80 | } 81 | } 82 | 83 | /** 84 | * Register routes. 85 | * 86 | * @return void 87 | */ 88 | private function registerRoute() 89 | { 90 | $this->loadRoutesFrom(__DIR__.'/Http/routes.php'); 91 | } 92 | 93 | /** 94 | * Register views. 95 | * 96 | * @return void 97 | */ 98 | private function registerViews() 99 | { 100 | $this->loadViewsFrom(__DIR__.'/../resources/views/', 'monitoring'); 101 | } 102 | 103 | /** 104 | * Load migrations. 105 | * 106 | * @return void 107 | */ 108 | private function loadMigrations() 109 | { 110 | if (config('monitoring.migrations', true) && $this->app->runningInConsole()) { 111 | $this->loadMigrationsFrom(__DIR__.'/../database/migrations/'); 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/System/CPU.php: -------------------------------------------------------------------------------- 1 | environment() === 'testing') { 13 | return 50; 14 | } 15 | 16 | $usage = str_replace("\n", '', shell_exec(file_get_contents(__DIR__.'/../../scripts/cpu.sh'))); 17 | if (is_numeric($usage)) { 18 | return $usage; 19 | } 20 | 21 | return null; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/System/Disk.php: -------------------------------------------------------------------------------- 1 | environment() === 'testing') { 13 | return 50; 14 | } 15 | 16 | $usage = str_replace("\n", '', shell_exec(file_get_contents(__DIR__.'/../../scripts/disk.sh'))); 17 | if (is_numeric($usage)) { 18 | return $usage; 19 | } 20 | 21 | return null; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/System/Memory.php: -------------------------------------------------------------------------------- 1 | environment() === 'testing') { 13 | return 50; 14 | } 15 | 16 | $usage = str_replace("\n", '', shell_exec(file_get_contents(__DIR__.'/../../scripts/memory.sh'))); 17 | if (is_numeric($usage)) { 18 | return $usage; 19 | } 20 | 21 | return null; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/System/SystemResource.php: -------------------------------------------------------------------------------- 1 | set('monitoring.notifications.channels', [Email::class]); 21 | config()->set('monitoring.notifications.email', 'user@example.com'); 22 | 23 | $alert = MonitoringAlert::query()->create([ 24 | 'instance_name' => config('monitoring.instance_name'), 25 | 'cpu' => 20, 26 | 'memory' => 40, 27 | 'disk' => 80, 28 | ]); 29 | 30 | // In `testing` environment this will record `50` for all resources 31 | $this->artisan('monitoring:record'); 32 | 33 | $this->assertDatabaseHas('monitoring_alerts', [ 34 | 'id' => $alert->id, 35 | 'occurred' => 1, 36 | ]); 37 | 38 | Mail::assertSent(ResourceUsageMail::class); 39 | } 40 | 41 | public function testNoAlert() 42 | { 43 | Mail::fake(); 44 | 45 | config()->set('monitoring.notifications.channels', [Email::class]); 46 | config()->set('monitoring.notifications.email', 'user@example.com'); 47 | 48 | $alert = MonitoringAlert::query()->create([ 49 | 'instance_name' => config('monitoring.instance_name'), 50 | 'cpu' => 70, 51 | 'memory' => 55, 52 | 'disk' => 90, 53 | ]); 54 | 55 | // In `testing` environment this will record `50` for all resources 56 | $this->artisan('monitoring:record'); 57 | 58 | $this->assertDatabaseHas('monitoring_alerts', [ 59 | 'id' => $alert->id, 60 | 'occurred' => 0, 61 | ]); 62 | 63 | Mail::assertNothingSent(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /tests/Actions/CreateAlertTest.php: -------------------------------------------------------------------------------- 1 | create([ 17 | 'instance_name' => 'some_name', 18 | 'cpu' => 10, 19 | 'memory' => 20, 20 | 'disk' => 30, 21 | ]); 22 | 23 | $this->assertEquals('some_name', $alert->instance_name); 24 | 25 | $this->assertDatabaseHas('monitoring_alerts', [ 26 | 'instance_name' => 'some_name', 27 | 'cpu' => 10, 28 | 'memory' => 20, 29 | 'disk' => 30, 30 | ]); 31 | } 32 | 33 | public function testValidationError() 34 | { 35 | $this->expectException(ValidationException::class); 36 | 37 | app(CreateAlert::class)->create([ 38 | 'instance_name' => 'some_name', 39 | 'cpu' => 111, 40 | 'memory' => 20, 41 | 'disk' => 30, 42 | ]); 43 | } 44 | 45 | public function testValidationErrorEmptyPayload() 46 | { 47 | $this->expectException(ValidationException::class); 48 | 49 | app(CreateAlert::class)->create([ 50 | 'instance_name' => 'some_name', 51 | ]); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/Actions/RecordUsageTest.php: -------------------------------------------------------------------------------- 1 | set('monitoring.instance_name', 'some_name'); 16 | 17 | $record = app(RecordUsage::class)->record([ 18 | 'cpu' => 10, 19 | 'memory' => 20, 20 | 'disk' => 30, 21 | ]); 22 | 23 | $this->assertEquals('some_name', $record->instance_name); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/Channels/DiscordTest.php: -------------------------------------------------------------------------------- 1 | set('monitoring.notifications.channels', [Discord::class]); 19 | config()->set('monitoring.notifications.discord_webhook_url', 'http://webhook'); 20 | 21 | Http::fake(); 22 | 23 | app(Discord::class)->send(MonitoringRecordFactory::new()->create()); 24 | 25 | Http::assertSent(function (Request $request) { 26 | return $request->url() == 'http://webhook'; 27 | }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Channels/EmailTest.php: -------------------------------------------------------------------------------- 1 | set('monitoring.notifications.channels', [Email::class]); 19 | 20 | Mail::fake(); 21 | 22 | app(Email::class)->send(MonitoringRecordFactory::new()->create()); 23 | 24 | Mail::assertSent(ResourceUsageMail::class); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/Channels/SlackTest.php: -------------------------------------------------------------------------------- 1 | set('monitoring.notifications.channels', [Slack::class]); 19 | config()->set('monitoring.notifications.slack_webhook_url', 'http://webhook'); 20 | 21 | Http::fake(); 22 | 23 | app(Slack::class)->send(MonitoringRecordFactory::new()->create()); 24 | 25 | Http::assertSent(function (Request $request) { 26 | return $request->url() == 'http://webhook'; 27 | }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Commands/PurgeCommandTest.php: -------------------------------------------------------------------------------- 1 | set('monitoring.purge_before', '-1 day'); 16 | 17 | MonitoringRecordFactory::times(10)->create([ 18 | 'created_at' => now()->subDays(10), 19 | ]); 20 | 21 | $this->artisan('monitoring:purge'); 22 | 23 | $this->assertDatabaseCount('monitoring_records', 0); 24 | } 25 | 26 | public function testPurgeLastHour() 27 | { 28 | config()->set('monitoring.purge_before', '-1 hour'); 29 | 30 | MonitoringRecordFactory::times(10)->create([ 31 | 'created_at' => now()->subMinutes(10), 32 | ]); 33 | 34 | MonitoringRecordFactory::times(10)->create([ 35 | 'created_at' => now()->subDays(10), 36 | ]); 37 | 38 | $this->artisan('monitoring:purge'); 39 | 40 | $this->assertDatabaseCount('monitoring_records', 10); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/Commands/RecordCommandTest.php: -------------------------------------------------------------------------------- 1 | artisan('monitoring:record'); 15 | 16 | $this->assertDatabaseHas('monitoring_records', [ 17 | 'instance_name' => config('monitoring.instance_name'), 18 | ]); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/Http/MonitoringAlertControllerTest.php: -------------------------------------------------------------------------------- 1 | post(config('monitoring.routes.prefix').'/alerts', [ 15 | 'instance_name' => config('monitoring.instance_name'), 16 | 'cpu' => 20, 17 | ])->assertStatus(200); 18 | 19 | $this->assertDatabaseHas('monitoring_alerts', [ 20 | 'instance_name' => config('monitoring.instance_name'), 21 | 'cpu' => 20, 22 | ]); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/Http/MonitoringRecordControllerTest.php: -------------------------------------------------------------------------------- 1 | create(); 17 | MonitoringAlertFactory::new()->create(); 18 | 19 | $this->get(config('monitoring.routes.prefix').'/records')->assertJsonStructure([ 20 | 'instances', 21 | 'records', 22 | 'alerts', 23 | 'charts', 24 | ]); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | 'SaeedVaziry\Monitoring\Facades\Monitoring', 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | resolve: { 5 | alias: { 6 | '@': path.resolve('resources/js'), 7 | }, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /webpack.mix.js: -------------------------------------------------------------------------------- 1 | const mix = require('laravel-mix'); 2 | 3 | /* 4 | |-------------------------------------------------------------------------- 5 | | Mix Asset Management 6 | |-------------------------------------------------------------------------- 7 | | 8 | | Mix provides a clean, fluent API for defining some Webpack build steps 9 | | for your Laravel application. By default, we are compiling the Sass 10 | | file for the application as well as bundling up all the JS files. 11 | | 12 | */ 13 | 14 | mix 15 | .setPublicPath('public') 16 | .js('resources/js/app.js', 'public/js') 17 | .vue() 18 | .postCss('resources/css/app.css', 'public/css', [ 19 | require('postcss-import'), 20 | require('tailwindcss'), 21 | ]) 22 | .webpackConfig(require('./webpack.config')) 23 | .disableNotifications() 24 | .version(); 25 | --------------------------------------------------------------------------------