Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
name: CI

on:
push:
branches: ["main", "master"]
pull_request:

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
extensions: mbstring, curl, dom, pdo_sqlite, fileinfo
tools: composer

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'

- name: Cache Composer dependencies
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT

- name: Restore Composer cache
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-

- name: Install Composer dependencies
run: composer install --no-progress --no-interaction --prefer-dist

- name: Copy environment file
run: cp .env.example .env

- name: Generate application key
run: php artisan key:generate

- name: Prepare SQLite database
run: php -r "file_exists('database/database.sqlite') || touch('database/database.sqlite');"

- name: Install Node dependencies
run: npm ci

- name: Build assets
run: npm run build

- name: Run test suite
env:
DB_CONNECTION: sqlite
DB_DATABASE: database/database.sqlite
run: composer test
104 changes: 104 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# MSP Monitor

MSP Monitor is a Laravel + Vue application built for managed service providers to track
infrastructure health across data warehouses, Spectrum servers, customer websites, SSL
certificates, and third-party telemetry delivered via webhooks. Checks run on a schedule
through dedicated drivers (HTTP, ICMP ping, TCP port, SSL certificate) with alert routing,
maintenance windows, and public status pages baked in.

## Key Capabilities
- Multi-protocol monitoring with driver architecture (HTTP, ping, TCP, SSL, webhook ingest)
- Per-monitor policies and notification channels (email, Slack webhooks)
- Maintenance windows to suppress noise during planned work
- UniFi Site Manager integration for automated device sync
- Public-facing status page support via Inertia/SPA frontend
- Queue-driven execution and centralized incident tracking

## Requirements
- PHP **8.2** or newer with required extensions (`bcmath`, `ctype`, `openssl`, `pdo`, `mbstring`, `curl`)
- Composer **2.x**
- Node.js **18+** and npm (or pnpm/yarn) for frontend assets
- Redis, SQS, or database queue driver (database queue enabled out of the box)
- A SQL database (MySQL 8+, MariaDB 10.5+, or PostgreSQL 13+)
- Supervisor/Process manager for long-running queue workers and scheduled commands

Optional:
- Access to UniFi Site Manager API (configure `UNIFI_SITE_MANAGER_BASE_URL` and `UNIFI_SITE_MANAGER_API_KEY`)
- Mail provider credentials for outbound alerts and user notifications

## First-Time Setup
1. **Clone & Install Dependencies**
```bash
git clone <repo-url>
cd monitor
composer install --no-interaction --prefer-dist
npm install
```

2. **Configure Environment**
```bash
cp .env.example .env
php artisan key:generate
```
Update `.env` with database credentials, queue driver, mail settings, and any optional
integrations (Slack, UniFi, etc.).

3. **Run Migrations**
```bash
php artisan migrate --force
```

4. **Build Frontend Assets** (development)
```bash
npm run dev
```
For production: `npm run build` (or `npm run build && npm run build:ssr` if SSR is used).

## Local Development
- Launch the bundled dev stack: `composer run dev`
- Serves the app, queue listener, log tailing, and Vite via `concurrently`.
- Alternatively run services individually:
```bash
php artisan serve
php artisan queue:listen --tries=1
npm run dev
```
- Execute the scheduler locally with: `php artisan schedule:work`

## Deployment Guide
1. **Install PHP Dependencies** on the target host (as in setup) using `composer install --no-dev --optimize-autoloader`.
2. **Install Frontend Assets** via `npm ci && npm run build` from the release directory.
3. **Set Environment** variables and ensure `.env` has production credentials.
4. **Run Database Migrations** during each deploy: `php artisan migrate --force`.
5. **Cache Optimizations** (optional but recommended):
```bash
php artisan config:cache
php artisan route:cache
php artisan view:cache
```
6. **Queue Worker**: supervise `php artisan queue:work --sleep=1 --tries=3` (or use Horizon).
7. **Scheduler**: configure cron to run every minute:
```cron
* * * * * php /path/to/artisan schedule:run >> /dev/null 2>&1
```
This dispatches monitor checks (`monitors:run`) and UniFi sync jobs.
8. **Permissions**: grant web server user write access to `storage/` and `bootstrap/cache/`.

