├── README.md ├── composer.json ├── resources └── views │ ├── date.blade.php │ ├── status-json.php │ ├── status-page.blade.php │ ├── status-panel.blade.php │ └── status.blade.php └── src ├── Command └── QueueQueueCheckCommand.php ├── Job.php ├── QueueChecker.php ├── QueueMonitor.php ├── QueueStatus.php └── ServiceProvider.php /README.md: -------------------------------------------------------------------------------- 1 | Laravel queue monitor 2 | ===================== 3 | 4 | This adds various tools to a project for monitoring its queue. 5 | 6 | Laravel version 7 | --------------- 8 | 9 | This branch and the `2.*` line of tags are for Laravel 5. For the Laravel 4 10 | version [see the laravel4 branch][l4] and the `1.*` line of tags. 11 | 12 | If you are using Laravel 5.5 or later, and are using PHP 7.1 or later, 13 | and your queue is backed by Redis, 14 | you may instead consider using [Laravel Horizon][horizon], 15 | which is an official tool and solves the same issue as this package. 16 | 17 | [l4]: https://github.com/tremby/laravel-queue-monitor/tree/laravel4 18 | [horizon]: https://laravel.com/docs/5.5/horizon 19 | 20 | Installation 21 | ------------ 22 | 23 | Require it in your Laravel project: 24 | 25 | composer require tremby/laravel-queue-monitor 26 | 27 | If you're running Laravel 5.4 or below, 28 | you have to register the service provider manually 29 | in your `config/app.php` file: 30 | 31 | ```php 32 | 'providers' => [ 33 | ... 34 | Tremby\QueueMonitor\ServiceProvider::class, 35 | ], 36 | ``` 37 | 38 | Use 39 | --- 40 | 41 | Add a cron job which runs the `queue:queuecheck` Artisan task for each queue you 42 | want to monitor. A queue name can be passed as an argument, or the default queue 43 | name is used if none is given. See `./artisan queue:queuecheck --help` for full 44 | details. 45 | 46 | Example cron job to check the default queue every 15 minutes: 47 | 48 | */15 * * * * php /home/forge/example.com/artisan queue:queuecheck 49 | 50 | This task records in the application cache (for one day) that a check for this 51 | queue is pending, then pushes a job to this queue. This job changes that cached 52 | status to "OK", so if the job doesn't run for whatever reason the status will be 53 | left at "pending". 54 | 55 | The status of all queue monitors can be checked by rendering one of the provided 56 | status views. The markup of the provided views are [Twitter 57 | Bootstrap](http://getbootstrap.com/)-friendly and if the `status-page` view is 58 | used Bootstrap is loaded from a CDN. 59 | 60 | ```php 61 | Route::get('queue-monitor', function () { 62 | return Response::view('queue-monitor::status-page'); 63 | }); 64 | ``` 65 | 66 | Other views available are `queue-monitor::status-panel`, which is the `.panel` 67 | element and its contents; and `queue-monitor::status`, which is just the `table` 68 | element. Either of these could be used to plug this monitor into a larger 69 | monitoring panel. A `panel_class` option can be passed, which defaults to 70 | `panel-default`. 71 | 72 | There's also `queue-monitor::status-json`, which renders JSON suitable for 73 | machine consumption. This allows rendering options to be passed to the 74 | underlying `json_encode` and can be used like this: 75 | 76 | ```php 77 | Route::get('queue-monitor.json', function () { 78 | $response = Response::view('queue-monitor::status-json', [ 79 | 'options' => \JSON_PRETTY_PRINT, 80 | ]); 81 | $response->header('Content-Type', 'application/json'); 82 | return $response; 83 | }); 84 | ``` 85 | 86 | In practice you might set the cron job to run every 15 minutes, and then 87 | automate another job (such as with a remote health checker) to run a few minutes 88 | later, consume the JSON, and ensure all queues have the `ok` status. If any 89 | don't, it could send an alert with a link to the HTML queue status view. It 90 | could also check that the date at which the last check was queued is reasonable, 91 | and so that the cron job has not stopped working. 92 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tremby/laravel-queue-monitor", 3 | "description": "Laravel queue monitoring tools", 4 | "require": { 5 | "php": ">=5.6.4", 6 | "laravel/framework": "~5.1" 7 | }, 8 | "license": "MIT", 9 | "authors": [ 10 | { 11 | "name": "Bart Nagel", 12 | "email": "bart@tremby.net" 13 | } 14 | ], 15 | "autoload": { 16 | "psr-4": { 17 | "Tremby\\QueueMonitor\\": "src/" 18 | } 19 | }, 20 | "extra": { 21 | "laravel": { 22 | "providers": [ 23 | "Tremby\\QueueMonitor\\ServiceProvider" 24 | ] 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /resources/views/date.blade.php: -------------------------------------------------------------------------------- 1 | @if ($date) 2 | 5 | @else 6 | 7 | null 8 | 9 | @endif 10 | -------------------------------------------------------------------------------- /resources/views/status-json.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Queue monitor 7 | 8 | 9 | 10 | 11 |
12 | @include('queue-monitor::status-panel') 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /resources/views/status-panel.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | Queue monitor 5 |

