Skip to content

Instantly share code, notes, and snippets.

@carestad
Last active March 27, 2025 10:34
Show Gist options
  • Save carestad/56cb8f45e63ef992544c8c97efaab464 to your computer and use it in GitHub Desktop.
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
<?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