Skip to content

Instantly share code, notes, and snippets.

@Radiergummi
Last active November 7, 2023 07:22
Show Gist options
  • Save Radiergummi/beb80325543c5b8bcd611f7229d3608a to your computer and use it in GitHub Desktop.
Save Radiergummi/beb80325543c5b8bcd611f7229d3608a to your computer and use it in GitHub Desktop.
Formatter for Laravel's artisan serve command that will preserve log output

This log formatter will preserve log output generated by the application and written to standard out/err when running on artisan serve. Users can still specify a custom log format, or even bring their own date format; we prefix all log messages with a special Artisan date format string that is the same as the one generated by the built-in HTTP server.

This works by overriding the log formatter in the app process using an environment variable, while preserving the original formatter in a second variable. On formatter creatiom time, we create an instance of the original formatter and return its output, prefixed with the serve-save date sequence.

Note: This requires two changes in ServeCommand:

  1. The explode call in the prcoess output handling needs to be limited to two segments:
    } elseif (!empty($line)) {
    -   $warning = explode('] ', $line);
    +   $warning = explode('] ', $line, 2);
        $this->components->warn(count($warning) > 1 ? $warning[1] : $warning[0]);
    }
  2. The environment needs to by extended with the formatter variables:
    protected function startProcess($hasEnvironment)
    {
    -   $process = new Process($this->serverCommand(), public_path(), collect($_ENV)->mapWithKeys(function ($value, $key) use ($hasEnvironment) {
    +   $env = collect($_ENV)->mapWithKeys(function ($value, $key) use ($hasEnvironment) {
    -       if ($this->option('no-reload') || ! $hasEnvironment) {
    -           return [$key => $value];
    -       }
    -
    -       return in_array($key, static::$passthroughVariables) ? [$key => $value] : [$key => false];
    -   })->all());
    +   });
    +   $process = new Process($this->serverCommand(), public_path(), $env->merge([
    +           'ORIGINAL_LOG_FORMATTER' => $env->get('LOG_FORMATTER', config('logging.channels.stderr.formatter')),
    +           'LOG_FORMATTER' =>   ArtisanServeFormatter::class,
    +       ])->all()
    +   );
    
        $process->start($this->handleProcessOutput());
    
        return $process;
    }

With these two changes and the formatter in place, logs will be printed to the console in an arbitrary, user-defined, format. I can't call it elegant with a straight face, but at least most of the heavy lifting will be handled by the existing logging infrastructure.

Ideally, the ServeCommand would either not need to split process output at all, or employ better heuristics to do it… If that isn't possible, the suggestion here could be improved by setting up some way to let Laravel know it's running within a serve process, and accomodate to that.

<?php
declare(strict_types=1);
namespace App\Support;
use DateTime;
use Monolog\Formatter\FormatterInterface;
use Monolog\Formatter\LineFormatter;
use Monolog\LogRecord;
use function app;
use function config;
use function env;
class ArtisanServeFormatter implements FormatterInterface
{
private readonly FormatterInterface $formatter;
public function __construct()
{
$formatterClass = env('ORIGINAL_LOG_FORMATTER', LineFormatter::class);
$this->formatter = app()->make($formatterClass, config('logging.channels.stderr.formatter_with', []));
}
/**
* @param LogRecord $record
* @return string
*/
public function format(LogRecord $record): string
{
$dateTime = (new DateTime())->format('D M d H:i:s Y');
return "[{$dateTime}] " . $this->formatter->format($record);
}
public function formatBatch(array $records): string
{
$message = '';
foreach ($records as $record) {
$message .= $this->format($record);
}
return $message;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment