Last active
March 27, 2025 10:34
-
-
Save carestad/56cb8f45e63ef992544c8c97efaab464 to your computer and use it in GitHub Desktop.
Alternative Laravel Octane Roadrunner command, with access logging enabled and proper error logs working
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Console\Commands; | |
use Illuminate\Support\Str; | |
use Laravel\Octane\Commands\StartRoadRunnerCommand; | |
use Laravel\Octane\RoadRunner\ServerProcessInspector; | |
use Laravel\Octane\RoadRunner\ServerStateFile; | |
use Symfony\Component\Process\PhpExecutableFinder; | |
use Symfony\Component\Process\Process; | |
class OctaneRoadrunnerCommand extends StartRoadRunnerCommand | |
{ | |
public $signature = 'octane:roadrunner | |
{--host= : The IP address the server should bind to} | |
{--port= : The port the server should be available on} | |
{--rpc-host= : The RPC IP address the server should bind to} | |
{--rpc-port= : The RPC port the server should be available on} | |
{--workers=auto : The number of workers that should be available to handle requests} | |
{--max-requests=500 : The number of requests to process before reloading the server} | |
{--rr-config= : The path to the RoadRunner .rr.yaml file} | |
{--watch : Automatically reload the server when the application is modified} | |
{--poll : Use file system polling while watching in order to watch files over a network} | |
{--log-level= : Log messages at or above the specified log level} | |
{--no-access-log : Disable access log} | |
'; | |
public function handle(ServerProcessInspector $inspector, ServerStateFile $serverStateFile) | |
{ | |
if (! $this->isRoadRunnerInstalled()) { | |
$this->components->error('RoadRunner not installed. Please execute the `octane:install` Artisan command.'); | |
return 1; | |
} | |
$roadRunnerBinary = $this->ensureRoadRunnerBinaryIsInstalled(); | |
$this->ensurePortIsAvailable(); | |
if ($inspector->serverIsRunning()) { | |
$this->components->error('RoadRunner server is already running.'); | |
return 1; | |
} | |
$this->ensureRoadRunnerBinaryMeetsRequirements($roadRunnerBinary); | |
$this->writeServerStateFile($serverStateFile); | |
$this->forgetEnvironmentVariables(); | |
$server = tap(new Process(array_filter([ | |
$roadRunnerBinary, | |
'-c', $this->configPath(), | |
'-o', 'version=3', | |
'-o', 'http.address=' . $this->getHost() . ':' . $this->getPort(), | |
'-o', 'server.command=' . (new PhpExecutableFinder)->find() . ',' . base_path(config('octane.roadrunner.command', 'vendor/bin/roadrunner-worker')), | |
'-o', 'http.pool.num_workers=' . $this->workerCount(), | |
'-o', 'http.pool.max_jobs=' . $this->option('max-requests'), | |
'-o', 'rpc.listen=tcp://' . $this->rpcHost() . ':' . $this->rpcPort(), | |
'-o', 'http.pool.supervisor.exec_ttl=' . $this->maxExecutionTime(), | |
'-o', 'http.static.dir=' . public_path(), | |
'-o', 'http.middleware=' . config('octane.roadrunner.http_middleware', 'static'), | |
'-o', 'logs.mode=production', | |
'-o', 'logs.level=' . ($this->option('log-level') ?: (app()->environment('local') ? 'debug' : 'warn')), | |
'-o', 'logs.output=stdout', | |
'-o', 'http.access_logs=' . ($this->option('no-access-log') ? 'false' : 'true'), | |
'-o', 'logs.encoding=json', | |
'serve', | |
]), base_path(), [ | |
'APP_ENV' => app()->environment(), | |
'APP_BASE_PATH' => base_path(), | |
'LARAVEL_OCTANE' => 1, | |
]))->start(); | |
$serverStateFile->writeProcessId($server->getPid()); | |
return $this->runServer($server, $inspector, 'roadrunner'); | |
} | |
/** | |
* Write the server process output to the console. | |
* | |
* @param \Symfony\Component\Process\Process $server | |
* @return void | |
*/ | |
protected function writeServerOutput($server) | |
{ | |
[$output, $errorOutput] = $this->getServerOutput($server); | |
Str::of($output) | |
->explode("\n") | |
->filter() | |
->each(function ($output) { | |
if (! is_array($debug = json_decode($output, true))) { | |
return $this->components->info($output); | |
} | |
if (is_array($stream = json_decode($debug['msg'], true))) { | |
return $this->handleStream($stream); | |
} | |
if ($debug['logger'] == 'server') { | |
if (Str::contains($debug['msg'], ['.DEBUG', '.INFO', '.WARN', '.ERROR'])) { | |
return $this->outputError($debug['msg']); | |
} | |
return $this->raw($debug['msg']); | |
} | |
if ( | |
! $this->option('no-access-log') && | |
$debug['level'] == 'info' | |
&& isset($debug['remote_address']) | |
&& isset($debug['msg']) | |
&& $debug['msg'] == 'http access log' | |
) { | |
return $this->accessLog($debug); | |
} | |
}); | |
Str::of($errorOutput) | |
->explode("\n") | |
->filter() | |
->each(function ($output) { | |
if (! Str::contains($output, ['DEBUG', 'INFO', 'WARN'])) { | |
$this->components->error($output); | |
} | |
}); | |
} | |
private function accessLog(array $request, $verbosity = null) | |
{ | |
$duration = number_format(round($this->calculateElapsedTime($request['elapsed']), 2), 2, '.', ''); | |
$this->line(sprintf('%s [%s] %s %s %s (%s ms) %s %s %s', ...[ | |
$request['remote_address'], | |
$request['time_local'], | |
$request['method'], | |
$request['URI'] ?: '/', | |
$request['status'], | |
$duration, | |
$request['write_bytes'], | |
$request['referer'] ?: '-', | |
$request['user_agent'] ?: '-', | |
])); | |
} | |
private function outputError($output): void | |
{ | |
$logLevel = $this->option('log-level') ?: (app()->environment('local') ? 'debug' : 'warn'); | |
$levelsToInclude = match($logLevel) { | |
'debug' => ['DEBUG', 'INFO', 'WARN', 'WARNING', 'ERROR'], | |
'info' => ['INFO', 'WARN', 'WARNING', 'ERROR'], | |
'warn', 'warning' => ['WARN', 'WARNING', 'ERROR'], | |
default => ['ERROR'], | |
}; | |
preg_match('/(\w+?)\.(DEBUG|INFO|(WARNING|WARN)|ERROR)/', $output, $matches); | |
@[$hostAndLevel, , $errorLogLevel] = $matches; | |
$errorLogMethod = match ($errorLogLevel) { | |
'WARN', 'WARNING' => 'warn', | |
default => mb_strtolower($errorLogLevel), | |
}; | |
if (in_array($errorLogLevel, $levelsToInclude) && method_exists($this, $errorLogMethod)) { | |
$this->{$errorLogMethod}(str($output)->replace($hostAndLevel . ': ', '')->trim()->toString()); | |
} | |
} | |
/** | |
* Debug log message. | |
*/ | |
private function debug(string $message) | |
{ | |
$this->label($message, null, 'DEBUG', 'gray', 'white'); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment