Migration Guide #

Complete guide for migrating to Server Monitor plugin from other monitoring solutions, upgrading between versions, and handling data migrations.

Migration Scenarios #

From External Monitoring Services #

Supported Migration Sources:

  • Uptime Robot
  • Pingdom
  • StatusCake
  • New Relic Synthetics
  • Custom monitoring solutions
  • CSV/Excel server lists

Migration Strategy:

  1. Export existing data from current service
  2. Map data structure to Server Monitor format
  3. Import servers in batches
  4. Configure notifications for existing users
  5. Verify monitoring is working correctly
  6. Transition gradually to avoid service gaps

Between Server Monitor Versions #

Version Compatibility:

  • v1.0.x → v1.1.x: Database migrations required
  • v1.1.x → v1.2.x: Notification settings migration
  • v1.2.x → v2.0.x: Major structural changes (future)

Upgrade Process:

  1. Backup current installation
  2. Review breaking changes
  3. Update codebase
  4. Run database migrations
  5. Test functionality
  6. Update configurations

Importing Server Data #

CSV Import Format #

Required CSV Structure:

title,endpoint,organization_name
Production Website,https://www.example.com,Acme Corp
API Server,https://api.example.com,Acme Corp
Database Health,https://db.example.com/health,Acme Corp

Optional Fields:

title,endpoint,organization_name,status,notes
Production Website,https://www.example.com,Acme Corp,200,Main website
API Server,https://api.example.com,Acme Corp,200,REST API endpoint
Staging App,https://staging.example.com,Acme Corp,404,Currently down for maintenance

Batch Import Script #

PHP Import Implementation:

<?php
// import-servers.php - Run via artisan command

use Albrightlabs\ServerMonitor\Models\Server;
use Albrightlabs\SaasBase\Models\Organization;
use Illuminate\Console\Command;

class ImportServersCommand extends Command
{
    protected $signature = 'servermonitor:import {file} {--dry-run} {--organization=}';
    protected $description = 'Import servers from CSV file';

    public function handle()
    {
        $file = $this->argument('file');
        $dryRun = $this->option('dry-run');
        $organizationId = $this->option('organization');

        if (!file_exists($file)) {
            $this->error("File not found: {$file}");
            return 1;
        }

        $csv = array_map('str_getcsv', file($file));
        $headers = array_shift($csv);

        $imported = 0;
        $errors = 0;

        foreach ($csv as $row) {
            $data = array_combine($headers, $row);

            try {
                if ($dryRun) {
                    $this->line("Would import: {$data['title']} -> {$data['endpoint']}");
                } else {
                    $this->importServer($data, $organizationId);
                }
                $imported++;
            } catch (\Exception $e) {
                $this->error("Error importing {$data['title']}: " . $e->getMessage());
                $errors++;
            }
        }

        $this->info("Import completed: {$imported} imported, {$errors} errors");
        return 0;
    }

    protected function importServer($data, $organizationId = null)
    {
        // Find or create organization
        if ($organizationId) {
            $organization = Organization::find($organizationId);
        } else {
            $organization = Organization::firstOrCreate(
                ['name' => $data['organization_name']],
                [
                    'selected_plan' => 'free',
                    'subscription_status' => 'active'
                ]
            );
        }

        if (!$organization) {
            throw new \Exception('Organization not found or could not be created');
        }

        // Create server
        $server = new Server();
        $server->title = $data['title'];
        $server->endpoint = $data['endpoint'];
        $server->organization_id = $organization->id;

        if (isset($data['status'])) {
            $server->status = $data['status'];
        }

        $server->save();

        $this->line("Imported: {$server->title} for {$organization->name}");
    }
}

Register Command:

// In Plugin.php register() method
$this->registerConsoleCommand('servermonitor.import', \Albrightlabs\ServerMonitor\Console\ImportServersCommand::class);

Usage Examples:

# Dry run to test import
php artisan servermonitor:import servers.csv --dry-run

# Import to specific organization
php artisan servermonitor:import servers.csv --organization=123

# Full import
php artisan servermonitor:import servers.csv

API Import Script #

REST API Import:

<?php
// api-import.php - Import via API calls

class ApiImporter
{
    protected $apiKey;
    protected $baseUrl;

    public function __construct($apiKey, $baseUrl)
    {
        $this->apiKey = $apiKey;
        $this->baseUrl = rtrim($baseUrl, '/');
    }

    public function importFromUptimeRobot($apiKey)
    {
        // Get monitors from Uptime Robot API
        $response = $this->callUptimeRobotApi($apiKey);

        foreach ($response['monitors'] as $monitor) {
            $this->createServer([
                'title' => $monitor['friendly_name'],
                'endpoint' => $monitor['url'],
                'status' => $this->mapUptimeRobotStatus($monitor['status'])
            ]);
        }
    }

    protected function createServer($data)
    {
        $response = $this->makeRequest('POST', '/api/servers', $data);

        if ($response['success']) {
            echo "Created server: {$data['title']}\n";
        } else {
            echo "Error creating {$data['title']}: {$response['message']}\n";
        }
    }

    protected function makeRequest($method, $endpoint, $data = [])
    {
        $url = $this->baseUrl . $endpoint;

        $curl = curl_init();
        curl_setopt_array($curl, [
            CURLOPT_URL => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CUSTOMREQUEST => $method,
            CURLOPT_HTTPHEADER => [
                'Authorization: Bearer ' . $this->apiKey,
                'Content-Type: application/json'
            ],
            CURLOPT_POSTFIELDS => json_encode($data)
        ]);

        $response = curl_exec($curl);
        curl_close($curl);

        return json_decode($response, true);
    }
}

// Usage
$importer = new ApiImporter('your-api-key', 'https://yoursite.com');
$importer->importFromUptimeRobot('uptime-robot-api-key');

Service-Specific Migrations #

From Uptime Robot #

Export Data from Uptime Robot:

  1. Login to Uptime Robot dashboard
  2. Go to "My Settings" → "API Settings"
  3. Generate API key
  4. Use API to export monitor data

Uptime Robot API Export:

// uptime-robot-export.php
function exportUptimeRobotMonitors($apiKey)
{
    $url = "https://api.uptimerobot.com/v2/getMonitors";

    $post = [
        'api_key' => $apiKey,
        'format' => 'json',
        'logs' => 0
    ];

    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_POST, 1);
    curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post));
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

    $response = curl_exec($ch);
    curl_close($ch);

    $data = json_decode($response, true);

    // Convert to CSV format
    $csv = "title,endpoint,status\n";
    foreach ($data['monitors'] as $monitor) {
        $status = $monitor['status'] == 2 ? '200' : '500'; // 2 = up, others = down
        $csv .= "\"{$monitor['friendly_name']}\",\"{$monitor['url']}\",\"{$status}\"\n";
    }

    file_put_contents('uptime-robot-export.csv', $csv);
    echo "Exported " . count($data['monitors']) . " monitors to uptime-robot-export.csv\n";
}

// Usage
exportUptimeRobotMonitors('your-uptime-robot-api-key');

From Pingdom #

Pingdom Export Process:

  1. Login to Pingdom
  2. Go to "Reports" → "Shared Reports"
  3. Create custom report with check details
  4. Export as CSV

Pingdom CSV Conversion:

// pingdom-convert.php
function convertPingdomCsv($inputFile, $outputFile)
{
    $input = fopen($inputFile, 'r');
    $output = fopen($outputFile, 'w');

    // Write new headers
    fputcsv($output, ['title', 'endpoint', 'status']);

    // Skip Pingdom headers
    fgetcsv($input);

    while (($row = fgetcsv($input)) !== FALSE) {
        // Pingdom format: [Name, URL, Status, Last Response Time, etc.]
        $title = $row[0];
        $endpoint = $row[1];
        $status = (strtolower($row[2]) === 'up') ? '200' : '500';

        fputcsv($output, [$title, $endpoint, $status]);
    }

    fclose($input);
    fclose($output);

    echo "Converted Pingdom data to Server Monitor format\n";
}

// Usage
convertPingdomCsv('pingdom-export.csv', 'servers-import.csv');

From Custom Monitoring #

Generic JSON/XML Import:

// custom-import.php
function importFromJson($jsonFile)
{
    $data = json_decode(file_get_contents($jsonFile), true);
    $csv = "title,endpoint,organization_name\n";

    foreach ($data['servers'] as $server) {
        $title = addslashes($server['name']);
        $endpoint = $server['url'];
        $org = addslashes($server['organization'] ?? 'Default Organization');

        $csv .= "\"{$title}\",\"{$endpoint}\",\"{$org}\"\n";
    }

    file_put_contents('custom-import.csv', $csv);
    echo "Converted custom JSON to importable CSV\n";
}

// Example JSON structure:
// {
//   "servers": [
//     {
//       "name": "Production API",
//       "url": "https://api.example.com",
//       "organization": "Example Corp"
//     }
//   ]
// }

Notification Migration #

Migrating User Settings #

Export Existing Notification Settings:

// export-notifications.php
use Albrightlabs\ServerMonitor\Models\NotificationSetting;
use Backend\Models\User;

function exportNotificationSettings()
{
    $settings = NotificationSetting::with('organization')->get();
    $export = [];

    foreach ($settings as $setting) {
        $orgData = [
            'organization_name' => $setting->organization->name,
            'organization_id' => $setting->organization_id,
            'users' => []
        ];

        foreach ($setting->administrators as $admin) {
            $user = User::find($admin['user_id']);
            if ($user) {
                $orgData['users'][] = [
                    'name' => $user->first_name . ' ' . $user->last_name,
                    'email' => $user->email,
                    'phone' => $user->phone_number,
                    'email_enabled' => $admin['email_enabled'],
                    'sms_enabled' => $admin['sms_enabled']
                ];
            }
        }

        $export[] = $orgData;
    }

    file_put_contents('notification-settings-export.json', json_encode($export, JSON_PRETTY_PRINT));
    echo "Exported notification settings for " . count($export) . " organizations\n";
}

Import Notification Settings:

// import-notifications.php
function importNotificationSettings($jsonFile)
{
    $data = json_decode(file_get_contents($jsonFile), true);

    foreach ($data as $orgData) {
        $org = Organization::where('name', $orgData['organization_name'])->first();
        if (!$org) {
            echo "Organization not found: {$orgData['organization_name']}\n";
            continue;
        }

        $settings = NotificationSetting::getForOrganization($org->id);
        $administrators = [];

        foreach ($orgData['users'] as $userData) {
            $user = User::where('email', $userData['email'])->first();
            if ($user && $user->organization_id == $org->id) {
                $administrators[] = [
                    'user_id' => $user->id,
                    'email_enabled' => $userData['email_enabled'],
                    'sms_enabled' => $userData['sms_enabled']
                ];
            }
        }

        $settings->administrators = $administrators;
        $settings->save();

        echo "Imported notifications for {$org->name}: " . count($administrators) . " users\n";
    }
}

Version Upgrade Procedures #

Pre-Upgrade Checklist #

Backup Requirements:

  • [ ] Database backup - Full database dump
  • [ ] File backup - Plugin files and configuration
  • [ ] Environment backup - .env file copy
  • [ ] Queue status - Note current job counts
  • [ ] Notification test - Verify current functionality

Environment Verification:

  • [ ] PHP version - Check compatibility
  • [ ] October CMS version - Verify plugin compatibility
  • [ ] SaasBase version - Ensure dependency compatibility
  • [ ] Database version - Check migration requirements
  • [ ] Available disk space - Ensure sufficient space

Backup Script #

#!/bin/bash
# backup-before-upgrade.sh

DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/backups/servermonitor-upgrade-$DATE"
SITE_ROOT="/var/www/your-site"

echo "Creating backup directory: $BACKUP_DIR"
mkdir -p $BACKUP_DIR

# Database backup
echo "Backing up database..."
mysqldump your_database > "$BACKUP_DIR/database.sql"

# Files backup
echo "Backing up files..."
cp -r "$SITE_ROOT/plugins/albrightlabs/servermonitor" "$BACKUP_DIR/plugin-files"
cp "$SITE_ROOT/.env" "$BACKUP_DIR/env-backup"

# Configuration backup
echo "Backing up configuration..."
cp -r "$SITE_ROOT/config" "$BACKUP_DIR/config-backup"

# Supervisor configuration
cp /etc/supervisor/conf.d/servermonitor-workers.conf "$BACKUP_DIR/supervisor-backup.conf"

# Queue status
echo "Recording queue status..."
redis-cli llen queues:premium > "$BACKUP_DIR/queue-premium-count.txt"
redis-cli llen queues:free > "$BACKUP_DIR/queue-free-count.txt"

echo "Backup completed: $BACKUP_DIR"

Version-Specific Upgrades #

v1.1.x to v1.2.x Upgrade:

#!/bin/bash
# upgrade-v1.1-to-v1.2.sh

echo "Upgrading Server Monitor from v1.1.x to v1.2.x"

# 1. Stop workers
echo "Stopping queue workers..."
supervisorctl stop servermonitor-premium:*
supervisorctl stop servermonitor-free:*
supervisorctl stop servermonitor-calculations:*

# 2. Backup current installation
./backup-before-upgrade.sh

# 3. Update plugin files
echo "Updating plugin files..."
cd plugins/albrightlabs/servermonitor
git fetch origin
git checkout v1.2.1

# 4. Run migrations
echo "Running database migrations..."
cd ../../../
php artisan october:migrate

# 5. Clear caches
echo "Clearing caches..."
php artisan cache:clear
php artisan config:clear

# 6. Update configuration if needed
echo "Checking configuration..."
# Add any new required config variables

# 7. Restart workers
echo "Restarting queue workers..."
supervisorctl start servermonitor-premium:*
supervisorctl start servermonitor-free:*
supervisorctl start servermonitor-calculations:*

# 8. Verify functionality
echo "Verifying installation..."
php artisan servermonitor:check-capacity

echo "Upgrade completed!"

Post-Upgrade Verification #

Functionality Checklist:

  • [ ] Dashboard loads - Admin interface accessible
  • [ ] Servers visible - Existing servers display correctly
  • [ ] Queue workers running - All workers operational
  • [ ] API endpoints respond - Test API functionality
  • [ ] Notifications work - Send test notification
  • [ ] Uptime calculations - Verify calculations are updating

Verification Script:

#!/bin/bash
# verify-upgrade.sh

echo "Verifying Server Monitor upgrade..."

# Check database integrity
echo "Checking database..."
php artisan tinker --execute="echo \Albrightlabs\ServerMonitor\Models\Server::count() . ' servers found\n';"

# Check queue workers
echo "Checking queue workers..."
supervisorctl status | grep servermonitor

# Test API endpoints
echo "Testing API..."
curl -s "https://yoursite.com/api/servermonitor/worker-status/YOUR_API_KEY" | jq '.status'

# Check recent logs
echo "Checking recent activity..."
tail -5 storage/logs/laravel.log

# Test notification (if configured)
echo "Testing notification system..."
php artisan servermonitor:testalerts

echo "Verification completed!"

Data Migration Scripts #

Historical Data Migration #

Migrate Uptime History:

// migrate-uptime-history.php
function migrateUptimeHistory($csvFile)
{
    $file = fopen($csvFile, 'r');
    $headers = fgetcsv($file);

    // Expected CSV: server_name, date, status, response_time
    while (($row = fgetcsv($file)) !== FALSE) {
        $data = array_combine($headers, $row);

        $server = Server::where('title', $data['server_name'])->first();
        if (!$server) {
            echo "Server not found: {$data['server_name']}\n";
            continue;
        }

        // Create historical log entry
        $log = new \Albrightlabs\ServerMonitor\Models\Log();
        $log->server_id = $server->id;
        $log->original_status = null;
        $log->updated_status = $data['status'];
        $log->note = "Historical data import";
        $log->created_at = $data['date'];
        $log->save();

        echo "Imported history for {$server->title}: {$data['date']} -> {$data['status']}\n";
    }

    fclose($file);
}

Organization Consolidation #

Merge Organizations:

// merge-organizations.php
function mergeOrganizations($sourceOrgId, $targetOrgId)
{
    $sourceOrg = Organization::find($sourceOrgId);
    $targetOrg = Organization::find($targetOrgId);

    if (!$sourceOrg || !$targetOrg) {
        throw new \Exception('Organizations not found');
    }

    echo "Merging {$sourceOrg->name} into {$targetOrg->name}\n";

    // Move servers
    $servers = Server::where('organization_id', $sourceOrgId)->get();
    foreach ($servers as $server) {
        $server->organization_id = $targetOrgId;
        $server->save();
        echo "Moved server: {$server->title}\n";
    }

    // Move alert logs
    AlertLog::where('organization_id', $sourceOrgId)
        ->update(['organization_id' => $targetOrgId]);

    // Move notification settings
    $sourceSettings = NotificationSetting::where('organization_id', $sourceOrgId)->first();
    $targetSettings = NotificationSetting::getForOrganization($targetOrgId);

    if ($sourceSettings) {
        // Merge administrators arrays
        $mergedAdmins = array_merge(
            $targetSettings->administrators ?? [],
            $sourceSettings->administrators ?? []
        );

        // Remove duplicates based on user_id
        $uniqueAdmins = [];
        foreach ($mergedAdmins as $admin) {
            $uniqueAdmins[$admin['user_id']] = $admin;
        }

        $targetSettings->administrators = array_values($uniqueAdmins);
        $targetSettings->save();

        $sourceSettings->delete();
    }

    echo "Merge completed. Review and delete source organization if appropriate.\n";
}