## Monitoring & Maintenance
- Manual ad-hoc monitor run: `php artisan monitors:run` or POST to `/monitors/{id}/run` in the UI.
- Webhook monitor ingestion endpoint: `POST /api/webhooks/monitors/{token}` with JSON payload.
- Queue health: tail `storage/logs/laravel.log` and `storage/logs/schedule.log` for job/scheduler output.

## Testing
```bash
php artisan test # Runs full Pest test suite
```

CI/CD pipelines should execute the test suite and a linter/formatter (e.g., `vendor/bin/pint`) prior to deploys.

## Additional Notes
- Configure trusted proxies/load balancers via `App\Http\Middleware\TrustProxies` if deploying behind a proxy.
- For SSL/TLS probing support ensure the host running checks can establish outbound connections
(ICMP, TCP, HTTP/HTTPS as needed).
- When using the ping driver on Linux, grant the PHP process permission to run `ping` (setcap or wrapper).

4 changes: 3 additions & 1 deletion app/Console/Commands/RunDueMonitors.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ class RunDueMonitors extends Command
public function handle(): int
{
$now = now();
$monitors = Monitor::all();
$monitors = Monitor::query()
->where('type', '!=', 'webhook')
->get();
$due = $monitors->filter(function ($m) use ($now) {
if (empty($m->frequency_minutes) || $m->frequency_minutes < 1) return false;
if (is_null($m->last_checked_at)) return true;
Expand Down
71 changes: 71 additions & 0 deletions app/Http/Controllers/MaintenanceWindowController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

namespace App\Http\Controllers;

use App\Models\Device;
use App\Models\MaintenanceWindow;
use App\Models\Monitor;
use App\Models\Organization;
use Illuminate\Http\Request;
use Inertia\Inertia;

class MaintenanceWindowController extends Controller
{
public function index()
{
$windows = MaintenanceWindow::orderByDesc('start_at')->get();
return Inertia::render('Maintenance/Index', [ 'windows' => $windows ]);
}

public function create()
{
return Inertia::render('Maintenance/Create', [
'organizations' => Organization::orderBy('name')->get(['id','name']),
'devices' => Device::orderBy('name')->get(['id','name']),
'monitors' => Monitor::orderBy('name')->get(['id','name']),
]);
}

public function store(Request $request)
{
$data = $request->validate([
'scope' => ['required','in:global,organization,device,monitor'],
'ref_id' => ['nullable','integer'],
'start_at' => ['required','date'],
'end_at' => ['required','date','after:start_at'],
'reason' => ['nullable','string','max:255'],
]);
MaintenanceWindow::create($data);
return redirect()->route('maintenance.index')->with('success','Maintenance window created.');
}

public function edit(MaintenanceWindow $maintenance)
{
return Inertia::render('Maintenance/Edit', [
'maintenance' => $maintenance,
'organizations' => Organization::orderBy('name')->get(['id','name']),
'devices' => Device::orderBy('name')->get(['id','name']),
'monitors' => Monitor::orderBy('name')->get(['id','name']),
]);
}

public function update(Request $request, MaintenanceWindow $maintenance)
{
$data = $request->validate([
'scope' => ['required','in:global,organization,device,monitor'],
'ref_id' => ['nullable','integer'],
'start_at' => ['required','date'],
'end_at' => ['required','date','after:start_at'],
'reason' => ['nullable','string','max:255'],
]);
$maintenance->update($data);
return redirect()->route('maintenance.index')->with('success','Maintenance window updated.');
}

public function destroy(MaintenanceWindow $maintenance)
{
$maintenance->delete();
return redirect()->route('maintenance.index')->with('success','Maintenance window deleted.');
}
}

Loading
Loading