6 |
7 | @include('queue-monitor::status') 8 |
9 | -------------------------------------------------------------------------------- /resources/views/status.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | 12 | 15 | 18 | 19 | 20 | @foreach ($queues as $queueStatus) 21 | 30 | 35 | 38 | 41 | 44 | 52 | 53 | @endforeach 54 | 55 |
4 | Queue name 5 | 7 | Status 8 | 10 | Details 11 | 13 | Check queued 14 | 16 | Queue job completed 17 |
31 | 32 | {{ $queueStatus->getQueueName() }} 33 | 34 | 36 | {{ $queueStatus->getStatus() }} 37 | 39 | {{ $queueStatus->getMessage() }} 40 | 42 | @include('queue-monitor::date', ['date' => $queueStatus->getStartTime()]) 43 | 45 | @include('queue-monitor::date', ['date' => $queueStatus->getEndTime()]) 46 | @if (($start = $queueStatus->getStartTime()) && ($end = $queueStatus->getEndTime())) 47 | 48 | ({{ $end->diffForHumans($start) }}) 49 | 50 | @endif 51 |
56 | -------------------------------------------------------------------------------- /src/Command/QueueQueueCheckCommand.php: -------------------------------------------------------------------------------- 1 | argument('queue')); 43 | } 44 | 45 | /** 46 | * Execute the console command for Laravel 5.5+ 47 | * 48 | * @return mixed 49 | */ 50 | public function handle() 51 | { 52 | $this->fire(); 53 | } 54 | 55 | /** 56 | * Get the console command arguments. 57 | * 58 | * @return array 59 | */ 60 | protected function getArguments() 61 | { 62 | return [ 63 | [ 64 | 'queue', 65 | InputArgument::OPTIONAL, 66 | "Queue to queue a check for" 67 | . " (default is the application's default queue)", 68 | null, 69 | ], 70 | ]; 71 | } 72 | 73 | /** 74 | * Get the console command options. 75 | * 76 | * @return array 77 | */ 78 | protected function getOptions() 79 | { 80 | return []; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Job.php: -------------------------------------------------------------------------------- 1 | queueName = $queueName; 35 | $this->startTime = $startTime; 36 | } 37 | 38 | /** 39 | * Execute the job 40 | * 41 | * @param Log $log 42 | * @return void 43 | */ 44 | public function handle(Log $log) 45 | { 46 | $log->debug("Handling check job for queue '{$this->queueName}', queued at {$this->startTime}"); 47 | $status = QueueStatus::get($this->queueName); 48 | if (!$status) { 49 | $message = "Queue status was not found in cache, yet queued job ran; is the cache correctly configured?"; 50 | $log->error($message); 51 | $status = new QueueStatus($this->queueName, QueueStatus::ERROR); 52 | $status->setMessage($message); 53 | $status->setEndTime(); 54 | $status->save(); 55 | } elseif (!$status->isPending()) { 56 | $log->warning("Non-pending status for check for queue '{$this->queueName}' found in the cache; ignoring: " . $status); 57 | } elseif (!$status->getStartTime() || $status->getStartTime()->ne($this->startTime)) { 58 | $log->warning("Pending status for check for queue '{$this->queueName}' found in the cache with mismatching time (expected {$this->startTime}, found {$status->getStartTime()}); ignoring: " . $status); 59 | } else { 60 | $log->debug("Successful queue check for queue '{$this->queueName}'"); 61 | $status->setStatus(QueueStatus::OK); 62 | $status->setEndTime(); 63 | $status->save(); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/QueueChecker.php: -------------------------------------------------------------------------------- 1 | queueName = $queueName; 27 | $this->startTime = Carbon::now(); 28 | } 29 | 30 | /** 31 | * Cache the fact that a queue check has been queued, and queue the check 32 | * 33 | * @return void 34 | */ 35 | public function queueCheck() 36 | { 37 | $status = new QueueStatus($this->queueName, QueueStatus::PENDING, $this->startTime); 38 | $status->save(); 39 | 40 | Queue::pushOn( 41 | $this->queueName, 42 | new Job($this->queueName, $this->startTime) 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/QueueMonitor.php: -------------------------------------------------------------------------------- 1 | queueCheck(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/QueueStatus.php: -------------------------------------------------------------------------------- 1 | queueName = $queueName; 55 | $this->status = $status; 56 | if ($startTime) { 57 | $this->startTime = $startTime; 58 | } 59 | } 60 | 61 | /** 62 | * Get the queue name 63 | * 64 | * @return string 65 | */ 66 | public function getQueueName() 67 | { 68 | return $this->queueName; 69 | } 70 | 71 | /** 72 | * Get the status 73 | * 74 | * @return string 75 | */ 76 | public function getStatus() 77 | { 78 | return $this->status; 79 | } 80 | 81 | /** 82 | * Is this queue pending its queued job? 83 | * 84 | * @return bool 85 | */ 86 | public function isPending() 87 | { 88 | return $this->getStatus() === self::PENDING; 89 | } 90 | 91 | /** 92 | * Is this queue OK? 93 | * 94 | * @return bool 95 | */ 96 | public function isOk() 97 | { 98 | return $this->getStatus() === self::OK; 99 | } 100 | 101 | /** 102 | * Does this queue have an error status? 103 | * 104 | * @return bool 105 | */ 106 | public function isError() 107 | { 108 | return $this->getStatus() === self::ERROR; 109 | } 110 | 111 | /** 112 | * Set the status 113 | * 114 | * @param string $status Status constant 115 | * @return void 116 | */ 117 | public function setStatus($status) 118 | { 119 | $this->status = $status; 120 | } 121 | 122 | /** 123 | * Get the start time 124 | * 125 | * @return Carbon 126 | */ 127 | public function getStartTime() 128 | { 129 | return $this->startTime; 130 | } 131 | 132 | /** 133 | * Get the end time 134 | * 135 | * @return Carbon 136 | */ 137 | public function getEndTime() 138 | { 139 | return $this->endTime; 140 | } 141 | 142 | /** 143 | * Set the end time 144 | * 145 | * @param Carbon $endTime End time, or null for the current time 146 | * @return void 147 | */ 148 | public function setEndTime(Carbon $endTime = null) 149 | { 150 | if ($endTime) { 151 | $this->endTime = $endTime; 152 | } else { 153 | $this->endTime = Carbon::now(); 154 | } 155 | } 156 | 157 | /** 158 | * Get the message 159 | * 160 | * @return string 161 | */ 162 | public function getMessage() 163 | { 164 | return $this->message; 165 | } 166 | 167 | /** 168 | * Set the message 169 | * 170 | * @param string $message 171 | * @return void 172 | */ 173 | public function setMessage($message) 174 | { 175 | $this->message = $message; 176 | } 177 | 178 | /** 179 | * Save in cache 180 | * 181 | * @return void 182 | */ 183 | public function save() 184 | { 185 | Cache::forever(QueueMonitor::getCheckCacheKey($this->getQueueName()), $this); 186 | } 187 | 188 | /** 189 | * {@inheritDoc} 190 | */ 191 | public function toJson($options = 0) 192 | { 193 | return json_encode($this->toArray(), $options); 194 | } 195 | 196 | /** 197 | * {@inheritDoc} 198 | */ 199 | public function jsonSerialize() 200 | { 201 | return $this->toArray(); 202 | } 203 | 204 | /** 205 | * {@inheritDoc} 206 | */ 207 | public function toArray() 208 | { 209 | return [ 210 | 'queueName' => $this->getQueueName(), 211 | 'status' => $this->getStatus(), 212 | 'message' => $this->getMessage(), 213 | 'startTime' => $this->getStartTime(), 214 | 'startTimeDiff' => $this->getStartTime() ? $this->getStartTime()->diffInSeconds(null, false) : null, 215 | 'endTime' => $this->getEndTime(), 216 | 'endTimeDiff' => $this->getEndTime() ? $this->getEndTime()->diffInSeconds(null, false) : null, 217 | ]; 218 | } 219 | 220 | /** 221 | * {@inheritDoc} 222 | */ 223 | public function __toString() 224 | { 225 | return $this->toJson(); 226 | } 227 | 228 | /** 229 | * Get an instance from cache by queue name 230 | * 231 | * @param string $queueName 232 | * @return QueueStatus 233 | */ 234 | public static function get($queueName) 235 | { 236 | return Cache::get(QueueMonitor::getCheckCacheKey($queueName)); 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /src/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | commands([ 16 | Command\QueueQueueCheckCommand::class, 17 | ]); 18 | } 19 | 20 | /** 21 | * {@inheritDoc} 22 | */ 23 | public function boot(ViewFactory $viewFactory, CacheRepository $cache) 24 | { 25 | // Define views path 26 | $this->loadViewsFrom(__DIR__ . '/../resources/views', 'queue-monitor'); 27 | 28 | // Composer for the status views 29 | $composer = function ($view) use ($cache) { 30 | $queues = []; 31 | foreach ($cache->get(QueueMonitor::QUEUES_CACHE_KEY, []) as $queueName) { 32 | $status = QueueStatus::get($queueName); 33 | if (!$status) { 34 | $status = new QueueStatus($queueName, QueueStatus::ERROR); 35 | $status->setMessage("Status not found in cache; is a cron job set up and running?"); 36 | } 37 | $queues[$queueName] = $status; 38 | } 39 | $view->withQueues($queues); 40 | }; 41 | $viewFactory->composer('queue-monitor::status', $composer); 42 | $viewFactory->composer('queue-monitor::status-json', $composer); 43 | } 44 | } 45 | --------------------------------------------------------------------------------