Rollback Procedures #

Emergency Rollback #

Quick Rollback Script:

#!/bin/bash
# emergency-rollback.sh

BACKUP_DIR="$1"

if [ -z "$BACKUP_DIR" ]; then
    echo "Usage: $0 /path/to/backup/directory"
    exit 1
fi

echo "EMERGENCY ROLLBACK - Restoring from $BACKUP_DIR"

# Stop workers
supervisorctl stop servermonitor-premium:*
supervisorctl stop servermonitor-free:*
supervisorctl stop servermonitor-calculations:*

# Restore database
echo "Restoring database..."
mysql your_database < "$BACKUP_DIR/database.sql"

# Restore plugin files
echo "Restoring plugin files..."
rm -rf plugins/albrightlabs/servermonitor
cp -r "$BACKUP_DIR/plugin-files" plugins/albrightlabs/servermonitor

# Restore configuration
echo "Restoring configuration..."
cp "$BACKUP_DIR/env-backup" .env

# Clear caches
php artisan cache:clear
php artisan config:clear

# Restart workers
supervisorctl start servermonitor-premium:*
supervisorctl start servermonitor-free:*
supervisorctl start servermonitor-calculations:*

echo "Rollback completed!"

Partial Rollback #

Configuration-Only Rollback:

# Rollback only configuration changes
cp "$BACKUP_DIR/env-backup" .env
cp -r "$BACKUP_DIR/config-backup/*" config/
php artisan config:clear

Database-Only Rollback:

# Rollback only database changes
mysql your_database < "$BACKUP_DIR/database.sql"
php artisan cache:clear

Testing Migration Success #

Automated Testing #

Migration Test Suite:

// tests/MigrationTest.php
class MigrationTest extends TestCase
{
    public function testServerDataIntegrity()
    {
        $serverCount = Server::count();
        $this->assertGreaterThan(0, $serverCount, 'Servers should exist after migration');

        $serversWithOrganizations = Server::whereNotNull('organization_id')->count();
        $this->assertEquals($serverCount, $serversWithOrganizations, 'All servers should have organizations');
    }

    public function testNotificationSettings()
    {
        $organizations = Organization::has('servers')->get();

        foreach ($organizations as $org) {
            $settings = NotificationSetting::where('organization_id', $org->id)->first();
            $this->assertNotNull($settings, "Organization {$org->name} should have notification settings");
        }
    }

    public function testQueueWorkers()
    {
        // Test that workers can process jobs
        $server = Server::first();
        CheckServerJob::dispatch($server->id, 'premium');

        // Process job
        Artisan::call('queue:work', ['--once' => true, '--queue' => 'premium']);

        $server->refresh();
        $this->assertNotNull($server->last_checked_at, 'Server should have been checked');
    }
}

Manual Verification #

Step-by-Step Verification:

  1. Login to admin - Verify authentication works
  2. Check server list - Ensure all servers are visible
  3. View server details - Check uptime and logs
  4. Test notifications - Send test alert
  5. Monitor queue health - Verify workers are processing
  6. Check API endpoints - Test external integrations

Migration Best Practices #

Planning Phase #

Pre-Migration Assessment:

  • Inventory current monitoring setup
  • Identify critical vs. non-critical servers
  • Plan migration in phases (test → staging → production)
  • Communicate timeline to stakeholders
  • Prepare rollback procedures

Execution Phase #

Migration Day Checklist:

  • [ ] Maintenance window - Schedule appropriate downtime
  • [ ] Team availability - Ensure technical team is available
  • [ ] Backups verified - Test backup restoration
  • [ ] Communication sent - Notify users of migration
  • [ ] Monitoring disabled - Prevent false alerts during migration

Post-Migration #

Monitoring Period:

  • Monitor system for 24-48 hours post-migration
  • Check notification delivery
  • Verify uptime calculations
  • Review error logs
  • Collect user feedback

Documentation Updates:

  • Update system documentation
  • Record lessons learned
  • Update team procedures
  • Update monitoring runbooks

Previous: ← Legal Pages | Next: Back to Documentation ↗