Chris
Chris

Reputation: 58242

How to capture output from a non-command scheduled task in Laravel?

Given the following snippet:

    $schedule->call(function () {
        // echo "HELLO 123";   // nope
        // return "HELLO 123"; // also nope
    })
        ->everyMinute()
        ->sendOutputTo(storage_path('logs/cron/hello.cron.log'))
        ->emailOutputTo('[email protected]');

The scheduled task is running, the emails are being generated, but with no content. I can capture output via the $schedule->exec and $schedule->command.

Ideal end state here is, run a few internal, non-command processes within the call method and output the results into a file/email.

Upvotes: 7

Views: 2388

Answers (3)

halloei
halloei

Reputation: 2046

I find it quiet unsatisfying that Laravel does not support running multiple Artisan commands consecutively, including capturing their outputs and stopping in case of exceptions.

\Storage::append() from @Chris' answer does only write into the storage/app/ directory, but I wanted the logs in storage/logs/. So I replaced it with \File::append().
However, my log file storage/logs/schedule.log is a symbolic link in production environment and file_put_contents(), on which both methods rely on, is not able to write to symbolic links.

This is the solution I came up with:

use Illuminate\Support\Facades\Artisan;
use Symfony\Component\Console\Output\OutputInterface;

// ...

$schedule->callWithOutput(function (OutputInterface $output) {
    Artisan::call(FooCommand::class, outputBuffer: $output);
    Artisan::call(BarCommand::class, outputBuffer: $output);
}, storage_path('logs/schedule.log'))>everyMinute();

Schedule::callWithOutput() is a macro that takes our callback and captures all output in a BufferedOutput(). It is eventually redirected to the log file (second argument) with shell_exec(), so it works with regular files and symlinks:

use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Support\Facades\App;
use Illuminate\Support\ServiceProvider;
use Symfony\Component\Console\Output\BufferedOutput;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Schedule::macro('callWithOutput', function (callable $callback, string $logFile, array $parameters = []) {
            /** @var Schedule $this */
            return $this->call(function () use ($callback, $logFile, $parameters) {
                $output = new BufferedOutput();

                try {
                    App::call($callback, compact('output') + $parameters);
                } finally {
                    shell_exec("echo -n '{$output->fetch()}' >> $logFile");
                }
            });
        });
    }
}

You can also pass $parameters (second parameter of Schedule::call()) to Schedule::callWithOutput() as third parameter.

If you're not executing Artisan commands, you can write output with $output->write() or $output->writeln().

Upvotes: 0

bloodstix
bloodstix

Reputation: 142

I just googled the documentation of the class you are using with the words laravel scheduled output and the documentation (Just above the anchor in a red box) states:

Note: The emailOutputTo and sendOutputTo methods are exclusive to the 
command method and are not supported for call.

Hope that helps.

Upvotes: 5

Chris
Chris

Reputation: 58242

The accepted answer got me to the solution, but for future reference:

$schedule->call(function () {
        // Do stuff

        // Report to file
        \Storage::append('logs/cron/hello.cron.log', "HELLO 123");
    })->everyMinute();

Upvotes: 2

Related Questions