Here's how to build a custom health audit dashboard that monitors your Drupal site's security, performance, and configuration—all from one page.
Why Build Your Own Drupal Site Health Monitoring Dashboard?
Drupal sites fail in predictable ways. Cron stops running. Security updates pile up. Files get corrupted. Memory runs out. By the time users complain, you're already in crisis mode.
A health dashboard catches these issues early. Instead of reactive firefighting, you get proactive monitoring. Plus, you can customize it to check the specific things that matter for your site.
What We'll Build: Complete Site Health Monitoring Solution
Our dashboard will check:
- Security update status
- Cron job health
- File system permissions
- Database integrity
- Performance bottlenecks
- Configuration errors
The final result: a single admin page that shows red, yellow, or green status for each check, with details on how to fix problems.
Prerequisites for Building Drupal Health Dashboard
You'll need:
- Drupal 10 or 11 running locally or on a server
- Basic PHP knowledge
- Access to create custom modules
- Drush installed (optional but helpful)
Step 1: Create the Custom Module Structure
First, create your module directory:
modules/custom/site_health_dashboard/
Create the info file at site_health_dashboard.info.yml:
name: 'Site Health Dashboard' type: module description: 'Monitors site health with customizable checks' core_version_requirement: ^10 || ^11 package: Custom dependencies: - drupal:system - drupal:update
This tells Drupal about your module and ensures it works with both Drupal 10 and 11.
Step 2: Set Up Routing and Controller Configuration
Create site_health_dashboard.routing.yml:
site_health_dashboard.dashboard: path: '/admin/reports/site-health' defaults: _controller: '\Drupal\site_health_dashboard\Controller\DashboardController::dashboard' _title: 'Site Health Dashboard' requirements: _permission: 'administer site configuration'
Now create the controller at src/Controller/DashboardController.php:
dateFormatter = $date_formatter; } public static function create(ContainerInterface $container) { return new static( $container->get('date.formatter') ); } public function dashboard() { $checks = $this->runHealthChecks(); return [ '#theme' => 'site_health_dashboard', '#checks' => $checks, '#attached' => [ 'library' => ['site_health_dashboard/dashboard'], ], ]; } private function runHealthChecks() { return [ 'cron' => $this->checkCronStatus(), 'updates' => $this->checkSecurityUpdates(), 'database' => $this->checkDatabaseHealth(), 'files' => $this->checkFileSystem(), ]; } private function checkCronStatus() { $last_run = \Drupal::state()->get('system.cron_last'); if (!$last_run) { return [ 'status' => 'error', 'message' => 'Cron has never run', 'details' => 'Run cron manually or set up automated cron jobs.', ]; } $hours_since = (time() - $last_run) / 3600; if ($hours_since > 24) { return [ 'status' => 'warning', 'message' => 'Cron last ran ' . round($hours_since) . ' hours ago', 'details' => 'Cron should run at least daily.', ]; } return [ 'status' => 'ok', 'message' => 'Cron ran ' . $this->dateFormatter->formatTimeDiffSince($last_run) . ' ago', 'details' => 'Cron is running normally.', ]; } private function checkSecurityUpdates() { $available = update_get_available(TRUE); $project_data = update_calculate_project_data($available); $security_updates = 0; foreach ($project_data as $project) { if (isset($project['security updates'])) { $security_updates = count($project['security updates']); } } if ($security_updates > 0) { return [ 'status' => 'error', 'message' => $security_updates . ' security updates available', 'details' => 'Apply security updates immediately.', ]; } return [ 'status' => 'ok', 'message' => 'No security updates needed', 'details' => 'All modules are up to date.', ]; } private function checkDatabaseHealth() { try { $database = \Drupal::database(); $result = $database->query("SELECT COUNT(*) FROM {users}")->fetchField(); return [ 'status' => 'ok', 'message' => 'Database connection working', 'details' => 'Found ' . $result . ' user records.', ]; } catch (\Exception $e) { return [ 'status' => 'error', 'message' => 'Database connection failed', 'details' => $e->getMessage(), ]; } } private function checkFileSystem() { $public_path = \Drupal::service('file_system')->realpath('public://'); $private_path = \Drupal::service('file_system')->realpath('private://'); $issues = []; if (!is_writable($public_path)) { $issues[] = 'Public files directory not writable'; } if ($private_path && !is_writable($private_path)) { $issues[] = 'Private files directory not writable'; } if (!empty($issues)) { return [ 'status' => 'error', 'message' => 'File system issues detected', 'details' => implode(', ', $issues), ]; } return [ 'status' => 'ok', 'message' => 'File system healthy', 'details' => 'All directories writable.', ]; } }
Step 3: Create the Dashboard Template
Create templates/site-health-dashboard.html.twig:
Site Health Status
{% for check_name, check in checks %}{% endfor %}{{ check_name|title }}
{{ check.status|upper }}{{ check.details }}
Step 4: Add Professional Dashboard Styling
Create site_health_dashboard.libraries.yml:
dashboard: css: theme: css/dashboard.css: {}
Create css/dashboard.css:
.site-health-dashboard { max-width: 1200px; } .health-checks { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1rem; margin-top: 1rem; } .health-check { border: 2px solid #ddd; border-radius: 8px; padding: 1rem; background: #fff; } .health-check--ok { border-color: #28a745; background-color: #f8fff9; } .health-check--warning { border-color: #ffc107; background-color: #fffdf0; } .health-check--error { border-color: #dc3545; background-color: #fff8f8; } .health-check__header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem; } .health-check__status { font-weight: bold; padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.75rem; } .health-check--ok .health-check__status { background-color: #28a745; color: white; } .health-check--warning .health-check__status { background-color: #ffc107; color: black; } .health-check--error .health-check__status { background-color: #dc3545; color: white; } .health-check__message { font-weight: bold; margin-bottom: 0.5rem; } .health-check__details { color: #666; font-size: 0.9rem; }
Step 5: Register the Theme Hook
Create or edit site_health_dashboard.module:
[ 'variables' => ['checks' => NULL], 'template' => 'site-health-dashboard', ], ]; }
Step 6: Enable and Test Your Drupal Health Dashboard
Enable your module:
drush en site_health_dashboard
Or through the admin interface at /admin/modules.
Visit /admin/reports/site-health to see your dashboard in action.
Making Your Site Health Dashboard More Powerful
The basic dashboard is just the start. Here are ways to extend it:
Add Advanced Health Monitoring Checks
Create additional health checks by adding methods to your controller:
private function checkMemoryUsage() { $memory_limit = ini_get('memory_limit'); $memory_usage = memory_get_peak_usage(true); // Convert memory_limit to bytes for comparison $limit_bytes = $this->convertToBytes($memory_limit); $usage_percent = ($memory_usage / $limit_bytes) * 100; if ($usage_percent > 80) { return [ 'status' => 'warning', 'message' => 'High memory usage: ' . round($usage_percent) . '%', 'details' => 'Consider increasing memory_limit or optimizing code.', ]; } return [ 'status' => 'ok', 'message' => 'Memory usage: ' . round($usage_percent) . '%', 'details' => 'Memory usage is within normal limits.', ]; }
Cache Results for Better Performance
For expensive checks, add caching:
private function checkSecurityUpdates() { $cache = \Drupal::cache()->get('site_health_dashboard.security_updates'); if ($cache && (time() - $cache->created) < 3600) { return $cache->data; } // Run the actual check... $result = [/* your check results */]; \Drupal::cache()->set('site_health_dashboard.security_updates', $result, time() 3600); return $result; }
Add Administrative Configuration Options
Let admins choose which checks to run by creating a config form.
Common Problems and Troubleshooting Solutions
Dashboard shows "Access denied"
Check that your user has the "Administer site configuration" permission.
Checks always show as "OK" even when there are problems
Make sure you're testing the actual conditions. Add debugging with \Drupal::logger('site_health_dashboard')->info() to see what's happening.
Styling looks wrong
Clear your cache after adding CSS changes: drush cr
Module won't enable
Check for syntax errors in your PHP files. Run php -l filename.php to check.
Performance Optimization for Site Health Monitoring
Health checks can slow down your dashboard if they're expensive. Here are ways to keep it fast:
- Cache results for checks that don't change often
- Run heavy checks via cron and store results
- Use AJAX to load individual checks after the page loads
- Limit check frequency - some things only need checking daily
Next Steps for Advanced Site Health Monitoring
Your health dashboard is now running and catching basic issues. To make it production-ready:
- Add more specific checks for your site's critical functions
- Set up email alerts for critical issues
- Create a mobile-friendly version for monitoring on the go
- Integrate with external monitoring services
- Add historical tracking to spot trends
Building this dashboard gives you the foundation for proactive site management. Instead of waiting for problems to find you, you'll catch them early and keep your Drupal site running smoothly.
The code examples here cover the essential structure. Customize the health checks for your specific needs, and you'll have a powerful tool for keeping your site healthy.