├── .php_cs.dist.php ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── UPGRADING.md ├── composer.json ├── config └── schedule-monitor.php ├── database ├── factories │ ├── MonitoredScheduledTaskFactory.php │ └── MonitoredScheduledTaskLogItemFactory.php └── migrations │ └── create_schedule_monitor_tables.php.stub ├── resources └── views │ ├── alert.blade.php │ ├── components │ ├── duplicate-tasks.blade.php │ ├── monitored-tasks.blade.php │ ├── ready-for-monitoring-tasks.blade.php │ ├── task.blade.php │ ├── title.blade.php │ └── unnamed-tasks.blade.php │ ├── list.blade.php │ └── sync.blade.php └── src ├── Commands ├── ListCommand.php ├── SyncCommand.php └── VerifyCommand.php ├── EventHandlers ├── BackgroundCommandListener.php └── ScheduledTaskEventSubscriber.php ├── Exceptions └── InvalidClassException.php ├── Jobs └── PingOhDearJob.php ├── Models ├── MonitoredScheduledTask.php └── MonitoredScheduledTaskLogItem.php ├── ScheduleMonitorServiceProvider.php └── Support ├── Concerns ├── UsesMonitoredScheduledTasks.php └── UsesScheduleMonitoringModels.php ├── OhDearPayload ├── OhDearPayloadFactory.php └── Payloads │ ├── FailedPayload.php │ ├── FinishedPayload.php │ ├── Payload.php │ └── StartingPayload.php └── ScheduledTasks ├── MonitoredScheduledTasks.php ├── ScheduledTaskFactory.php ├── ScheduledTasks.php └── Tasks ├── ClosureTask.php ├── CommandTask.php ├── JobTask.php ├── ShellTask.php └── Task.php /.php_cs.dist.php: -------------------------------------------------------------------------------- 1 | in([ 5 | __DIR__ . '/src', 6 | __DIR__ . '/tests', 7 | ]) 8 | ->name('*.php') 9 | ->notName('*.blade.php') 10 | ->ignoreDotFiles(true) 11 | ->ignoreVCS(true); 12 | 13 | return (new PhpCsFixer\Config()) 14 | ->setRules([ 15 | '@PSR2' => true, 16 | 'array_syntax' => ['syntax' => 'short'], 17 | 'ordered_imports' => ['sort_algorithm' => 'alpha'], 18 | 'no_unused_imports' => true, 19 | 'not_operator_with_successor_space' => true, 20 | 'trailing_comma_in_multiline' => true, 21 | 'phpdoc_scalar' => true, 22 | 'unary_operator_spaces' => true, 23 | 'binary_operator_spaces' => true, 24 | 'blank_line_before_statement' => [ 25 | 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], 26 | ], 27 | 'phpdoc_single_line_var_spacing' => true, 28 | 'phpdoc_var_without_name' => true, 29 | 'method_argument_space' => [ 30 | 'on_multiline' => 'ensure_fully_multiline', 31 | 'keep_multiple_spaces_after_comma' => true, 32 | ], 33 | 'single_trait_insert_per_statement' => true, 34 | ]) 35 | ->setFinder($finder); 36 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `laravel-schedule-monitor` will be documented in this file 4 | 5 | ## 3.10.3 - 2025-02-21 6 | 7 | ### What's Changed 8 | 9 | * set default oh dear api url by @resohead in https://github.com/spatie/laravel-schedule-monitor/pull/125 10 | 11 | **Full Changelog**: https://github.com/spatie/laravel-schedule-monitor/compare/3.10.2...3.10.3 12 | 13 | ## 3.10.2 - 2025-02-21 14 | 15 | ### What's Changed 16 | 17 | * Additional custom ping endpoint and config by @resohead in https://github.com/spatie/laravel-schedule-monitor/pull/123 18 | 19 | ### New Contributors 20 | 21 | * @resohead made their first contribution in https://github.com/spatie/laravel-schedule-monitor/pull/123 22 | 23 | **Full Changelog**: https://github.com/spatie/laravel-schedule-monitor/compare/3.10.1...3.10.2 24 | 25 | ## 3.10.1 - 2025-02-21 26 | 27 | ### What's Changed 28 | 29 | * Laravel 12.x Compatibility by @laravel-shift in https://github.com/spatie/laravel-schedule-monitor/pull/122 30 | 31 | **Full Changelog**: https://github.com/spatie/laravel-schedule-monitor/compare/3.10.0...3.10.1 32 | 33 | ## 2.4.8 - 2025-02-17 34 | 35 | ### What's Changed 36 | 37 | * Update MonitoredScheduledTask.php to get the failed response to be st… by @675076143 in https://github.com/spatie/laravel-schedule-monitor/pull/121 38 | 39 | ### New Contributors 40 | 41 | * @675076143 made their first contribution in https://github.com/spatie/laravel-schedule-monitor/pull/121 42 | 43 | **Full Changelog**: https://github.com/spatie/laravel-schedule-monitor/compare/2.4.7...2.4.8 44 | 45 | ## 3.10.0 - 2025-02-05 46 | 47 | ### What's Changed 48 | 49 | * Add support for a custom ping endpoint in Oh Dear by @mattiasgeniar in https://github.com/spatie/laravel-schedule-monitor/pull/119 50 | 51 | **Full Changelog**: https://github.com/spatie/laravel-schedule-monitor/compare/3.9.2...3.10.0 52 | 53 | ## 3.9.2 - 2025-01-17 54 | 55 | ### What's Changed 56 | 57 | * Use explicit nullable type is Task::nextRunAt by @bastien-phi in https://github.com/spatie/laravel-schedule-monitor/pull/117 58 | 59 | **Full Changelog**: https://github.com/spatie/laravel-schedule-monitor/compare/3.9.1...3.9.2 60 | 61 | ## 3.9.1 - 2025-01-17 62 | 63 | ### What's Changed 64 | 65 | * Fix CronExpression deprecation using constructor instead of factory method by @bastien-phi in https://github.com/spatie/laravel-schedule-monitor/pull/118 66 | 67 | ### New Contributors 68 | 69 | * @bastien-phi made their first contribution in https://github.com/spatie/laravel-schedule-monitor/pull/118 70 | 71 | **Full Changelog**: https://github.com/spatie/laravel-schedule-monitor/compare/3.9.0...3.9.1 72 | 73 | ## 3.9.0 - 2025-01-06 74 | 75 | ### What's Changed 76 | 77 | * Store schedule monitoring configurations in its own singleton by @m-bymike in https://github.com/spatie/laravel-schedule-monitor/pull/114 78 | 79 | ### New Contributors 80 | 81 | * @m-bymike made their first contribution in https://github.com/spatie/laravel-schedule-monitor/pull/114 82 | 83 | **Full Changelog**: https://github.com/spatie/laravel-schedule-monitor/compare/3.8.2...3.9.0 84 | 85 | ## 3.8.2 - 2024-12-16 86 | 87 | ### What's Changed 88 | 89 | * don't write to horizon config when not available by @Propaganistas in https://github.com/spatie/laravel-schedule-monitor/pull/116 90 | 91 | ### New Contributors 92 | 93 | * @Propaganistas made their first contribution in https://github.com/spatie/laravel-schedule-monitor/pull/116 94 | 95 | **Full Changelog**: https://github.com/spatie/laravel-schedule-monitor/compare/3.8.1...3.8.2 96 | 97 | ## 3.8.1 - 2024-07-29 98 | 99 | ### What's Changed 100 | 101 | * Fix prune link in config file comment by @pelmered in https://github.com/spatie/laravel-schedule-monitor/pull/113 102 | 103 | ### New Contributors 104 | 105 | * @pelmered made their first contribution in https://github.com/spatie/laravel-schedule-monitor/pull/113 106 | 107 | **Full Changelog**: https://github.com/spatie/laravel-schedule-monitor/compare/3.8.0...3.8.1 108 | 109 | ## 3.8.0 - 2024-06-17 110 | 111 | ### What's Changed 112 | 113 | * Update README.md typo by @acip in https://github.com/spatie/laravel-schedule-monitor/pull/111 114 | * Make `graceTimeInMinutes` configurable by @faustbrian in https://github.com/spatie/laravel-schedule-monitor/pull/112 115 | 116 | ### New Contributors 117 | 118 | * @acip made their first contribution in https://github.com/spatie/laravel-schedule-monitor/pull/111 119 | * @faustbrian made their first contribution in https://github.com/spatie/laravel-schedule-monitor/pull/112 120 | 121 | **Full Changelog**: https://github.com/spatie/laravel-schedule-monitor/compare/3.7.1...3.8.0 122 | 123 | ## 3.7.1 - 2024-03-28 124 | 125 | ### What's Changed 126 | 127 | * Fix wrong lastRunFinishedTooLate behaviour when lastStartedAt and lastFinishedAt are within a second because of a very fast task by @mathiasmoser in https://github.com/spatie/laravel-schedule-monitor/pull/109 128 | 129 | ### New Contributors 130 | 131 | * @mathiasmoser made their first contribution in https://github.com/spatie/laravel-schedule-monitor/pull/109 132 | 133 | **Full Changelog**: https://github.com/spatie/laravel-schedule-monitor/compare/3.7.0...3.7.1 134 | 135 | ## 3.7.0 - 2024-03-02 136 | 137 | ### What's Changed 138 | 139 | * Laravel 11.x Compatibility by @laravel-shift in https://github.com/spatie/laravel-schedule-monitor/pull/106 140 | 141 | ### New Contributors 142 | 143 | * @laravel-shift made their first contribution in https://github.com/spatie/laravel-schedule-monitor/pull/106 144 | 145 | **Full Changelog**: https://github.com/spatie/laravel-schedule-monitor/compare/3.6.0...3.7.0 146 | 147 | ## 3.6.0 - 2024-02-28 148 | 149 | ### What's Changed 150 | 151 | * New method runsInBackground() added in Spatie\ScheduleMonitor\Support\ScheduledTasks\Tasks\Task:class by @ravi289 in https://github.com/spatie/laravel-schedule-monitor/pull/105 152 | 153 | ### New Contributors 154 | 155 | * @ravi289 made their first contribution in https://github.com/spatie/laravel-schedule-monitor/pull/105 156 | 157 | **Full Changelog**: https://github.com/spatie/laravel-schedule-monitor/compare/3.5.0...3.6.0 158 | 159 | ## 3.5.0 - 2024-01-26 160 | 161 | ### What's Changed 162 | 163 | * Allow tasks to be monitored but not synced with oh dear by @oddvalue in https://github.com/spatie/laravel-schedule-monitor/pull/102 164 | 165 | ### New Contributors 166 | 167 | * @oddvalue made their first contribution in https://github.com/spatie/laravel-schedule-monitor/pull/102 168 | 169 | **Full Changelog**: https://github.com/spatie/laravel-schedule-monitor/compare/3.4.3...3.5.0 170 | 171 | ## 3.4.3 - 2024-01-19 172 | 173 | ### What's Changed 174 | 175 | * Update nunomaduro/termwind to 2.0 by @yoeriboven in https://github.com/spatie/laravel-schedule-monitor/pull/99 176 | 177 | **Full Changelog**: https://github.com/spatie/laravel-schedule-monitor/compare/3.4.2...3.4.3 178 | 179 | ## 3.4.2 - 2023-12-14 180 | 181 | ### What's Changed 182 | 183 | * fix: PHP warning about creation of dynamic properties by @Pr3d4dor in https://github.com/spatie/laravel-schedule-monitor/pull/98 184 | 185 | ### New Contributors 186 | 187 | * @Pr3d4dor made their first contribution in https://github.com/spatie/laravel-schedule-monitor/pull/98 188 | 189 | **Full Changelog**: https://github.com/spatie/laravel-schedule-monitor/compare/3.4.1...3.4.2 190 | 191 | ## 3.4.1 - 2023-11-29 192 | 193 | ### What's Changed 194 | 195 | * Update README.md by @robjbrain in https://github.com/spatie/laravel-schedule-monitor/pull/96 196 | * Update MonitoredScheduledTask.php to get the failed response to be st… by @AKHIL-882 in https://github.com/spatie/laravel-schedule-monitor/pull/97 197 | 198 | ### New Contributors 199 | 200 | * @robjbrain made their first contribution in https://github.com/spatie/laravel-schedule-monitor/pull/96 201 | * @AKHIL-882 made their first contribution in https://github.com/spatie/laravel-schedule-monitor/pull/97 202 | 203 | **Full Changelog**: https://github.com/spatie/laravel-schedule-monitor/compare/3.4.0...3.4.1 204 | 205 | ## 3.4.0 - 2023-08-01 206 | 207 | ### What's Changed 208 | 209 | - Add note that syncing will remove other monitors by @keithbrink in https://github.com/spatie/laravel-schedule-monitor/pull/90 210 | - Fix anchor in link to Laravel docs by @limenet in https://github.com/spatie/laravel-schedule-monitor/pull/92 211 | - Non destructive sync option (keep-old) by @keithbrink in https://github.com/spatie/laravel-schedule-monitor/pull/91 212 | 213 | ### New Contributors 214 | 215 | - @keithbrink made their first contribution in https://github.com/spatie/laravel-schedule-monitor/pull/90 216 | - @limenet made their first contribution in https://github.com/spatie/laravel-schedule-monitor/pull/92 217 | 218 | **Full Changelog**: https://github.com/spatie/laravel-schedule-monitor/compare/3.3.0...3.4.0 219 | 220 | ## 3.3.0 - 2023-05-24 221 | 222 | ### What's Changed 223 | 224 | - Add a boolean parameter to the doNotMonitor function by @bilfeldt in https://github.com/spatie/laravel-schedule-monitor/pull/88 225 | 226 | **Full Changelog**: https://github.com/spatie/laravel-schedule-monitor/compare/3.2.1...3.3.0 227 | 228 | ## 3.2.1 - 2023-02-01 229 | 230 | - fix silent by default 231 | 232 | ## 3.2.0 - 2023-02-01 233 | 234 | - silence jobs by default 235 | 236 | **Full Changelog**: https://github.com/spatie/laravel-schedule-monitor/compare/3.1.1...3.2.0 237 | 238 | ## 3.1.1 - 2023-01-23 239 | 240 | - support L10 241 | 242 | ## 3.0.4 - 2022-10-02 243 | 244 | - update deps 245 | 246 | **Full Changelog**: https://github.com/spatie/laravel-schedule-monitor/compare/3.0.3...3.0.4 247 | 248 | ## 3.0.3 - 2022-05-13 249 | 250 | ## What's Changed 251 | 252 | - fix: Use `flex` and `content-repeat` on Termwind outputs. by @xiCO2k in https://github.com/spatie/laravel-schedule-monitor/pull/76 253 | 254 | **Full Changelog**: https://github.com/spatie/laravel-schedule-monitor/compare/3.0.2...3.0.3 255 | 256 | ## 3.0.2 - 2022-05-05 257 | 258 | ## What's Changed 259 | 260 | - Update readme about model pruning by @patrickbrouwers in https://github.com/spatie/laravel-schedule-monitor/pull/71 261 | - PHPUnit to Pest Converter by @freekmurze in https://github.com/spatie/laravel-schedule-monitor/pull/73 262 | - chore: add multitenancy documentation by @ju5t in https://github.com/spatie/laravel-schedule-monitor/pull/75 263 | - Add Termwind to improve the Command Outputs. by @xiCO2k in https://github.com/spatie/laravel-schedule-monitor/pull/74 264 | 265 | ## New Contributors 266 | 267 | - @ju5t made their first contribution in https://github.com/spatie/laravel-schedule-monitor/pull/75 268 | - @xiCO2k made their first contribution in https://github.com/spatie/laravel-schedule-monitor/pull/74 269 | 270 | **Full Changelog**: https://github.com/spatie/laravel-schedule-monitor/compare/3.0.1...3.0.2 271 | 272 | ## 3.0.1 - 2022-02-13 273 | 274 | ## What's Changed 275 | 276 | - Fix return type by @SamuelNitsche in https://github.com/spatie/laravel-schedule-monitor/pull/70 277 | 278 | ## New Contributors 279 | 280 | - @SamuelNitsche made their first contribution in https://github.com/spatie/laravel-schedule-monitor/pull/70 281 | 282 | **Full Changelog**: https://github.com/spatie/laravel-schedule-monitor/compare/3.0.0...3.0.1 283 | 284 | ## 3.0.0 - 2022-01-14 285 | 286 | - Support Laravel 9 287 | 288 | ## 2.4.7 - 2021-11-17 289 | 290 | ## What's Changed 291 | 292 | - Update lorisleiva/cron-translator to version 0.3 by @bilfeldt in https://github.com/spatie/laravel-schedule-monitor/pull/67 293 | 294 | ## New Contributors 295 | 296 | - @bilfeldt made their first contribution in https://github.com/spatie/laravel-schedule-monitor/pull/67 297 | 298 | **Full Changelog**: https://github.com/spatie/laravel-schedule-monitor/compare/2.4.6...2.4.7 299 | 300 | ## 2.4.6 - 2021-11-02 301 | 302 | - Make sure retryUntil is returning a DateTime (#66) 303 | 304 | ## 2.4.5 - 2021-09-16 305 | 306 | - take environments property into account for scheduled tasks (#64) 307 | 308 | ## 2.4.4 - 2021-09-07 309 | 310 | - add `retryUntil` for PingOhdearJobs (#63) 311 | 312 | ## 2.4.3 - 2021-08-02 313 | 314 | - automatically retry ping if OhDear had downtime (#54) 315 | 316 | ## 2.4.2 - 2021-07-22 317 | 318 | - add link to docs 319 | 320 | ## 2.4.1 - 2021-06-15 321 | 322 | - update user API token url (#50) 323 | 324 | ## 2.4.0 - 2021-06-10 325 | 326 | - enable custom models 327 | 328 | ## 2.3.0 - 2021-05-13 329 | 330 | - add `storeOutputInDb` 331 | 332 | ## 2.2.1 - 2021-03-29 333 | 334 | - upgrade to latest lorisleiva/cron-translator version (#40) 335 | 336 | ## 2.2.0 - 2021-01-15 337 | 338 | - throw an exception if pinging Oh Dear has failed [#37](https://github.com/spatie/laravel-schedule-monitor/pull/37) 339 | - pass 0 instead of null parameters to Oh dear for Background tasks [#37](https://github.com/spatie/laravel-schedule-monitor/pull/37) 340 | 341 | ## 2.1.0 - 2020-12-04 342 | 343 | - add support for PHP 8 344 | 345 | ## 2.0.2 - 2020-10-14 346 | 347 | - drop support for Laravel 7 348 | - fix command description 349 | 350 | ## 2.0.1 - 2020-10-06 351 | 352 | - report right exit code for scheduled tasks in background 353 | 354 | ## 2.0.0 - 2020-09-29 355 | 356 | - add support for timezones 357 | 358 | ## 1.0.4 - 2020-09-08 359 | 360 | - add support for Laravel 8 361 | 362 | ## 1.0.3 - 2020-07-14 363 | 364 | - fix link config file 365 | 366 | ## 1.0.2 - 2020-07-14 367 | 368 | - add `CarbonImmutable` support (#3) 369 | 370 | ## 1.0.1 - 2020-07-12 371 | 372 | - improve output of commands 373 | 374 | ## 1.0.0 - 2020-07-09 375 | 376 | - initial release 377 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Immutable 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Monitor scheduled tasks in a Laravel app 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/laravel-schedule-monitor.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-schedule-monitor) 4 | [![Total Downloads](https://img.shields.io/packagist/dt/spatie/laravel-schedule-monitor.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-schedule-monitor) 5 | 6 | This package will monitor your Laravel schedule. It will write an entry to a log table in the db each time a schedule tasks starts, end, fails or is skipped. Using the `list` command you can check when the scheduled tasks have been executed. 7 | 8 | ![screenshot](https://github.com/spatie/laravel-schedule-monitor/blob/main/docs/list-with-failure.png) 9 | 10 | This package can also sync your schedule with [Oh Dear](https://ohdear.app). Oh Dear will send you a notification whenever a scheduled task doesn't run on time or fails. 11 | 12 | ## Support us 13 | 14 | [](https://spatie.be/github-ad-click/laravel-schedule-monitor) 15 | 16 | We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us). 17 | 18 | We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards). 19 | 20 | ## Installation 21 | 22 | You can install the package via composer: 23 | 24 | ```bash 25 | composer require spatie/laravel-schedule-monitor 26 | ``` 27 | 28 | If you need Laravel 8 support, you can install v2 of the package using `composer require spatie/laravel-schedule-monitor:^2`. 29 | 30 | #### Preparing the database 31 | 32 | You must publish and run migrations: 33 | 34 | ```bash 35 | php artisan vendor:publish --provider="Spatie\ScheduleMonitor\ScheduleMonitorServiceProvider" --tag="schedule-monitor-migrations" 36 | php artisan migrate 37 | ``` 38 | 39 | #### Publishing the config file 40 | 41 | You can publish the config file with: 42 | ```bash 43 | php artisan vendor:publish --provider="Spatie\ScheduleMonitor\ScheduleMonitorServiceProvider" --tag="schedule-monitor-config" 44 | ``` 45 | 46 | This is the contents of the published config file: 47 | 48 | ```php 49 | return [ 50 | /* 51 | * The schedule monitor will log each start, finish and failure of all scheduled jobs. 52 | * After a while the `monitored_scheduled_task_log_items` might become big. 53 | * Here you can specify the amount of days log items should be kept. 54 | * 55 | * Use Laravel's pruning command to delete old `MonitoredScheduledTaskLogItem` models. 56 | * More info: https://laravel.com/docs/9.x/eloquent#mass-assignment 57 | */ 58 | 'delete_log_items_older_than_days' => 30, 59 | 60 | /* 61 | * The date format used for all dates displayed on the output of commands 62 | * provided by this package. 63 | */ 64 | 'date_format' => 'Y-m-d H:i:s', 65 | 66 | 'models' => [ 67 | /* 68 | * The model you want to use as a MonitoredScheduledTask model needs to extend the 69 | * `Spatie\ScheduleMonitor\Models\MonitoredScheduledTask` Model. 70 | */ 71 | 'monitored_scheduled_task' => Spatie\ScheduleMonitor\Models\MonitoredScheduledTask::class, 72 | 73 | /* 74 | * The model you want to use as a MonitoredScheduledTaskLogItem model needs to extend the 75 | * `Spatie\ScheduleMonitor\Models\MonitoredScheduledTaskLogItem` Model. 76 | */ 77 | 'monitored_scheduled_log_item' => Spatie\ScheduleMonitor\Models\MonitoredScheduledTaskLogItem::class, 78 | ], 79 | 80 | /* 81 | * Oh Dear can notify you via Mail, Slack, SMS, web hooks, ... when a 82 | * scheduled task does not run on time. 83 | * 84 | * More info: https://ohdear.app/cron-checks 85 | */ 86 | 'oh_dear' => [ 87 | /* 88 | * You can generate an API token at the Oh Dear user settings screen 89 | * 90 | * https://ohdear.app/user/api-tokens 91 | */ 92 | 'api_token' => env('OH_DEAR_API_TOKEN', ''), 93 | 94 | /* 95 | * The id of the site you want to sync the schedule with. 96 | * 97 | * You'll find this id on the settings page of a site at Oh Dear. 98 | */ 99 | 'site_id' => env('OH_DEAR_SITE_ID'), 100 | 101 | /* 102 | * To keep scheduled jobs as short as possible, Oh Dear will be pinged 103 | * via a queued job. Here you can specify the name of the queue you wish to use. 104 | */ 105 | 'queue' => env('OH_DEAR_QUEUE'), 106 | 107 | /* 108 | * `PingOhDearJob`s will automatically be skipped if they've been queued for 109 | * longer than the time configured here. 110 | */ 111 | 'retry_job_for_minutes' => 10, 112 | ], 113 | ]; 114 | ``` 115 | 116 | #### Cleaning the database 117 | 118 | The schedule monitor will log each start, finish and failure of all scheduled jobs. After a while the `monitored_scheduled_task_log_items` might become big. 119 | 120 | Use [Laravel's model pruning feature](https://laravel.com/docs/9.x/eloquent#pruning-models) , you can delete old `MonitoredScheduledTaskLogItem` models. Models older than the amount of days configured in the `delete_log_items_older_than_days` in the `schedule-monitor` config file, will be deleted. 121 | 122 | ```php 123 | // app/Console/Kernel.php 124 | 125 | use Spatie\ScheduleMonitor\Models\MonitoredScheduledTaskLogItem; 126 | 127 | class Kernel extends ConsoleKernel 128 | { 129 | protected function schedule(Schedule $schedule) 130 | { 131 | $schedule->command('model:prune', ['--model' => MonitoredScheduledTaskLogItem::class])->daily(); 132 | } 133 | } 134 | ``` 135 | 136 | #### Syncing the schedule 137 | 138 | Every time you deploy your application, you should execute the `schedule-monitor:sync` command 139 | 140 | ```bash 141 | php artisan schedule-monitor:sync 142 | ``` 143 | 144 | This command is responsible for syncing your schedule with the database, and optionally Oh Dear. We highly recommend adding this command to the script that deploys your production environment. 145 | 146 | In a non-production environment you should manually run `schedule-monitor:sync`. You can verify if everything synced correctly using `schedule-monitor:list`. 147 | 148 | **Note:** Running the sync command will remove any other cron monitors that you've defined other than the application schedule. 149 | 150 | If you would like to use non-destructive syncs to Oh Dear so that you can monitor other cron tasks outside of Laravel, you can use the `--keep-old` flag. This will only push new tasks to Oh Dear, rather than a full sync. Note that this will not remove any tasks from Oh Dear that are no longer in your schedule. 151 | 152 | ## Usage 153 | 154 | To monitor your schedule you should first run `schedule-monitor:sync`. This command will take a look at your schedule and create an entry for each task in the `monitored_scheduled_tasks` table. 155 | 156 | ![screenshot](https://github.com/spatie/laravel-schedule-monitor/blob/main/docs/sync.png) 157 | 158 | To view all monitored scheduled tasks, you can run `schedule-monitor:list`. This command will list all monitored scheduled tasks. It will show you when a scheduled task has last started, finished, or failed. 159 | 160 | ![screenshot](https://github.com/spatie/laravel-schedule-monitor/blob/main/docs/list.png) 161 | 162 | The package will write an entry to the `monitored_scheduled_task_log_items` table in the db each time a schedule tasks starts, end, fails or is skipped. Take a look at the contents of that table if you want to know when and how scheduled tasks did execute. The log items also hold other interesting metrics like memory usage, execution time, and more. 163 | 164 | ### Naming tasks 165 | 166 | Schedule monitor will try to automatically determine a name for a scheduled task. For commands this is the command name, for anonymous jobs the class name of the first argument will be used. For some tasks, like scheduled closures, a name cannot be determined automatically. 167 | 168 | To manually set a name of the scheduled task, you can tack on `monitorName()`. 169 | 170 | Here's an example. 171 | 172 | ```php 173 | // in app/Console/Kernel.php 174 | 175 | protected function schedule(Schedule $schedule) 176 | { 177 | $schedule->command('your-command')->daily()->monitorName('a-custom-name'); 178 | $schedule->call(fn () => 1 + 1)->hourly()->monitorName('addition-closure'); 179 | } 180 | ``` 181 | 182 | When you change the name of task, the schedule monitor will remove all log items of the monitor with the old name, and create a new monitor using the new name of the task. 183 | 184 | ### Setting a grace time 185 | 186 | When the package detects that the last run of a scheduled task did not run in time, the `schedule-monitor` list will display that task using a red background color. In this screenshot the task named `your-command` ran too late. 187 | 188 | ![screenshot](https://github.com/spatie/laravel-schedule-monitor/blob/main/docs/list-with-failure.png) 189 | 190 | The package will determine that a task ran too late if it was not finished at the time it was supposed to run + the grace time. You can think of the grace time as the number of minutes that a task under normal circumstances needs to finish. By default, the package grants a grace time of 5 minutes to each task. 191 | 192 | You can customize the grace time by using the `graceTimeInMinutes` method on a task. In this example a grace time of 10 minutes is used for the `your-command` task. 193 | 194 | ```php 195 | // in app/Console/Kernel.php 196 | 197 | protected function schedule(Schedule $schedule) 198 | { 199 | $schedule->command('your-command')->daily()->graceTimeInMinutes(10); 200 | } 201 | ``` 202 | 203 | ### Ignoring scheduled tasks 204 | 205 | You can avoid a scheduled task being monitored by tacking on `doNotMonitor` when scheduling the task. 206 | 207 | ```php 208 | // in app/Console/Kernel.php 209 | 210 | protected function schedule(Schedule $schedule) 211 | { 212 | $schedule->command('your-command')->daily()->doNotMonitor(); 213 | } 214 | ``` 215 | 216 | ### Storing output in the database 217 | 218 | You can store the output by tacking on `storeOutputInDb` when scheduling the task. 219 | 220 | ```php 221 | // in app/Console/Kernel.php 222 | 223 | protected function schedule(Schedule $schedule) 224 | { 225 | $schedule->command('your-command')->daily()->storeOutputInDb(); 226 | } 227 | ``` 228 | 229 | The output will be stored in the `monitored_scheduled_task_log_items` table, in the `output` key of the `meta` column. 230 | 231 | ### Multitenancy 232 | 233 | If you're using [spatie/laravel-multitenancy](https://github.com/spatie/laravel-multitenancy) you should add the `PingOhDearJob` to 234 | the `not_tenant_aware_jobs` array in `config/multitenancy.php`. 235 | 236 | ```php 237 | 'not_tenant_aware_jobs' => [ 238 | // ... 239 | \Spatie\ScheduleMonitor\Jobs\PingOhDearJob::class, 240 | ] 241 | ``` 242 | 243 | Without it, the `PingOhDearJob` will fail as no tenant will be set. 244 | 245 | ### Getting notified when a scheduled task doesn't finish in time 246 | 247 | This package can sync your schedule with the [Oh Dear](https://ohdear.app) cron check. Oh Dear will send you a notification whenever a scheduled task does not finish on time. 248 | 249 | To get started you will first need to install the Oh Dear SDK. 250 | 251 | ```bash 252 | composer require ohdearapp/ohdear-php-sdk 253 | ``` 254 | 255 | Next you, need to make sure the `api_token` and `site_id` keys of the `schedule-monitor` are filled with an API token, and an Oh Dear site id. To verify that these values hold correct values you can run this command. 256 | 257 | ```bash 258 | php artisan schedule-monitor:verify 259 | ``` 260 | 261 | ![screenshot](https://github.com/spatie/laravel-schedule-monitor/blob/main/docs/verify.png) 262 | 263 | To sync your schedule with Oh Dear run this command: 264 | 265 | ```bash 266 | php artisan schedule-monitor:sync 267 | ``` 268 | 269 | ![screenshot](https://github.com/spatie/laravel-schedule-monitor/blob/main/docs/sync-oh-dear.png) 270 | 271 | After that, the `list` command should show that all the scheduled tasks in your app are registered on Oh Dear. 272 | 273 | ![screenshot](https://github.com/spatie/laravel-schedule-monitor/blob/main/docs/list-oh-dear.png) 274 | 275 | To keep scheduled jobs as short as possible, Oh Dear will be pinged via queued jobs. To ensure speedy delivery to Oh Dear, and to avoid false positive notifications, we highly recommend creating a dedicated queue for these jobs. You can put the name of that queue in the `queue` key of the config file. 276 | 277 | Oh Dear will wait for the completion of a schedule tasks for a given amount of minutes. This is called the grace time. By default, all scheduled tasks will have a grace time of 5 minutes. To customize this value, you can tack on `graceTimeInMinutes` to your scheduled tasks. 278 | 279 | Here's an example where Oh Dear will send a notification if the task didn't finish by 00:10. 280 | 281 | ```php 282 | // in app/Console/Kernel.php 283 | 284 | protected function schedule(Schedule $schedule) 285 | { 286 | $schedule->command('your-command')->daily()->graceTimeInMinutes(10); 287 | } 288 | ``` 289 | 290 | ### Disabling Oh Dear for individual tasks 291 | 292 | If you want to have a task monitored by the schedule monitor, but not by Oh Dear, you can tack on `doMonitorAtOhDear` to your scheduled tasks. 293 | 294 | ```php 295 | // in app/Console/Kernel.php 296 | 297 | protected function schedule(Schedule $schedule) 298 | { 299 | $schedule->command('your-command')->daily()->doNotMonitorAtOhDear(); 300 | } 301 | ``` 302 | 303 | ## Unsupported methods 304 | 305 | Currently, this package does not work for tasks that use these methods: 306 | 307 | - `between` 308 | - `unlessBetween` 309 | - `when` 310 | - `skip` 311 | 312 | ## Third party scheduled task monitors 313 | 314 | We assume that, when your scheduled tasks do not run properly, a scheduled task that sends out notifications would probably not run either. That's why this package doesn't send out notifications by itself. 315 | 316 | These services can notify you when scheduled tasks do not run properly: 317 | 318 | - [Oh Dear](https://ohdear.app) 319 | - [thenping.me](https://thenping.me) 320 | - [Healthchecks.io](https://healthchecks.io) 321 | - [Cronitor](https://cronitor.io) 322 | - [Cronhub](https://cronhub.io/) 323 | - [DeadMansSnitch](https://deadmanssnitch.com/) 324 | - [CronAlarm](https://www.cronalarm.com/) 325 | - [PushMon](https://www.pushmon.com/) 326 | 327 | ## Testing 328 | 329 | ``` bash 330 | composer test 331 | ``` 332 | 333 | ## Changelog 334 | 335 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 336 | 337 | ## Contributing 338 | 339 | Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details. 340 | 341 | ## Security 342 | 343 | If you've found a bug regarding security please mail [security@spatie.be](mailto:security@spatie.be) instead of using the issue tracker. 344 | 345 | ## Credits 346 | 347 | - [Freek Van der Herten](https://github.com/freekmurze) 348 | - [All Contributors](../../contributors) 349 | 350 | ## License 351 | 352 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 353 | -------------------------------------------------------------------------------- /UPGRADING.md: -------------------------------------------------------------------------------- 1 | ## Upgrading 2 | 3 | ### From v2 to v3 4 | 5 | The `CleanCommand` has been removed. You can now use Laravel's pruning feature to [delete old log items](https://github.com/spatie/laravel-schedule-monitor#cleaning-the-database). 6 | 7 | ### From v1 to v2 8 | 9 | Add a column `timezone` (string, nullable) to the `monitored_scheduled_tasks` table. In existing rows you should fill to column with the timezone in your app. 10 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spatie/laravel-schedule-monitor", 3 | "description": "Monitor scheduled tasks in a Laravel app", 4 | "keywords": [ 5 | "spatie", 6 | "laravel-schedule-monitor" 7 | ], 8 | "homepage": "https://github.com/spatie/laravel-schedule-monitor", 9 | "license": "MIT", 10 | "authors": [ 11 | { 12 | "name": "Freek Van der Herten", 13 | "email": "freek@spatie.be", 14 | "homepage": "https://spatie.be", 15 | "role": "Developer" 16 | } 17 | ], 18 | "require": { 19 | "php": "^8.1", 20 | "illuminate/bus": "^9.0|^10.0|^11.0|^12.0", 21 | "lorisleiva/cron-translator": "^0.3.0|^0.4.0", 22 | "nesbot/carbon": "^2.63|^3.0", 23 | "nunomaduro/termwind": "^1.10.1|^2.0", 24 | "spatie/laravel-package-tools": "^1.9" 25 | }, 26 | "require-dev": { 27 | "mockery/mockery": "^1.4", 28 | "ohdearapp/ohdear-php-sdk": "^3.0", 29 | "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0", 30 | "pestphp/pest": "^1.20|^2.34|^3.7", 31 | "pestphp/pest-plugin-laravel": "^1.2|^2.3|^3.1", 32 | "spatie/pest-plugin-snapshots": "^1.1|^2.1", 33 | "spatie/phpunit-snapshot-assertions": "^4.2|^5.1", 34 | "spatie/test-time": "^1.2" 35 | }, 36 | "suggest": { 37 | "ohdearapp/ohdear-php-sdk": "Needed to sync your schedule with Oh Dear" 38 | }, 39 | "autoload": { 40 | "psr-4": { 41 | "Spatie\\ScheduleMonitor\\": "src", 42 | "Spatie\\ScheduleMonitor\\Database\\Factories\\": "database/factories" 43 | } 44 | }, 45 | "autoload-dev": { 46 | "psr-4": { 47 | "Spatie\\ScheduleMonitor\\Tests\\": "tests" 48 | } 49 | }, 50 | "scripts": { 51 | "test": "vendor/bin/pest", 52 | "test-coverage": "vendor/bin/phpunit --coverage-html coverage", 53 | "format": "vendor/bin/php-cs-fixer fix --allow-risky=yes" 54 | }, 55 | "config": { 56 | "sort-packages": true, 57 | "allow-plugins": { 58 | "composer/package-versions-deprecated": true, 59 | "pestphp/pest-plugin": true 60 | } 61 | }, 62 | "extra": { 63 | "laravel": { 64 | "providers": [ 65 | "Spatie\\ScheduleMonitor\\ScheduleMonitorServiceProvider" 66 | ] 67 | } 68 | }, 69 | "minimum-stability": "dev", 70 | "prefer-stable": true 71 | } 72 | -------------------------------------------------------------------------------- /config/schedule-monitor.php: -------------------------------------------------------------------------------- 1 | 30, 13 | 14 | /* 15 | * The date format used for all dates displayed on the output of commands 16 | * provided by this package. 17 | */ 18 | 'date_format' => 'Y-m-d H:i:s', 19 | 20 | 'models' => [ 21 | /* 22 | * The model you want to use as a MonitoredScheduledTask model needs to extend the 23 | * `Spatie\ScheduleMonitor\Models\MonitoredScheduledTask` Model. 24 | */ 25 | 'monitored_scheduled_task' => Spatie\ScheduleMonitor\Models\MonitoredScheduledTask::class, 26 | 27 | /* 28 | * The model you want to use as a MonitoredScheduledTaskLogItem model needs to extend the 29 | * `Spatie\ScheduleMonitor\Models\MonitoredScheduledTaskLogItem` Model. 30 | */ 31 | 'monitored_scheduled_log_item' => Spatie\ScheduleMonitor\Models\MonitoredScheduledTaskLogItem::class, 32 | ], 33 | 34 | /* 35 | * Oh Dear can notify you via Mail, Slack, SMS, web hooks, ... when a 36 | * scheduled task does not run on time. 37 | * 38 | * More info: https://ohdear.app/docs/features/cron-job-monitoring 39 | */ 40 | 'oh_dear' => [ 41 | /* 42 | * You can generate an API token at the Oh Dear user settings screen 43 | * 44 | * https://ohdear.app/user/api-tokens 45 | */ 46 | 'api_token' => env('OH_DEAR_API_TOKEN', ''), 47 | 48 | /* 49 | * The id of the site you want to sync the schedule with. 50 | * 51 | * You'll find this id on the settings page of a site at Oh Dear. 52 | */ 53 | 'site_id' => env('OH_DEAR_SITE_ID'), 54 | 55 | /* 56 | * To keep scheduled jobs as short as possible, Oh Dear will be pinged 57 | * via a queued job. Here you can specify the name of the queue you wish to use. 58 | */ 59 | 'queue' => env('OH_DEAR_QUEUE'), 60 | 61 | /* 62 | * `PingOhDearJob`s will automatically be skipped if they've been queued for 63 | * longer than the time configured here. 64 | */ 65 | 'retry_job_for_minutes' => 10, 66 | 67 | /* 68 | * When set to true, we will automatically add the `PingOhDearJob` to Horizon's 69 | * silenced jobs. 70 | */ 71 | 'silence_ping_oh_dear_job_in_horizon' => true, 72 | 73 | /* 74 | * Send the start of a scheduled job to Oh Dear. This is not needed 75 | * for notifications to work correctly. 76 | */ 77 | 'send_starting_ping' => env('OH_DEAR_SEND_STARTING_PING', false), 78 | 79 | /** 80 | * The amount of minutes a scheduled task is allowed to run before it is 81 | * considered late. 82 | */ 83 | 'grace_time_in_minutes' => 5, 84 | 85 | /** 86 | * Which endpoint to ping on Oh Dear. 87 | */ 88 | 'endpoint_url' => env('OH_DEAR_PING_ENDPOINT_URL'), 89 | 90 | /** 91 | * The URL of the Oh Dear API. 92 | */ 93 | 'api_url' => env('OH_DEAR_API_URL', 'https://ohdear.app/api/'), 94 | ], 95 | ]; 96 | -------------------------------------------------------------------------------- /database/factories/MonitoredScheduledTaskFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->name, 16 | 'type' => $this->faker->randomElement(['command', 'shell', 'job', 'closure']), 17 | 'cron_expression' => '* * * * *', 18 | 'grace_time_in_minutes' => 5, 19 | ]; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /database/factories/MonitoredScheduledTaskLogItemFactory.php: -------------------------------------------------------------------------------- 1 | MonitoredScheduledTask::factory(), 17 | 'type' => $this->faker->randomElement([ 18 | MonitoredScheduledTaskLogItem::TYPE_STARTING, 19 | MonitoredScheduledTaskLogItem::TYPE_FINISHED, 20 | MonitoredScheduledTaskLogItem::TYPE_SKIPPED, 21 | ]), 22 | 'meta' => [], 23 | ]; 24 | } 25 | 26 | public function configure() 27 | { 28 | return $this->afterMaking(function(MonitoredScheduledTaskLogItem $logItem) { 29 | $scheduledTask = $logItem->monitoredScheduledTask; 30 | 31 | $scheduledTask->ping_url = config('schedule-monitor.oh_dear.endpoint_url', 'https://ping.ohdear.app'); 32 | $scheduledTask->save(); 33 | 34 | return $logItem; 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /database/migrations/create_schedule_monitor_tables.php.stub: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 13 | 14 | $table->string('name'); 15 | $table->string('type')->nullable(); 16 | $table->string('cron_expression'); 17 | $table->string('timezone')->nullable(); 18 | $table->string('ping_url')->nullable(); 19 | 20 | $table->dateTime('last_started_at')->nullable(); 21 | $table->dateTime('last_finished_at')->nullable(); 22 | $table->dateTime('last_failed_at')->nullable(); 23 | $table->dateTime('last_skipped_at')->nullable(); 24 | 25 | $table->dateTime('registered_on_oh_dear_at')->nullable(); 26 | $table->dateTime('last_pinged_at')->nullable(); 27 | $table->integer('grace_time_in_minutes'); 28 | 29 | $table->timestamps(); 30 | }); 31 | 32 | 33 | Schema::create('monitored_scheduled_task_log_items', function (Blueprint $table) { 34 | $table->bigIncrements('id'); 35 | 36 | $table->unsignedBigInteger('monitored_scheduled_task_id'); 37 | $table 38 | ->foreign('monitored_scheduled_task_id', 'fk_scheduled_task_id') 39 | ->references('id') 40 | ->on('monitored_scheduled_tasks') 41 | ->cascadeOnDelete(); 42 | 43 | $table->string('type'); 44 | 45 | $table->json('meta')->nullable(); 46 | 47 | $table->timestamps(); 48 | }); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /resources/views/alert.blade.php: -------------------------------------------------------------------------------- 1 |
2 | {!! $message !!} 3 |
4 | -------------------------------------------------------------------------------- /resources/views/components/duplicate-tasks.blade.php: -------------------------------------------------------------------------------- 1 | @props(['tasks']) 2 |
3 | Duplicate Tasks 4 | 5 |
These tasks could not be monitored because they have a duplicate name.
6 | 7 |
8 | @foreach ($tasks as $task) 9 | 10 | @endforeach 11 |
12 | 13 |
14 | To monitor these tasks you should add ->monitorName() in the schedule to manually specify a unique name. 15 |
16 |
17 | -------------------------------------------------------------------------------- /resources/views/components/monitored-tasks.blade.php: -------------------------------------------------------------------------------- 1 | @props(['tasks', 'dateFormat', 'usingOhDear']) 2 |
3 | Monitored Tasks 4 | 5 |
6 | @forelse ($tasks as $task) 7 |
8 | 9 |
10 |
11 | 12 | ⇁ Started at: 13 | 14 | {{ optional($task->lastRunStartedAt())->format($dateFormat) ?? '--' }} 15 | 16 | 17 | 18 | ⇁ Finished at: 19 | 20 | {{ optional($task->lastRunFinishedAt())->format($dateFormat) ?? '--' }} 21 | 22 | 23 |
24 | 25 | ⇁ Failed at: 26 | 27 | {{ optional($task->lastRunFailedAt())->format($dateFormat) ?? '--' }} 28 | 29 | 30 | 31 | 32 | ⇁ Next run: 33 | {{ $task->nextRunAt()->format($dateFormat) }} 34 | 35 |
36 | 37 | ⇁ Grace time: 38 | {{ $task->graceTimeInMinutes() }} minutes 39 | 40 | @if ($usingOhDear) 41 | 42 | ⇁ Registered at Oh Dear: 43 | @if ($task->isBeingMonitoredAtOhDear()) 44 | Yes 45 | @else 46 | No 47 | @endif 48 | 49 | @endif 50 |
51 |
52 |
53 | @empty 54 |
There currently are no tasks being monitored!
55 | @endforelse 56 |
57 | @if ($usingOhDear) 58 |
59 | Some tasks are not registered on oh dear. You will not be notified when they do not run on time.
60 | Run php artisan schedule-monitor:sync to register them and receive notifications. 61 |
62 | @endif 63 |
64 | -------------------------------------------------------------------------------- /resources/views/components/ready-for-monitoring-tasks.blade.php: -------------------------------------------------------------------------------- 1 | @props(['tasks']) 2 |
3 | Run sync to start monitoring 4 | 5 |
These tasks will be monitored after running: php artisan schedule-monitor:sync
6 | 7 |
8 | @foreach ($tasks as $task) 9 | 10 | @endforeach 11 |
12 |
13 | -------------------------------------------------------------------------------- /resources/views/components/task.blade.php: -------------------------------------------------------------------------------- 1 | @props(['task']) 2 |
3 | @if ($task->name()) 4 | {{ $task->name() }} 5 | ({{ $task->type() }}) 6 | @else 7 | {{ $task->type() }} 8 | @endif 9 | 10 | {{ $task->humanReadableCron() }} 11 |
12 | -------------------------------------------------------------------------------- /resources/views/components/title.blade.php: -------------------------------------------------------------------------------- 1 |
2 | {{ $slot }} 3 |
4 | -------------------------------------------------------------------------------- /resources/views/components/unnamed-tasks.blade.php: -------------------------------------------------------------------------------- 1 | @props(['tasks']) 2 |
3 | Unnamed Tasks 4 | 5 |
These tasks cannot be monitored because no name could be determined for them.
6 | 7 |
8 | @foreach ($tasks as $task) 9 | 10 | @endforeach 11 |
12 | 13 |
To monitor these tasks you should add ->monitorName() in the schedule to manually specify a name.
14 |
15 | -------------------------------------------------------------------------------- /resources/views/list.blade.php: -------------------------------------------------------------------------------- 1 |
2 | 7 | @if (! $readyForMonitoringTasks->isEmpty()) 8 | 11 | @endif 12 | @if (! $unnamedTasks->isEmpty()) 13 | 16 | @endif 17 | @if (! $duplicateTasks->isEmpty()) 18 | 21 | @endif 22 |
23 | -------------------------------------------------------------------------------- /resources/views/sync.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
All done! Now monitoring {{ $monitoredScheduledTasksCount }} {{ str()->plural('scheduled task', $monitoredScheduledTasksCount) }}.
3 |
Run php artisan schedule-monitor:list to see which jobs are now monitored.
4 |
5 | -------------------------------------------------------------------------------- /src/Commands/ListCommand.php: -------------------------------------------------------------------------------- 1 | apply('w-' . strlen(date($dateFormat))); 20 | 21 | render(view('schedule-monitor::list', [ 22 | 'monitoredTasks' => ScheduledTasks::createForSchedule()->monitoredTasks(), 23 | 'readyForMonitoringTasks' => ScheduledTasks::createForSchedule()->readyForMonitoringTasks(), 24 | 'unnamedTasks' => ScheduledTasks::createForSchedule()->unnamedTasks(), 25 | 'duplicateTasks' => ScheduledTasks::createForSchedule()->duplicateTasks(), 26 | 'usingOhDear' => $this->usingOhDear(), 27 | 'dateFormat' => $dateFormat, 28 | ])); 29 | } 30 | 31 | protected function usingOhDear(): bool 32 | { 33 | if (! class_exists(OhDear::class)) { 34 | return false; 35 | } 36 | 37 | if (empty(config('schedule-monitor.oh_dear.api_token'))) { 38 | return false; 39 | } 40 | 41 | if (empty(config('schedule-monitor.oh_dear.site_id'))) { 42 | return false; 43 | } 44 | 45 | return true; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Commands/SyncCommand.php: -------------------------------------------------------------------------------- 1 | 'Start syncing schedule...', 26 | 'class' => 'text-green', 27 | ])); 28 | 29 | $this 30 | ->storeScheduledTasksInDatabase() 31 | ->storeMonitoredScheduledTasksInOhDear(); 32 | 33 | $monitoredScheduledTasksCount = $this->getMonitoredScheduleTaskModel()->count(); 34 | 35 | render(view('schedule-monitor::sync', [ 36 | 'monitoredScheduledTasksCount' => $monitoredScheduledTasksCount, 37 | ])); 38 | } 39 | 40 | protected function storeScheduledTasksInDatabase(): self 41 | { 42 | render(view('schedule-monitor::alert', [ 43 | 'message' => 'Start syncing schedule with database...', 44 | ])); 45 | 46 | $monitoredScheduledTasks = ScheduledTasks::createForSchedule() 47 | ->uniqueTasks() 48 | ->map(function (Task $task) { 49 | return $this->getMonitoredScheduleTaskModel()->updateOrCreate( 50 | ['name' => $task->name()], 51 | array_merge([ 52 | 'type' => $task->type(), 53 | 'cron_expression' => $task->cronExpression(), 54 | 'timezone' => $task->timezone(), 55 | 'grace_time_in_minutes' => $task->graceTimeInMinutes(), 56 | ], $task->shouldMonitorAtOhDear() ? [] : ['ping_url' => null]) 57 | ); 58 | }); 59 | 60 | if (! $this->option('keep-old')) { 61 | $this->getMonitoredScheduleTaskModel()->query() 62 | ->whereNotIn('id', $monitoredScheduledTasks->pluck('id')) 63 | ->delete(); 64 | } 65 | 66 | return $this; 67 | } 68 | 69 | protected function storeMonitoredScheduledTasksInOhDear(): self 70 | { 71 | if (! class_exists(OhDear::class)) { 72 | return $this; 73 | } 74 | 75 | $siteId = config('schedule-monitor.oh_dear.site_id'); 76 | 77 | if (! $siteId) { 78 | render(view('schedule-monitor::alert', [ 79 | 'message' => << 81 | Not syncing schedule with oh dear because not site_id 82 | is not set in the oh-dear config file. 83 | 84 |
85 | Learn how to set this up at https://ohdear.app/docs/general/cron-job-monitoring/php#cron-monitoring-in-laravel-php. 86 |
87 | HTML, 88 | 'class' => 'text-yellow', 89 | ])); 90 | 91 | return $this; 92 | } 93 | 94 | render(view('schedule-monitor::alert', [ 95 | 'message' => 'Start syncing schedule with Oh Dear...', 96 | ])); 97 | 98 | $cronChecks = $this->option('keep-old') 99 | ? $this->pushMonitoredScheduledTaskToOhDear($siteId) 100 | : $this->syncMonitoredScheduledTaskWithOhDear($siteId); 101 | 102 | render(view('schedule-monitor::alert', [ 103 | 'message' => 'Successfully synced schedule with Oh Dear!', 104 | 'class' => 'text-green', 105 | ])); 106 | 107 | collect($cronChecks) 108 | ->each( 109 | function (CronCheck $cronCheck) { 110 | if (! $monitoredScheduledTask = $this->getMonitoredScheduleTaskModel()->findForCronCheck($cronCheck)) { 111 | return; 112 | } 113 | 114 | $monitoredScheduledTask->update(['ping_url' => $this->pingUrl($cronCheck)]); 115 | $monitoredScheduledTask->markAsRegisteredOnOhDear(); 116 | } 117 | ); 118 | 119 | return $this; 120 | } 121 | 122 | protected function pingUrl(CronCheck $cronCheck): string 123 | { 124 | if ($userDefinedEndpoint = config('schedule-monitor.oh_dear.endpoint_url')) { 125 | return rtrim($userDefinedEndpoint, '/') . '/' . $cronCheck->uuid; 126 | } 127 | 128 | return $cronCheck->pingUrl; 129 | } 130 | 131 | protected function syncMonitoredScheduledTaskWithOhDear(int $siteId): array 132 | { 133 | $monitoredScheduledTasks = $this->getMonitoredScheduleTaskModel() 134 | ->whereIn( 135 | 'name', 136 | ScheduledTasks::createForSchedule() 137 | ->monitoredAtOhDear() 138 | ->map->name() 139 | ) 140 | ->get(); 141 | 142 | $cronChecks = $monitoredScheduledTasks 143 | ->map(function (MonitoredScheduledTask $monitoredScheduledTask) { 144 | return [ 145 | 'name' => $monitoredScheduledTask->name, 146 | 'type' => 'cron', 147 | 'cron_expression' => $monitoredScheduledTask->cron_expression, 148 | 'grace_time_in_minutes' => $monitoredScheduledTask->grace_time_in_minutes, 149 | 'server_timezone' => $monitoredScheduledTask->timezone, 150 | 'description' => '', 151 | ]; 152 | }) 153 | ->toArray(); 154 | 155 | $cronChecks = app(OhDear::class)->site($siteId)->syncCronChecks($cronChecks); 156 | 157 | return $cronChecks; 158 | } 159 | 160 | protected function pushMonitoredScheduledTaskToOhDear(int $siteId): array 161 | { 162 | $tasksToRegister = $this->getMonitoredScheduleTaskModel() 163 | ->whereNull('registered_on_oh_dear_at') 164 | ->whereIn( 165 | 'name', 166 | ScheduledTasks::createForSchedule() 167 | ->monitoredAtOhDear() 168 | ->map->name() 169 | ) 170 | ->get(); 171 | 172 | $cronChecks = []; 173 | foreach ($tasksToRegister as $taskToRegister) { 174 | $cronChecks[] = app(OhDear::class)->createCronCheck( 175 | siteId: $siteId, 176 | name: $taskToRegister->name, 177 | cronExpression: $taskToRegister->cron_expression, 178 | graceTimeInMinutes: $taskToRegister->grace_time_in_minutes, 179 | description: '', 180 | serverTimezone: $taskToRegister->timezone, 181 | ); 182 | } 183 | 184 | return $cronChecks; 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/Commands/VerifyCommand.php: -------------------------------------------------------------------------------- 1 | 'Verifying if Oh Dear is configured correctly...', 22 | ])); 23 | 24 | $this 25 | ->verifySdkInstalled() 26 | ->verifyApiToken($ohDearConfig) 27 | ->verifySiteId($ohDearConfig) 28 | ->verifyConnection($ohDearConfig); 29 | 30 | render(view('schedule-monitor::alert', [ 31 | 'message' => <<All ok! Run php artisan schedule-monitor:sync 33 | to sync your scheduled tasks with oh dear. 34 | HTML, 35 | ])); 36 | } 37 | 38 | public function verifySdkInstalled(): self 39 | { 40 | if (! class_exists(OhDear::class)) { 41 | throw new Exception("You must install the Oh Dear SDK in order to sync your schedule with Oh Dear. Run `composer require ohdearapp/ohdear-php-sdk`."); 42 | } 43 | 44 | render(view('schedule-monitor::alert', [ 45 | 'message' => 'The Oh Dear SDK is installed.', 46 | ])); 47 | 48 | return $this; 49 | } 50 | 51 | protected function verifyApiToken(array $ohDearConfig): self 52 | { 53 | if (empty($ohDearConfig['api_token'])) { 54 | throw new Exception('No API token found. Make sure you added an API token to the `api_token` key of the `schedule-monitor` config file. You can generate a new token here: https://ohdear.app/user/api-tokens'); 55 | } 56 | 57 | render(view('schedule-monitor::alert', [ 58 | 'message' => 'Oh Dear API token found.', 59 | ])); 60 | 61 | return $this; 62 | } 63 | 64 | protected function verifySiteId(array $ohDearConfig): self 65 | { 66 | if (empty($ohDearConfig['site_id'])) { 67 | throw new Exception('No site id found. Make sure you added an site id to the `site_id` key of the `schedule-monitor` config file. You can found your site id on the settings page of a site on Oh Dear.'); 68 | } 69 | 70 | render(view('schedule-monitor::alert', [ 71 | 'message' => 'Oh Dear site id found.', 72 | ])); 73 | 74 | return $this; 75 | } 76 | 77 | protected function verifyConnection(array $ohDearConfig) 78 | { 79 | $this->comment('Trying to reach Oh Dear...'); 80 | 81 | $site = app(OhDear::class)->site($ohDearConfig['site_id']); 82 | 83 | render(view('schedule-monitor::alert', [ 84 | 'message' => "Successfully connected to Oh Dear. The configured site URL is: {$site->sortUrl}", 85 | ])); 86 | 87 | return $this; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/EventHandlers/BackgroundCommandListener.php: -------------------------------------------------------------------------------- 1 | command !== 'schedule:finish') { 18 | return; 19 | } 20 | 21 | collect(app(Schedule::class)->events()) 22 | ->filter(fn (Event $task) => $task->runInBackground) 23 | ->each(function (Event $task) { 24 | $task 25 | ->then( 26 | function () use ($task) { 27 | if (! $monitoredTask = $this->getMonitoredScheduleTaskModel()->findForTask($task)) { 28 | return; 29 | } 30 | 31 | $event = new ScheduledTaskFinished( 32 | $task, 33 | 0 34 | ); 35 | 36 | $monitoredTask->markAsFinished($event); 37 | } 38 | ); 39 | }); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/EventHandlers/ScheduledTaskEventSubscriber.php: -------------------------------------------------------------------------------- 1 | listen( 19 | ScheduledTaskStarting::class, 20 | fn (ScheduledTaskStarting $event) => optional($this->getMonitoredScheduleTaskModel()->findForTask($event->task))->markAsStarting($event) 21 | ); 22 | 23 | $events->listen( 24 | ScheduledTaskFinished::class, 25 | fn (ScheduledTaskFinished $event) => optional($this->getMonitoredScheduleTaskModel()->findForTask($event->task))->markAsFinished($event) 26 | ); 27 | 28 | $events->listen( 29 | ScheduledTaskFailed::class, 30 | fn (ScheduledTaskFailed $event) => optional($this->getMonitoredScheduleTaskModel()->findForTask($event->task))->markAsFailed($event) 31 | ); 32 | 33 | $events->listen( 34 | ScheduledTaskSkipped::class, 35 | fn (ScheduledTaskSkipped $event) => optional($this->getMonitoredScheduleTaskModel()->findForTask($event->task))->markAsSkipped($event) 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidClassException.php: -------------------------------------------------------------------------------- 1 | logItem = $logItem; 29 | 30 | if ($queue = config('schedule-monitor.oh_dear.queue')) { 31 | $this->onQueue($queue); 32 | } 33 | } 34 | 35 | public function handle() 36 | { 37 | if (! $payload = OhDearPayloadFactory::createForLogItem($this->logItem)) { 38 | return; 39 | } 40 | 41 | $response = Http::retry(3, 10 * 1000)->post($payload->url(), $payload->data()); 42 | $response->throw(); 43 | 44 | $this->logItem->monitoredScheduledTask->update(['last_pinged_at' => now()]); 45 | } 46 | 47 | public function retryUntil(): DateTime 48 | { 49 | return now()->addMinutes(config('schedule-monitor.oh_dear.retry_job_for_minutes', 10))->toDateTime(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Models/MonitoredScheduledTask.php: -------------------------------------------------------------------------------- 1 | 'datetime', 30 | 'last_pinged_at' => 'datetime', 31 | 'last_started_at' => 'datetime', 32 | 'last_finished_at' => 'datetime', 33 | 'last_skipped_at' => 'datetime', 34 | 'last_failed_at' => 'datetime', 35 | 'grace_time_in_minutes' => 'integer', 36 | ]; 37 | 38 | public function logItems(): HasMany 39 | { 40 | return $this->hasMany($this->getMonitoredScheduleTaskLogItemModel(), 'monitored_scheduled_task_id')->orderByDesc('id'); 41 | } 42 | 43 | public static function findByName(string $name): ?self 44 | { 45 | $monitoredScheduledTask = new static(); 46 | 47 | return $monitoredScheduledTask 48 | ->getMonitoredScheduleTaskModel() 49 | ->where('name', $name) 50 | ->first(); 51 | } 52 | 53 | public static function findForTask(Event $event): ?self 54 | { 55 | $task = ScheduledTaskFactory::createForEvent($event); 56 | $monitoredScheduledTask = new static(); 57 | 58 | if (empty($task->name())) { 59 | return null; 60 | } 61 | 62 | return $monitoredScheduledTask 63 | ->getMonitoredScheduleTaskModel() 64 | ->findByName($task->name()); 65 | } 66 | 67 | public static function findForCronCheck(CronCheck $cronCheck): ?self 68 | { 69 | $monitoredScheduledTask = new static(); 70 | 71 | return $monitoredScheduledTask 72 | ->getMonitoredScheduleTaskModel() 73 | ->findByName($cronCheck->name); 74 | } 75 | 76 | public function markAsRegisteredOnOhDear(): self 77 | { 78 | if (is_null($this->registered_on_oh_dear_at)) { 79 | $this->update(['registered_on_oh_dear_at' => now()]); 80 | } 81 | 82 | return $this; 83 | } 84 | 85 | public function markAsStarting(ScheduledTaskStarting $event): self 86 | { 87 | $logItem = $this->createLogItem($this->getMonitoredScheduleTaskLogItemModel()::TYPE_STARTING); 88 | 89 | $logItem->updateMeta([ 90 | 'memory' => memory_get_usage(true), 91 | ]); 92 | 93 | $this->update([ 94 | 'last_started_at' => now(), 95 | ]); 96 | 97 | if (config('schedule-monitor.oh_dear.send_starting_ping') === true) { 98 | $this->pingOhDear($logItem); 99 | } 100 | 101 | return $this; 102 | } 103 | 104 | public function markAsFinished(ScheduledTaskFinished $event): self 105 | { 106 | if ($this->eventConcernsBackgroundTaskThatCompletedInForeground($event)) { 107 | return $this; 108 | } 109 | 110 | if ($event->task->exitCode !== 0 && ! is_null($event->task->exitCode)) { 111 | return $this->markAsFailed($event); 112 | } 113 | 114 | $logItem = $this->createLogItem($this->getMonitoredScheduleTaskLogItemModel()::TYPE_FINISHED); 115 | 116 | $logItem->updateMeta([ 117 | 'runtime' => $event->task->runInBackground ? 0 : $event->runtime, 118 | 'exit_code' => $event->task->exitCode, 119 | 'memory' => $event->task->runInBackground ? 0 : memory_get_usage(true), 120 | 'output' => $this->getEventTaskOutput($event), 121 | ]); 122 | 123 | $this->update(['last_finished_at' => now()]); 124 | 125 | $this->pingOhDear($logItem); 126 | 127 | return $this; 128 | } 129 | 130 | public function eventConcernsBackgroundTaskThatCompletedInForeground(ScheduledTaskFinished $event): bool 131 | { 132 | if (! $event->task->runInBackground) { 133 | return false; 134 | } 135 | 136 | return $event->task->exitCode === null; 137 | } 138 | 139 | /** 140 | * @param ScheduledTaskFailed|ScheduledTaskFinished $event 141 | * 142 | * @return $this 143 | */ 144 | public function markAsFailed($event): self 145 | { 146 | $logItem = $this->createLogItem($this->getMonitoredScheduleTaskLogItemModel()::TYPE_FAILED); 147 | 148 | if ($event instanceof ScheduledTaskFailed) { 149 | $logItem->updateMeta([ 150 | 'failure_message' => Str::limit(optional($event->exception)->getMessage(), 255), 151 | ]); 152 | } 153 | 154 | if ($event instanceof ScheduledTaskFinished) { 155 | $logItem->updateMeta([ 156 | 'runtime' => $event->runtime, 157 | 'exit_code' => $event->task->exitCode, 158 | 'memory' => memory_get_usage(true), 159 | 'output' => $this->getEventTaskOutput($event), 160 | ]); 161 | } 162 | 163 | $this->update(['last_failed_at' => now()]); 164 | 165 | $this->pingOhDear($logItem); 166 | 167 | return $this; 168 | } 169 | 170 | public function markAsSkipped(ScheduledTaskSkipped $event): self 171 | { 172 | $this->createLogItem($this->getMonitoredScheduleTaskLogItemModel()::TYPE_SKIPPED); 173 | 174 | $this->update(['last_skipped_at' => now()]); 175 | 176 | return $this; 177 | } 178 | 179 | public function pingOhDear(MonitoredScheduledTaskLogItem $logItem): self 180 | { 181 | if (empty($this->ping_url)) { 182 | return $this; 183 | } 184 | 185 | if (! in_array($logItem->type, [ 186 | $this->getMonitoredScheduleTaskLogItemModel()::TYPE_STARTING, 187 | $this->getMonitoredScheduleTaskLogItemModel()::TYPE_FAILED, 188 | $this->getMonitoredScheduleTaskLogItemModel()::TYPE_FINISHED, 189 | ], true)) { 190 | return $this; 191 | } 192 | 193 | dispatch(new PingOhDearJob($logItem)); 194 | 195 | return $this; 196 | } 197 | 198 | public function createLogItem(string $type): MonitoredScheduledTaskLogItem 199 | { 200 | return $this->logItems()->create([ 201 | 'type' => $type, 202 | ]); 203 | } 204 | 205 | /** 206 | * @param ScheduledTaskFailed|ScheduledTaskFinished $event 207 | */ 208 | public function getEventTaskOutput($event): ?string 209 | { 210 | if (! ($this->getMonitoredScheduledTasks()->getStoreOutputInDb($event->task) ?? false)) { 211 | return null; 212 | } 213 | 214 | if (is_null($event->task->output)) { 215 | return null; 216 | } 217 | 218 | if ($event->task->output === $event->task->getDefaultOutput()) { 219 | return null; 220 | } 221 | 222 | if (! is_file($event->task->output)) { 223 | return null; 224 | } 225 | 226 | $output = file_get_contents($event->task->output); 227 | 228 | return $output ?: null; 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/Models/MonitoredScheduledTaskLogItem.php: -------------------------------------------------------------------------------- 1 | 'array', 27 | ]; 28 | 29 | public function monitoredScheduledTask(): BelongsTo 30 | { 31 | return $this->belongsTo($this->getMonitoredScheduleTaskModel(), 'monitored_scheduled_task_id'); 32 | } 33 | 34 | public function updateMeta(array $values): self 35 | { 36 | $this->update(['meta' => $values]); 37 | 38 | return $this; 39 | } 40 | 41 | public function prunable(): Builder 42 | { 43 | $days = config('schedule-monitor.delete_log_items_older_than_days'); 44 | 45 | return static::where('created_at', '<=', now()->subDays($days)); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/ScheduleMonitorServiceProvider.php: -------------------------------------------------------------------------------- 1 | name('laravel-schedule-monitor') 39 | ->hasViews() 40 | ->hasConfigFile() 41 | ->hasMigrations('create_schedule_monitor_tables') 42 | ->hasCommands([ 43 | ListCommand::class, 44 | SyncCommand::class, 45 | VerifyCommand::class, 46 | ]); 47 | } 48 | 49 | public function packageBooted() 50 | { 51 | $this 52 | ->configureOhDearApi() 53 | ->silenceOhDearJob() 54 | ->registerEventHandlers() 55 | ->registerSchedulerEventMacros() 56 | ->registerModelBindings(); 57 | } 58 | 59 | protected function registerModelBindings() 60 | { 61 | $config = config('schedule-monitor.models'); 62 | 63 | $this->app->bind(MonitoredScheduledTask::class, $config['monitored_scheduled_task']); 64 | $this->app->bind(MonitoredScheduledTaskLogItem::class, $config['monitored_scheduled_log_item']); 65 | 66 | $this->protectAgainstInvalidClassDefinition(MonitoredScheduledTask::class, app($config['monitored_scheduled_task'])); 67 | $this->protectAgainstInvalidClassDefinition(MonitoredScheduledTaskLogItem::class, app($config['monitored_scheduled_log_item'])); 68 | 69 | return $this; 70 | } 71 | 72 | protected function configureOhDearApi(): self 73 | { 74 | if (! class_exists(OhDear::class)) { 75 | return $this; 76 | } 77 | 78 | $this->app->bind(OhDear::class, function () { 79 | $apiToken = config('schedule-monitor.oh_dear.api_token'); 80 | 81 | return new OhDear($apiToken, config('schedule-monitor.oh_dear.api_url', 'https://ohdear.app/api/')); 82 | }); 83 | 84 | return $this; 85 | } 86 | 87 | protected function silenceOhDearJob(): self 88 | { 89 | if (! config('schedule-monitor.oh_dear.silence_ping_oh_dear_job_in_horizon', true)) { 90 | return $this; 91 | } 92 | 93 | if (! class_exists(Horizon::class)) { 94 | return $this; 95 | } 96 | 97 | $silencedJobs = config('horizon.silenced', []); 98 | 99 | if (in_array(PingOhDearJob::class, $silencedJobs)) { 100 | return $this; 101 | } 102 | 103 | $silencedJobs[] = PingOhDearJob::class; 104 | 105 | config()->set('horizon.silenced', $silencedJobs); 106 | 107 | return $this; 108 | } 109 | 110 | protected function registerEventHandlers(): self 111 | { 112 | Event::subscribe(ScheduledTaskEventSubscriber::class); 113 | Event::listen(CommandStarting::class, BackgroundCommandListener::class); 114 | 115 | return $this; 116 | } 117 | 118 | protected function registerSchedulerEventMacros(): self 119 | { 120 | $this->app->singleton( 121 | MonitoredScheduledTasks::class, 122 | fn () => new MonitoredScheduledTasks(), 123 | ); 124 | 125 | /** @var MonitoredScheduledTasks $monitoredScheduledTasks */ 126 | $monitoredScheduledTasks = $this->app->make(MonitoredScheduledTasks::class); 127 | 128 | SchedulerEvent::macro('monitorName', function (string $monitorName) use ($monitoredScheduledTasks) { 129 | $monitoredScheduledTasks->setMonitorName($this, $monitorName); 130 | 131 | return $this; 132 | }); 133 | 134 | SchedulerEvent::macro('graceTimeInMinutes', function (int $graceTimeInMinutes) use ($monitoredScheduledTasks) { 135 | $monitoredScheduledTasks->setGraceTimeInMinutes($this, $graceTimeInMinutes); 136 | 137 | return $this; 138 | }); 139 | 140 | SchedulerEvent::macro('doNotMonitor', function (bool $bool = true) use ($monitoredScheduledTasks) { 141 | $monitoredScheduledTasks->setDoNotMonitor($this, $bool); 142 | 143 | return $this; 144 | }); 145 | 146 | SchedulerEvent::macro('doNotMonitorAtOhDear', function (bool $bool = true) use ($monitoredScheduledTasks) { 147 | $monitoredScheduledTasks->setDoNotMonitorAtOhDear($this, $bool); 148 | 149 | return $this; 150 | }); 151 | 152 | SchedulerEvent::macro('storeOutputInDb', function (bool $bool = true) use ($monitoredScheduledTasks) { 153 | $monitoredScheduledTasks->setStoreOutputInDb($this, $bool); 154 | /** @psalm-suppress UndefinedMethod */ 155 | $this->ensureOutputIsBeingCaptured(); 156 | 157 | return $this; 158 | }); 159 | 160 | return $this; 161 | } 162 | 163 | protected function protectAgainstInvalidClassDefinition($packageClass, $providedModel): void 164 | { 165 | if (! ($providedModel instanceof $packageClass)) { 166 | $providedClass = get_class($providedModel); 167 | 168 | throw new InvalidClassException("The provided class name {$providedClass} does not extend the required package class {$packageClass}."); 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/Support/Concerns/UsesMonitoredScheduledTasks.php: -------------------------------------------------------------------------------- 1 | first(fn (string $payloadClass) => $payloadClass::canHandle($logItem)); 23 | 24 | if (! $payloadClass) { 25 | return null; 26 | } 27 | 28 | return new $payloadClass($logItem); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Support/OhDearPayload/Payloads/FailedPayload.php: -------------------------------------------------------------------------------- 1 | type === MonitoredScheduledTaskLogItem::TYPE_FAILED; 13 | } 14 | 15 | public function url() 16 | { 17 | return "{$this->baseUrl()}/failed"; 18 | } 19 | 20 | public function data(): array 21 | { 22 | return Arr::only($this->logItem->meta ?? [], [ 23 | 'failure_message', 24 | ]); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Support/OhDearPayload/Payloads/FinishedPayload.php: -------------------------------------------------------------------------------- 1 | type === MonitoredScheduledTaskLogItem::TYPE_FINISHED; 13 | } 14 | 15 | public function url() 16 | { 17 | return "{$this->baseUrl()}/finished"; 18 | } 19 | 20 | public function data(): array 21 | { 22 | return Arr::only($this->logItem->meta ?? [], [ 23 | ...(config('schedule-monitor.oh_dear.send_starting_ping') ? [] : ['runtime']), 24 | 'exit_code', 25 | 'memory', 26 | ]); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Support/OhDearPayload/Payloads/Payload.php: -------------------------------------------------------------------------------- 1 | logItem = $logItem; 16 | } 17 | 18 | abstract public function url(); 19 | 20 | abstract public function data(); 21 | 22 | protected function baseUrl(): string 23 | { 24 | return $this->logItem->monitoredScheduledTask->ping_url; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Support/OhDearPayload/Payloads/StartingPayload.php: -------------------------------------------------------------------------------- 1 | type === MonitoredScheduledTaskLogItem::TYPE_STARTING; 12 | } 13 | 14 | public function url() 15 | { 16 | return "{$this->baseUrl()}/starting"; 17 | } 18 | 19 | public function data(): array 20 | { 21 | return []; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Support/ScheduledTasks/MonitoredScheduledTasks.php: -------------------------------------------------------------------------------- 1 | 'obj_234' => [ 'propertyName' => 'some_value' ]]] 14 | * ``` 15 | * 16 | * @see self::makeKey() 17 | * 18 | * @var array>> 19 | */ 20 | protected array $store = []; 21 | 22 | public function setMonitorName(object $target, string $monitorName): void 23 | { 24 | $this->setProperty($target, 'monitorName', $monitorName); 25 | } 26 | 27 | public function getMonitorName(object $target): ?string 28 | { 29 | return $this->getProperty($target, 'monitorName'); 30 | } 31 | 32 | public function setGraceTimeInMinutes(object $target, int $graceTimeInMinutes): void 33 | { 34 | $this->setProperty($target, 'graceTimeInMinutes', $graceTimeInMinutes); 35 | } 36 | 37 | public function getGraceTimeInMinutes(object $target): ?int 38 | { 39 | return $this->getProperty($target, 'graceTimeInMinutes'); 40 | } 41 | 42 | public function setDoNotMonitor(object $target, bool $doNotMonitor = true): void 43 | { 44 | $this->setProperty($target, 'doNotMonitor', $doNotMonitor); 45 | } 46 | 47 | public function getDoNotMonitor(object $target): ?bool 48 | { 49 | return $this->getProperty($target, 'doNotMonitor'); 50 | } 51 | 52 | public function setDoNotMonitorAtOhDear(object $target, bool $doNotMonitorAtOhDear = true): void 53 | { 54 | $this->setProperty($target, 'doNotMonitorAtOhDear', $doNotMonitorAtOhDear); 55 | } 56 | 57 | public function getDoNotMonitorAtOhDear(object $target): ?bool 58 | { 59 | return $this->getProperty($target, 'doNotMonitorAtOhDear'); 60 | } 61 | 62 | public function setStoreOutputInDb(object $target, bool $storeOutputInDb = true): void 63 | { 64 | $this->setProperty($target, 'storeOutputInDb', $storeOutputInDb); 65 | } 66 | 67 | public function getStoreOutputInDb(object $target): ?bool 68 | { 69 | return $this->getProperty($target, 'storeOutputInDb'); 70 | } 71 | 72 | 73 | protected function setProperty(object $target, string $key, mixed $value): void 74 | { 75 | data_set($this->store, $this->makeKey($target, $key), $value); 76 | } 77 | 78 | protected function getProperty(object $target, string $key): mixed 79 | { 80 | return data_get($this->store, $this->makeKey($target, $key)); 81 | } 82 | 83 | protected function makeKey(object $target, string $key): array 84 | { 85 | return [ 86 | $target::class, 87 | spl_object_hash($target), 88 | $key, 89 | ]; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Support/ScheduledTasks/ScheduledTaskFactory.php: -------------------------------------------------------------------------------- 1 | first(fn (string $taskClass) => $taskClass::canHandleEvent($event)); 23 | 24 | return new $taskClass($event); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Support/ScheduledTasks/ScheduledTasks.php: -------------------------------------------------------------------------------- 1 | schedule = $schedule; 26 | 27 | $this->tasks = collect($this->schedule->events()) 28 | ->filter( 29 | fn (Event $event): bool => $event->runsInEnvironment(config('app.env')) 30 | ) 31 | ->map( 32 | fn (Event $event): Task => ScheduledTaskFactory::createForEvent($event) 33 | ); 34 | } 35 | 36 | public function uniqueTasks(): Collection 37 | { 38 | return $this->tasks 39 | ->filter(fn (Task $task) => $task->shouldMonitor()) 40 | ->reject(fn (Task $task) => empty($task->name())) 41 | ->unique(fn (Task $task) => $task->name()) 42 | ->values(); 43 | } 44 | 45 | public function monitoredAtOhDear() 46 | { 47 | return $this->uniqueTasks() 48 | ->filter(fn (Task $task) => $task->shouldMonitorAtOhDear()); 49 | } 50 | 51 | public function duplicateTasks(): Collection 52 | { 53 | $uniqueTasksIds = $this->uniqueTasks() 54 | ->map(fn (Task $task) => $task->uniqueId()) 55 | ->toArray(); 56 | 57 | return $this->tasks 58 | ->filter(fn (Task $task) => $task->shouldMonitor()) 59 | ->reject(fn (Task $task) => empty($task->name())) 60 | ->reject(fn (Task $task) => in_array($task->uniqueId(), $uniqueTasksIds)) 61 | ->values(); 62 | } 63 | 64 | public function readyForMonitoringTasks(): Collection 65 | { 66 | return $this->uniqueTasks() 67 | ->reject(fn (Task $task) => $task->isBeingMonitored()); 68 | } 69 | 70 | public function monitoredTasks(): Collection 71 | { 72 | return $this->uniqueTasks() 73 | ->filter(fn (Task $task) => $task->isBeingMonitored()); 74 | } 75 | 76 | public function unmonitoredTasks(): Collection 77 | { 78 | return $this->tasks->reject(fn (Task $task) => $task->shouldMonitor()); 79 | } 80 | 81 | public function unnamedTasks(): Collection 82 | { 83 | return $this->tasks 84 | ->filter(fn (Task $task) => empty($task->name())) 85 | ->values(); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Support/ScheduledTasks/Tasks/ClosureTask.php: -------------------------------------------------------------------------------- 1 | getSummaryForDisplay(), ['Closure', 'Callback']); 17 | } 18 | 19 | public function type(): string 20 | { 21 | return 'closure'; 22 | } 23 | 24 | public function defaultName(): ?string 25 | { 26 | return null; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Support/ScheduledTasks/Tasks/CommandTask.php: -------------------------------------------------------------------------------- 1 | command, self::artisanString()); 18 | } 19 | 20 | public function defaultName(): ?string 21 | { 22 | return Str::after($this->event->command, self::artisanString() . ' '); 23 | } 24 | 25 | public function type(): string 26 | { 27 | return 'command'; 28 | } 29 | 30 | public static function artisanString(): string 31 | { 32 | $baseString = 'artisan'; 33 | 34 | $quote = self::isRunningWindows() 35 | ? '"' 36 | : "'"; 37 | 38 | return "{$quote}{$baseString}{$quote}"; 39 | } 40 | 41 | protected static function isRunningWindows() 42 | { 43 | return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN'; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Support/ScheduledTasks/Tasks/JobTask.php: -------------------------------------------------------------------------------- 1 | command)) { 19 | return false; 20 | } 21 | 22 | if (empty($event->description)) { 23 | return false; 24 | } 25 | 26 | return class_exists($event->description); 27 | } 28 | 29 | public function defaultName(): ?string 30 | { 31 | return $this->event->description; 32 | } 33 | 34 | public function type(): string 35 | { 36 | return 'job'; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Support/ScheduledTasks/Tasks/ShellTask.php: -------------------------------------------------------------------------------- 1 | event->command, 255); 23 | } 24 | 25 | public function type(): string 26 | { 27 | return 'shell'; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Support/ScheduledTasks/Tasks/Task.php: -------------------------------------------------------------------------------- 1 | event = $event; 36 | 37 | $this->uniqueId = (string)Str::uuid(); 38 | 39 | if (! empty($this->name())) { 40 | $this->monitoredScheduledTask = $this->getMonitoredScheduleTaskModel()->findByName($this->name()); 41 | } 42 | } 43 | 44 | public function uniqueId(): string 45 | { 46 | return $this->uniqueId; 47 | } 48 | 49 | public function name(): ?string 50 | { 51 | return $this->getMonitoredScheduledTasks()->getMonitorName($this->event) 52 | ?? $this->defaultName(); 53 | } 54 | 55 | public function shouldMonitor(): bool 56 | { 57 | $doNotMonitor = $this->getMonitoredScheduledTasks() 58 | ->getDoNotMonitor($this->event); 59 | if (! isset($doNotMonitor)) { 60 | return true; 61 | } 62 | 63 | return ! $doNotMonitor; 64 | } 65 | 66 | public function isBeingMonitored(): bool 67 | { 68 | return ! is_null($this->monitoredScheduledTask); 69 | } 70 | 71 | public function shouldMonitorAtOhDear(): bool 72 | { 73 | $doNotMonitorAtOhDear = $this->getMonitoredScheduledTasks() 74 | ->getDoNotMonitorAtOhDear($this->event); 75 | if (! isset($doNotMonitorAtOhDear)) { 76 | return true; 77 | } 78 | 79 | return ! $doNotMonitorAtOhDear; 80 | } 81 | 82 | public function isBeingMonitoredAtOhDear(): bool 83 | { 84 | if (! $this->isBeingMonitored()) { 85 | return false; 86 | } 87 | 88 | if (! $this->shouldMonitorAtOhDear()) { 89 | return false; 90 | } 91 | 92 | return ! empty($this->monitoredScheduledTask->ping_url); 93 | } 94 | 95 | public function previousRunAt(): CarbonInterface 96 | { 97 | $dateTime = (new CronExpression($this->cronExpression()))->getPreviousRunDate(now()); 98 | 99 | return Date::instance($dateTime); 100 | } 101 | 102 | public function nextRunAt(?CarbonInterface $now = null): CarbonInterface 103 | { 104 | $dateTime = (new CronExpression($this->cronExpression()))->getNextRunDate( 105 | $now ?? now(), 106 | 0, 107 | false, 108 | $this->timezone() 109 | ); 110 | 111 | $date = Date::instance($dateTime); 112 | 113 | $date->setTimezone(config('app.timezone')); 114 | 115 | return $date; 116 | } 117 | 118 | public function lastRunStartedAt(): ?CarbonInterface 119 | { 120 | return optional($this->monitoredScheduledTask)->last_started_at; 121 | } 122 | 123 | public function lastRunFinishedAt(): ?CarbonInterface 124 | { 125 | return optional($this->monitoredScheduledTask)->last_finished_at; 126 | } 127 | 128 | public function lastRunFailedAt(): ?CarbonInterface 129 | { 130 | return optional($this->monitoredScheduledTask)->last_failed_at; 131 | } 132 | 133 | public function lastRunSkippedAt(): ?CarbonInterface 134 | { 135 | return optional($this->monitoredScheduledTask)->last_skipped_at; 136 | } 137 | 138 | public function lastRunFinishedTooLate(): bool 139 | { 140 | if (! $this->isBeingMonitored()) { 141 | return false; 142 | } 143 | 144 | $lastFinishedAt = $this->lastRunFinishedAt() 145 | ? $this->lastRunFinishedAt() 146 | : $this->monitoredScheduledTask->created_at->subSecond(); 147 | 148 | $expectedNextRunStart = $this->nextRunAt($lastFinishedAt); 149 | $shouldHaveFinishedAt = $expectedNextRunStart->addMinutes($this->graceTimeInMinutes()); 150 | 151 | return $shouldHaveFinishedAt->isPast(); 152 | } 153 | 154 | public function lastRunFailed(): bool 155 | { 156 | if (! $this->isBeingMonitored()) { 157 | return false; 158 | } 159 | 160 | if (! $lastRunFailedAt = $this->lastRunFailedAt()) { 161 | return false; 162 | } 163 | 164 | if (! $lastRunStartedAt = $this->lastRunStartedAt()) { 165 | return true; 166 | } 167 | 168 | return $lastRunFailedAt->isAfter($lastRunStartedAt->subSecond()); 169 | } 170 | 171 | public function graceTimeInMinutes() 172 | { 173 | return $this->getMonitoredScheduledTasks()->getGraceTimeInMinutes($this->event) 174 | ?? config('schedule-monitor.oh_dear.grace_time_in_minutes', 5); 175 | } 176 | 177 | public function cronExpression(): string 178 | { 179 | return $this->event->getExpression(); 180 | } 181 | 182 | public function timezone(): string 183 | { 184 | return (string)$this->event->timezone; 185 | } 186 | 187 | public function humanReadableCron(): string 188 | { 189 | try { 190 | return CronTranslator::translate($this->cronExpression()); 191 | } catch (CronParsingException $exception) { 192 | return $this->cronExpression(); 193 | } 194 | } 195 | 196 | public function runsInBackground():bool 197 | { 198 | return $this->event->runInBackground; 199 | } 200 | } 201 | --------------------------------------------------------------------------------