Initial commit
This commit is contained in:
21
vendor/laravel/pail/LICENSE.md
vendored
Executable file
21
vendor/laravel/pail/LICENSE.md
vendored
Executable file
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) Taylor Otwell
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
69
vendor/laravel/pail/composer.json
vendored
Normal file
69
vendor/laravel/pail/composer.json
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"name": "laravel/pail",
|
||||
"description": "Easily delve into your Laravel application's log files directly from the command line.",
|
||||
"keywords": ["php", "tail", "laravel", "logs"],
|
||||
"homepage": "https://github.com/laravel/pail",
|
||||
"license": "MIT",
|
||||
"support": {
|
||||
"issues": "https://github.com/laravel/pail/issues",
|
||||
"source": "https://github.com/laravel/pail"
|
||||
},
|
||||
"authors": [
|
||||
{
|
||||
"name": "Taylor Otwell",
|
||||
"email": "taylor@laravel.com"
|
||||
},
|
||||
{
|
||||
"name": "Nuno Maduro",
|
||||
"email": "enunomaduro@gmail.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"ext-mbstring": "*",
|
||||
"illuminate/console": "^10.24|^11.0",
|
||||
"illuminate/contracts": "^10.24|^11.0",
|
||||
"illuminate/log": "^10.24|^11.0",
|
||||
"illuminate/process": "^10.24|^11.0",
|
||||
"illuminate/support": "^10.24|^11.0",
|
||||
"nunomaduro/termwind": "^1.15|^2.0",
|
||||
"symfony/console": "^6.0|^7.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"laravel/framework": "^10.24|^11.0",
|
||||
"laravel/pint": "^1.13",
|
||||
"orchestra/testbench-core": "^8.12|^9.0",
|
||||
"pestphp/pest": "^2.20",
|
||||
"pestphp/pest-plugin-type-coverage": "^2.3",
|
||||
"phpstan/phpstan": "^1.10",
|
||||
"symfony/var-dumper": "^6.3|^7.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Laravel\\Pail\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "1.x-dev"
|
||||
},
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Laravel\\Pail\\PailServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"sort-packages": true,
|
||||
"allow-plugins": {
|
||||
"pestphp/pest-plugin": true
|
||||
}
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true
|
||||
}
|
||||
107
vendor/laravel/pail/src/Console/Commands/PailCommand.php
vendored
Normal file
107
vendor/laravel/pail/src/Console/Commands/PailCommand.php
vendored
Normal file
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Pail\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Process\Exceptions\ProcessTimedOutException;
|
||||
use Laravel\Pail\File;
|
||||
use Laravel\Pail\Guards\EnsurePcntlIsAvailable;
|
||||
use Laravel\Pail\Options;
|
||||
use Laravel\Pail\ProcessFactory;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Process\Exception\ProcessSignaledException;
|
||||
|
||||
use function Termwind\render;
|
||||
use function Termwind\renderUsing;
|
||||
|
||||
#[AsCommand(name: 'pail')]
|
||||
class PailCommand extends Command
|
||||
{
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected $signature = 'pail
|
||||
{--filter= : Filter the logs by the given value}
|
||||
{--message= : Filter the logs by the given message}
|
||||
{--level= : Filter the logs by the given level}
|
||||
{--auth= : Filter the logs by the given authenticated ID}
|
||||
{--user= : Filter the logs by the given authenticated ID (alias for --auth)}
|
||||
{--timeout=3600 : The maximum execution time in seconds}';
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected $description = 'Tails the application logs.';
|
||||
|
||||
/**
|
||||
* The file instance, if any.
|
||||
*/
|
||||
protected ?File $file = null;
|
||||
|
||||
/**
|
||||
* Handles the command execution.
|
||||
*/
|
||||
public function handle(ProcessFactory $processFactory): void
|
||||
{
|
||||
EnsurePcntlIsAvailable::check();
|
||||
|
||||
renderUsing($this->output);
|
||||
render(<<<'HTML'
|
||||
<div class="max-w-150 mx-2 mt-1 flex">
|
||||
<div>
|
||||
<span class="px-1 bg-blue uppercase text-white">INFO</span>
|
||||
<span class="flex-1">
|
||||
<span class="ml-1 ">Tailing application logs.</span>
|
||||
</span>
|
||||
</div>
|
||||
<span class="flex-1"></span>
|
||||
<span class="text-gray ml-1">
|
||||
<span class="text-gray">Press Ctrl+C to exit</span>
|
||||
</span>
|
||||
</div>
|
||||
HTML,
|
||||
);
|
||||
|
||||
render(<<<'HTML'
|
||||
<div class="max-w-150 mx-2 flex">
|
||||
<div>
|
||||
</div>
|
||||
<span class="flex-1"></span>
|
||||
<span class="text-gray ml-1">
|
||||
<span class="text-gray">Use -v|-vv to show more details</span>
|
||||
</span>
|
||||
</div>
|
||||
HTML,
|
||||
);
|
||||
|
||||
$this->file = new File(storage_path('pail/'.uniqid().'.pail'));
|
||||
$this->file->create();
|
||||
$this->trap([SIGINT, SIGTERM], fn () => $this->file->destroy());
|
||||
|
||||
$options = Options::fromCommand($this);
|
||||
|
||||
assert($this->file instanceof File);
|
||||
|
||||
try {
|
||||
$processFactory->run($this->file, $this->output, $this->laravel->basePath(), $options);
|
||||
} catch (ProcessSignaledException $e) {
|
||||
if (in_array($e->getSignal(), [SIGINT, SIGTERM], true)) {
|
||||
$this->newLine();
|
||||
}
|
||||
} catch (ProcessTimedOutException $e) {
|
||||
$this->components->info('Maximum execution time exceeded.');
|
||||
} finally {
|
||||
$this->file?->destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the object destruction.
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
if ($this->file) {
|
||||
$this->file->destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
13
vendor/laravel/pail/src/Contracts/Printer.php
vendored
Normal file
13
vendor/laravel/pail/src/Contracts/Printer.php
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Pail\Contracts;
|
||||
|
||||
use Laravel\Pail\ValueObjects\MessageLogged;
|
||||
|
||||
interface Printer
|
||||
{
|
||||
/**
|
||||
* Prints the given message logged.
|
||||
*/
|
||||
public function print(MessageLogged $messageLogged): void;
|
||||
}
|
||||
100
vendor/laravel/pail/src/File.php
vendored
Normal file
100
vendor/laravel/pail/src/File.php
vendored
Normal file
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Pail;
|
||||
|
||||
use Stringable;
|
||||
|
||||
class File implements Stringable
|
||||
{
|
||||
/**
|
||||
* The time to live of the file.
|
||||
*/
|
||||
protected const TTL = 3600;
|
||||
|
||||
/**
|
||||
* Creates a new instance of the file.
|
||||
*/
|
||||
public function __construct(
|
||||
protected string $file,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the file exists.
|
||||
*/
|
||||
public function create(): void
|
||||
{
|
||||
if (! $this->exists()) {
|
||||
$directory = dirname($this->file);
|
||||
|
||||
if (! is_dir($directory)) {
|
||||
mkdir($directory, 0755, true);
|
||||
|
||||
file_put_contents($directory.'/.gitignore', "*\n!.gitignore\n");
|
||||
}
|
||||
|
||||
touch($this->file);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the file exists.
|
||||
*/
|
||||
public function exists(): bool
|
||||
{
|
||||
return file_exists($this->file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the file.
|
||||
*/
|
||||
public function destroy(): void
|
||||
{
|
||||
if ($this->exists()) {
|
||||
unlink($this->file);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a log message to the file.
|
||||
*
|
||||
* @param array<string, mixed> $context
|
||||
*/
|
||||
public function log(string $level, string $message, array $context = []): void
|
||||
{
|
||||
if ($this->isStale()) {
|
||||
$this->destroy();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$loggerFactory = new LoggerFactory($this);
|
||||
|
||||
$logger = $loggerFactory->create();
|
||||
|
||||
$logger->log($level, $message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file as string.
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the file is staled.
|
||||
*/
|
||||
protected function isStale(): bool
|
||||
{
|
||||
$modificationTime = @filemtime($this->file);
|
||||
|
||||
if ($modificationTime === false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return time() - $modificationTime > static::TTL;
|
||||
}
|
||||
}
|
||||
30
vendor/laravel/pail/src/Files.php
vendored
Normal file
30
vendor/laravel/pail/src/Files.php
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Pail;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class Files
|
||||
{
|
||||
/**
|
||||
* Creates a new instance of the files.
|
||||
*/
|
||||
public function __construct(
|
||||
protected string $path,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of files.
|
||||
*
|
||||
* @return \Illuminate\Support\Collection<int, File>
|
||||
*/
|
||||
public function all(): Collection
|
||||
{
|
||||
$files = glob($this->path.'/*.pail') ?: [];
|
||||
|
||||
return collect($files)
|
||||
->map(fn (string $file) => new File($file));
|
||||
}
|
||||
}
|
||||
18
vendor/laravel/pail/src/Guards/EnsurePcntlIsAvailable.php
vendored
Normal file
18
vendor/laravel/pail/src/Guards/EnsurePcntlIsAvailable.php
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Pail\Guards;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class EnsurePcntlIsAvailable
|
||||
{
|
||||
/**
|
||||
* Checks if the pcntl extension is available.
|
||||
*/
|
||||
public static function check(): void
|
||||
{
|
||||
if (! function_exists('pcntl_fork')) {
|
||||
throw new RuntimeException('The [pcntl] extension is required to run Pail.');
|
||||
}
|
||||
}
|
||||
}
|
||||
121
vendor/laravel/pail/src/Handler.php
vendored
Normal file
121
vendor/laravel/pail/src/Handler.php
vendored
Normal file
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Pail;
|
||||
|
||||
use Illuminate\Console\Events\CommandStarting;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
use Illuminate\Foundation\Auth\User;
|
||||
use Illuminate\Log\Context\Repository as ContextRepository;
|
||||
use Illuminate\Log\Events\MessageLogged;
|
||||
use Illuminate\Queue\Events\JobExceptionOccurred;
|
||||
use Illuminate\Queue\Events\JobProcessing;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Throwable;
|
||||
|
||||
class Handler
|
||||
{
|
||||
/**
|
||||
* The last lifecycle captured event.
|
||||
*/
|
||||
protected CommandStarting|JobProcessing|JobExceptionOccurred|null $lastLifecycleEvent = null;
|
||||
|
||||
/**
|
||||
* The artisan command being executed, if any.
|
||||
*/
|
||||
protected ?string $artisanCommand = null;
|
||||
|
||||
/**
|
||||
* Creates a new instance of the handler.
|
||||
*/
|
||||
public function __construct(
|
||||
protected Container $container,
|
||||
protected Files $files,
|
||||
protected bool $runningInConsole,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports the given message logged.
|
||||
*/
|
||||
public function log(MessageLogged $messageLogged): void
|
||||
{
|
||||
$files = $this->files->all();
|
||||
|
||||
if ($files->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$context = $this->context($messageLogged);
|
||||
|
||||
$files->each(
|
||||
fn (File $file) => $file->log(
|
||||
$messageLogged->level,
|
||||
$messageLogged->message,
|
||||
$context,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the last application lifecycle event.
|
||||
*/
|
||||
public function setLastLifecycleEvent(CommandStarting|JobProcessing|JobExceptionOccurred|null $event): void
|
||||
{
|
||||
if ($event instanceof CommandStarting) {
|
||||
$this->artisanCommand = $event->command;
|
||||
}
|
||||
|
||||
$this->lastLifecycleEvent = $event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the context array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function context(MessageLogged $messageLogged): array
|
||||
{
|
||||
$context = ['__pail' => ['origin' => match (true) {
|
||||
$this->artisanCommand && $this->lastLifecycleEvent && in_array($this->lastLifecycleEvent::class, [JobProcessing::class, JobExceptionOccurred::class]) => [
|
||||
'type' => 'queue',
|
||||
'command' => $this->artisanCommand,
|
||||
'queue' => $this->lastLifecycleEvent->job->getQueue(),
|
||||
'job' => $this->lastLifecycleEvent->job->resolveName(),
|
||||
],
|
||||
$this->runningInConsole => [
|
||||
'type' => 'console',
|
||||
'command' => $this->artisanCommand,
|
||||
],
|
||||
default => [
|
||||
'type' => 'http',
|
||||
'method' => request()->method(),
|
||||
'path' => request()->path(),
|
||||
'auth_id' => Auth::id(),
|
||||
'auth_email' => Auth::user() instanceof User ? Auth::user()->email : null, // @phpstan-ignore property.notFound
|
||||
],
|
||||
}]];
|
||||
|
||||
if (isset($messageLogged->context['exception']) && $this->lastLifecycleEvent instanceof JobExceptionOccurred) {
|
||||
if ($messageLogged->context['exception'] === $this->lastLifecycleEvent->exception) {
|
||||
$this->setLastLifecycleEvent(null);
|
||||
}
|
||||
}
|
||||
|
||||
$context['__pail']['origin']['trace'] = isset($messageLogged->context['exception'])
|
||||
&& $messageLogged->context['exception'] instanceof Throwable ? collect($messageLogged->context['exception']->getTrace())
|
||||
->filter(fn (array $frame) => isset($frame['file']))
|
||||
->map(fn (array $frame) => [
|
||||
'file' => $frame['file'], // @phpstan-ignore offsetAccess.notFound
|
||||
'line' => $frame['line'] ?? null,
|
||||
])->values()
|
||||
: null;
|
||||
|
||||
return collect($messageLogged->context)
|
||||
->merge($context)
|
||||
->when($this->container->bound(ContextRepository::class), function (Collection $context) {
|
||||
return $context->merge($this->container->make(ContextRepository::class)->all()); // @phpstan-ignore method.nonObject
|
||||
})->toArray();
|
||||
}
|
||||
}
|
||||
32
vendor/laravel/pail/src/LoggerFactory.php
vendored
Normal file
32
vendor/laravel/pail/src/LoggerFactory.php
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Pail;
|
||||
|
||||
use Monolog\Formatter\JsonFormatter;
|
||||
use Monolog\Handler\StreamHandler;
|
||||
use Monolog\Level;
|
||||
use Monolog\Logger;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class LoggerFactory
|
||||
{
|
||||
/**
|
||||
* Creates a new instance of the logger factory.
|
||||
*/
|
||||
public function __construct(
|
||||
protected File $file,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance of the logger.
|
||||
*/
|
||||
public function create(): LoggerInterface
|
||||
{
|
||||
$handler = new StreamHandler($this->file->__toString(), Level::Debug);
|
||||
$handler->setFormatter(new JsonFormatter);
|
||||
|
||||
return new Logger('pail', [$handler]);
|
||||
}
|
||||
}
|
||||
76
vendor/laravel/pail/src/Options.php
vendored
Normal file
76
vendor/laravel/pail/src/Options.php
vendored
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Pail;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Laravel\Pail\ValueObjects\MessageLogged;
|
||||
|
||||
class Options
|
||||
{
|
||||
/**
|
||||
* Creates a new instance of the tail options.
|
||||
*/
|
||||
public function __construct(
|
||||
protected int $timeout,
|
||||
protected ?string $authId,
|
||||
protected ?string $level,
|
||||
protected ?string $filter,
|
||||
protected ?string $message,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance of the tail options from the given console command.
|
||||
*/
|
||||
public static function fromCommand(Command $command): static
|
||||
{
|
||||
$authId = $command->option('auth') ?? $command->option('user');
|
||||
assert(is_string($authId) || $authId === null);
|
||||
|
||||
$level = $command->option('level');
|
||||
assert(is_string($level) || $level === null);
|
||||
|
||||
$filter = $command->option('filter');
|
||||
assert(is_string($filter) || $filter === null);
|
||||
|
||||
$message = $command->option('message');
|
||||
assert(is_string($message) || $message === null);
|
||||
|
||||
$timeout = (int) $command->option('timeout');
|
||||
|
||||
return new static($timeout, $authId, $level, $filter, $message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the tail options accept the given message logged.
|
||||
*/
|
||||
public function accepts(MessageLogged $messageLogged): bool
|
||||
{
|
||||
if (is_string($this->authId) && $messageLogged->authId() !== $this->authId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (is_string($this->level) && strtolower($messageLogged->level()) !== strtolower($this->level)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (is_string($this->filter) && ! str_contains(strtolower((string) $messageLogged), strtolower($this->filter))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (is_string($this->message) && ! str_contains(strtolower($messageLogged->message()), strtolower($this->message))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of seconds before the process is killed.
|
||||
*/
|
||||
public function timeout(): int
|
||||
{
|
||||
return $this->timeout;
|
||||
}
|
||||
}
|
||||
80
vendor/laravel/pail/src/PailServiceProvider.php
vendored
Normal file
80
vendor/laravel/pail/src/PailServiceProvider.php
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Pail;
|
||||
|
||||
use Illuminate\Console\Events\CommandStarting;
|
||||
use Illuminate\Contracts\Foundation\Application;
|
||||
use Illuminate\Log\Events\MessageLogged;
|
||||
use Illuminate\Queue\Events\JobExceptionOccurred;
|
||||
use Illuminate\Queue\Events\JobProcessed;
|
||||
use Illuminate\Queue\Events\JobProcessing;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Laravel\Pail\Console\Commands\PailCommand;
|
||||
|
||||
class PailServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Registers the application services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->singleton(
|
||||
Files::class,
|
||||
fn (Application $app) => new Files($app->storagePath('pail'))
|
||||
);
|
||||
|
||||
$this->app->singleton(Handler::class, fn (Application $app) => new Handler(
|
||||
$app,
|
||||
$app->make(Files::class), // @phpstan-ignore argument.type
|
||||
$app->runningInConsole(),
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstraps the application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
if (! $this->runningPailTests() && ($this->app->runningUnitTests() || ($_ENV['VAPOR_SSM_PATH'] ?? false))) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var \Illuminate\Contracts\Events\Dispatcher $events */
|
||||
$events = $this->app->make('events');
|
||||
|
||||
$events->listen(MessageLogged::class, function (MessageLogged $messageLogged) {
|
||||
/** @var Handler $handler */
|
||||
$handler = $this->app->make(Handler::class);
|
||||
|
||||
$handler->log($messageLogged);
|
||||
});
|
||||
|
||||
$events->listen([CommandStarting::class, JobProcessing::class, JobExceptionOccurred::class], function (CommandStarting|JobProcessing|JobExceptionOccurred $lifecycleEvent) {
|
||||
/** @var Handler $handler */
|
||||
$handler = $this->app->make(Handler::class);
|
||||
|
||||
$handler->setLastLifecycleEvent($lifecycleEvent);
|
||||
});
|
||||
|
||||
$events->listen([JobProcessed::class], function () {
|
||||
/** @var Handler $handler */
|
||||
$handler = $this->app->make(Handler::class);
|
||||
|
||||
$handler->setLastLifecycleEvent(null);
|
||||
});
|
||||
|
||||
if ($this->app->runningInConsole()) {
|
||||
$this->commands([
|
||||
PailCommand::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the Pail's test suite is running.
|
||||
*/
|
||||
protected function runningPailTests(): bool
|
||||
{
|
||||
return $_ENV['PAIL_TESTS'] ?? false;
|
||||
}
|
||||
}
|
||||
278
vendor/laravel/pail/src/Printers/CliPrinter.php
vendored
Normal file
278
vendor/laravel/pail/src/Printers/CliPrinter.php
vendored
Normal file
@@ -0,0 +1,278 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Pail\Printers;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Pail\Contracts\Printer;
|
||||
use Laravel\Pail\ValueObjects\MessageLogged;
|
||||
use Laravel\Pail\ValueObjects\Origin\Http;
|
||||
use Laravel\Pail\ValueObjects\Origin\Queue;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
use function Termwind\render;
|
||||
use function Termwind\renderUsing;
|
||||
use function Termwind\terminal;
|
||||
|
||||
class CliPrinter implements Printer
|
||||
{
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function print(MessageLogged $messageLogged): void
|
||||
{
|
||||
$classOrType = $this->truncateClassOrType($messageLogged->classOrType());
|
||||
$color = $messageLogged->color();
|
||||
$message = $this->truncateMessage($messageLogged->message());
|
||||
$date = $this->output->isVerbose() ? $messageLogged->date() : $messageLogged->time();
|
||||
|
||||
$fileHtml = $this->fileHtml($messageLogged->file(), $classOrType);
|
||||
$messageHtml = $this->messageHtml($message);
|
||||
$optionsHtml = $this->optionsHtml($messageLogged);
|
||||
$traceHtml = $this->traceHtml($messageLogged);
|
||||
|
||||
$messageClasses = $this->output->isVerbose() ? '' : 'truncate';
|
||||
|
||||
$endingTopRight = $this->output->isVerbose() ? '' : '┐';
|
||||
$endingMiddle = $this->output->isVerbose() ? '' : '│';
|
||||
$endingBottomRight = $this->output->isVerbose() ? '' : '┘';
|
||||
|
||||
renderUsing($this->output);
|
||||
render(<<<HTML
|
||||
<div class="max-w-150">
|
||||
<div class="flex">
|
||||
<div>
|
||||
<span class="mr-1 text-gray">┌</span>
|
||||
<span class="text-gray">$date</span>
|
||||
<span class="px-1 text-$color font-bold">$classOrType</span>
|
||||
</div>
|
||||
<span class="flex-1 content-repeat-[─] text-gray"></span>
|
||||
<span class="text-gray">
|
||||
$fileHtml
|
||||
<span class="text-gray">$endingTopRight</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex $messageClasses">
|
||||
<span>
|
||||
<span class="mr-1 text-gray">│</span>
|
||||
$messageHtml
|
||||
</span>
|
||||
<span class="flex-1"></span>
|
||||
<span class="flex-1 text-gray text-right">$endingMiddle</span>
|
||||
</div>
|
||||
$traceHtml
|
||||
<div class="flex text-gray">
|
||||
<span>└</span>
|
||||
<span class="mr-1 flex-1 content-repeat-[─]"></span>
|
||||
$optionsHtml
|
||||
<span class="ml-1">$endingBottomRight</span>
|
||||
</div>
|
||||
</div>
|
||||
HTML);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance printer instance.
|
||||
*/
|
||||
public function __construct(protected OutputInterface $output, protected string $basePath)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the file html.
|
||||
*/
|
||||
protected function fileHtml(?string $file, string $classOrType): ?string
|
||||
{
|
||||
if (is_null($file)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($_ENV['PAIL_TESTS'] ?? false) {
|
||||
$file = $this->basePath.'/app/MyClass.php:12';
|
||||
}
|
||||
|
||||
$file = str_replace($this->basePath.'/', '', $file);
|
||||
|
||||
if (! $this->output->isVerbose()) {
|
||||
$file = Str::of($file)
|
||||
->explode('/')
|
||||
->when(
|
||||
fn (Collection $file) => $file->count() > 4,
|
||||
fn (Collection $file) => $file->take(2)->merge(
|
||||
['…', (string) $file->last()],
|
||||
),
|
||||
)->implode('/');
|
||||
|
||||
$fileSize = max(0, min(terminal()->width() - strlen($classOrType) - 16, 145));
|
||||
|
||||
if (strlen($file) > $fileSize) {
|
||||
$file = mb_substr($file, 0, $fileSize).'…';
|
||||
}
|
||||
}
|
||||
|
||||
if ($file === '…') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$file = str_replace('……', '…', $file);
|
||||
|
||||
return <<<HTML
|
||||
<span class="text-gray mx-1">
|
||||
$file
|
||||
</span>
|
||||
HTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the message html.
|
||||
*/
|
||||
protected function messageHtml(string $message): string
|
||||
{
|
||||
if (empty($message)) {
|
||||
return '<span class="text-gray">No message.</span>';
|
||||
}
|
||||
|
||||
$message = htmlspecialchars($message);
|
||||
|
||||
return "<span>$message</span>";
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncates the class or type, if needed.
|
||||
*/
|
||||
protected function truncateClassOrType(string $classOrType): string
|
||||
{
|
||||
if ($this->output->isVerbose()) {
|
||||
return $classOrType;
|
||||
}
|
||||
|
||||
return Str::of($classOrType)
|
||||
->explode('\\')
|
||||
->when(
|
||||
fn (Collection $classOrType) => $classOrType->count() > 4,
|
||||
fn (Collection $classOrType) => $classOrType->take(2)->merge(
|
||||
['…', (string) $classOrType->last()]
|
||||
),
|
||||
)->implode('\\');
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncates the message, if needed.
|
||||
*/
|
||||
protected function truncateMessage(string $message): string
|
||||
{
|
||||
if (! $this->output->isVerbose()) {
|
||||
$messageSize = max(0, min(terminal()->width() - 5, 145));
|
||||
|
||||
if (strlen($message) > $messageSize) {
|
||||
$message = mb_substr($message, 0, $messageSize).'…';
|
||||
}
|
||||
}
|
||||
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the options html.
|
||||
*/
|
||||
public function optionsHtml(MessageLogged $messageLogged): string
|
||||
{
|
||||
$origin = $messageLogged->origin();
|
||||
|
||||
if ($origin instanceof Http) {
|
||||
if (str_starts_with($path = $origin->path, '/') === false) {
|
||||
$path = '/'.$origin->path;
|
||||
}
|
||||
|
||||
$options = [
|
||||
strtoupper($origin->method) => $path,
|
||||
'Auth ID' => $origin->authId
|
||||
? ($origin->authId.($origin->authEmail ? " ({$origin->authEmail})" : ''))
|
||||
: 'guest',
|
||||
];
|
||||
} elseif ($origin instanceof Queue) {
|
||||
$options = [
|
||||
$origin->command ? "artisan {$origin->command}" : null,
|
||||
$origin->queue,
|
||||
$origin->job,
|
||||
];
|
||||
} else {
|
||||
$options = [
|
||||
$origin->command ? "artisan {$origin->command}" : 'artisan',
|
||||
];
|
||||
}
|
||||
|
||||
return collect($options)->merge(
|
||||
$messageLogged->context() // @phpstan-ignore argument.type
|
||||
)->reject(fn (mixed $value, string|int $key) => is_int($key) && is_null($value))
|
||||
->map(fn (mixed $value) => is_string($value) ? $value : var_export($value, true))
|
||||
->map(fn (string $value) => htmlspecialchars($value))
|
||||
->map(fn (string $value, string|int $key) => is_string($key) ? "$key: $value" : $value)
|
||||
->map(fn (string $value) => "<span class=\"font-bold\">$value</span>")
|
||||
->implode(' • ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the trace html.
|
||||
*/
|
||||
public function traceHtml(MessageLogged $messageLogged): string
|
||||
{
|
||||
if (! $this->output->isVeryVerbose()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$trace = $messageLogged->trace();
|
||||
|
||||
if ($_ENV['PAIL_TESTS'] ?? false) {
|
||||
$trace = [
|
||||
[
|
||||
'line' => 12,
|
||||
'file' => $this->basePath.'/app/MyClass.php',
|
||||
],
|
||||
[
|
||||
'line' => 34,
|
||||
'file' => $this->basePath.'/app/MyClass.php',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
if (is_null($trace)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return collect($trace)
|
||||
->map(function (array $frame, int $index) {
|
||||
$number = $index + 1;
|
||||
|
||||
[
|
||||
'line' => $line,
|
||||
'file' => $file,
|
||||
] = $frame;
|
||||
|
||||
$file = str_replace($this->basePath.'/', '', $file);
|
||||
|
||||
$remainingTraces = '';
|
||||
|
||||
if (! $this->output->isVerbose()) {
|
||||
$file = (string) Str::of($file)
|
||||
->explode('/')
|
||||
->when(
|
||||
fn (Collection $file) => $file->count() > 4,
|
||||
fn (Collection $file) => $file->take(2)->merge(
|
||||
['…', (string) $file->last()],
|
||||
),
|
||||
)->implode('/');
|
||||
}
|
||||
|
||||
return <<<HTML
|
||||
<div class="flex text-gray">
|
||||
<span>
|
||||
<span class="mr-1 text-gray">│</span>
|
||||
<span>$number. $file:$line $remainingTraces</span>
|
||||
</span>
|
||||
</div>
|
||||
HTML;
|
||||
})->implode('');
|
||||
}
|
||||
}
|
||||
56
vendor/laravel/pail/src/ProcessFactory.php
vendored
Normal file
56
vendor/laravel/pail/src/ProcessFactory.php
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Pail;
|
||||
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Pail\Printers\CliPrinter;
|
||||
use Laravel\Pail\ValueObjects\MessageLogged;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class ProcessFactory
|
||||
{
|
||||
/**
|
||||
* Creates a new instance of the process factory.
|
||||
*/
|
||||
public function run(File $file, OutputInterface $output, string $basePath, Options $options): void
|
||||
{
|
||||
$printer = new CliPrinter($output, $basePath);
|
||||
|
||||
$remainingBuffer = '';
|
||||
|
||||
Process::timeout($options->timeout())
|
||||
->tty(false)
|
||||
->run(
|
||||
$this->command($file),
|
||||
function (string $type, string $buffer) use ($options, $printer, &$remainingBuffer) {
|
||||
$lines = Str::of($buffer)->explode("\n");
|
||||
|
||||
if ($remainingBuffer !== '' && isset($lines[0])) {
|
||||
$lines[0] = $remainingBuffer.$lines[0];
|
||||
$remainingBuffer = '';
|
||||
}
|
||||
|
||||
if ($lines->last() === '') {
|
||||
$lines = $lines->slice(0, -1);
|
||||
} elseif (! str_ends_with((string) $lines->last(), "\n")) {
|
||||
$remainingBuffer = $lines->pop();
|
||||
}
|
||||
|
||||
$lines
|
||||
->filter(fn (string $line) => $line !== '')
|
||||
->map(fn (string $line) => MessageLogged::fromJson($line))
|
||||
->filter(fn (MessageLogged $messageLogged) => $options->accepts($messageLogged))
|
||||
->each(fn (MessageLogged $messageLogged) => $printer->print($messageLogged));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw command.
|
||||
*/
|
||||
protected function command(File $file): string
|
||||
{
|
||||
return '\\tail -F "'.$file->__toString().'"';
|
||||
}
|
||||
}
|
||||
180
vendor/laravel/pail/src/ValueObjects/MessageLogged.php
vendored
Normal file
180
vendor/laravel/pail/src/ValueObjects/MessageLogged.php
vendored
Normal file
@@ -0,0 +1,180 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Pail\ValueObjects;
|
||||
|
||||
use Illuminate\Support\Carbon;
|
||||
use Stringable;
|
||||
|
||||
class MessageLogged implements Stringable
|
||||
{
|
||||
/**
|
||||
* Creates a new instance of the message logged.
|
||||
*
|
||||
* @param array{__pail: array{origin: array{trace: array<int, array{file: string, line: int}>|null, type: string, queue: string, job: string, command: string, method: string, path: string, auth_id: ?string, auth_email: ?string}}, exception: array{class: string, file: string}} $context
|
||||
*/
|
||||
protected function __construct(
|
||||
protected string $message,
|
||||
protected string $datetime,
|
||||
protected string $levelName,
|
||||
protected array $context,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance of the message logged from a json string.
|
||||
*/
|
||||
public static function fromJson(string $json): static
|
||||
{
|
||||
/** @var array{message: string, context: array{__pail: array{origin: array{trace: array<int, array{file: string, line: int}>|null, type: string, queue: string, job: string, command: string, method: string, path: string, auth_id: ?string, auth_email: ?string}}, exception: array{class: string, file: string}}, level_name: string, datetime: string} $array */
|
||||
$array = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
|
||||
|
||||
[
|
||||
'message' => $message,
|
||||
'datetime' => $datetime,
|
||||
'level_name' => $levelName,
|
||||
'context' => $context,
|
||||
] = $array;
|
||||
|
||||
return new static($message, $datetime, $levelName, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the log message's message.
|
||||
*/
|
||||
public function message(): string
|
||||
{
|
||||
return $this->message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the log message's date.
|
||||
*/
|
||||
public function date(): string
|
||||
{
|
||||
if ($_ENV['PAIL_TESTS'] ?? false) {
|
||||
return '2024-01-01 03:04:05';
|
||||
}
|
||||
|
||||
$time = Carbon::createFromFormat('Y-m-d\TH:i:s.uP', $this->datetime);
|
||||
|
||||
assert($time instanceof Carbon);
|
||||
|
||||
return $time->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the log message's time.
|
||||
*/
|
||||
public function time(): string
|
||||
{
|
||||
if ($_ENV['PAIL_TESTS'] ?? false) {
|
||||
return '03:04:05';
|
||||
}
|
||||
|
||||
$time = Carbon::createFromFormat('Y-m-d\TH:i:s.uP', $this->datetime);
|
||||
|
||||
assert($time instanceof Carbon);
|
||||
|
||||
return $time->format('H:i:s');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the log message's class.
|
||||
*/
|
||||
public function classOrType(): string
|
||||
{
|
||||
return $this->context['exception']['class'] ?? strtoupper($this->levelName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the log message's color.
|
||||
*/
|
||||
public function color(): string
|
||||
{
|
||||
return match ($this->levelName) {
|
||||
'DEBUG' => 'gray',
|
||||
'INFO' => 'blue',
|
||||
'NOTICE' => 'yellow',
|
||||
'WARNING' => 'yellow',
|
||||
'ERROR' => 'red',
|
||||
'CRITICAL' => 'red',
|
||||
'ALERT' => 'red',
|
||||
'EMERGENCY' => 'red',
|
||||
default => 'gray',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the log message's level.
|
||||
*/
|
||||
public function level(): string
|
||||
{
|
||||
return $this->levelName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the log message's file, if any.
|
||||
*/
|
||||
public function file(): ?string
|
||||
{
|
||||
return $this->context['exception']['file'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the log message's auth id.
|
||||
*/
|
||||
public function authId(): ?string
|
||||
{
|
||||
return $this->context['__pail']['origin']['auth_id'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the log message's origin.
|
||||
*/
|
||||
public function origin(): Origin\Console|Origin\Http|Origin\Queue
|
||||
{
|
||||
return match ($this->context['__pail']['origin']['type']) {
|
||||
'console' => Origin\Console::fromArray($this->context['__pail']['origin']),
|
||||
'queue' => Origin\Queue::fromArray($this->context['__pail']['origin']),
|
||||
default => Origin\Http::fromArray($this->context['__pail']['origin']),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the log message's trace, if any.
|
||||
*
|
||||
* @return array<int, array{file: string, line: int}>|null
|
||||
*/
|
||||
public function trace(): ?array
|
||||
{
|
||||
return $this->context['__pail']['origin']['trace'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the log message's context.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function context(): array
|
||||
{
|
||||
return collect($this->context)->except([
|
||||
'__pail',
|
||||
'exception',
|
||||
'userId',
|
||||
])->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return json_encode([
|
||||
'message' => $this->message,
|
||||
'datetime' => $this->datetime,
|
||||
'level_name' => $this->levelName,
|
||||
'context' => $this->context,
|
||||
], JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
|
||||
}
|
||||
}
|
||||
27
vendor/laravel/pail/src/ValueObjects/Origin/Console.php
vendored
Normal file
27
vendor/laravel/pail/src/ValueObjects/Origin/Console.php
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Pail\ValueObjects\Origin;
|
||||
|
||||
class Console
|
||||
{
|
||||
/**
|
||||
* Creates a new instance of the console origin.
|
||||
*/
|
||||
public function __construct(
|
||||
public ?string $command,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance of the console origin from the given json string.
|
||||
*
|
||||
* @param array{command?: string} $array
|
||||
*/
|
||||
public static function fromArray(array $array): static
|
||||
{
|
||||
$command = $array['command'] ?? null;
|
||||
|
||||
return new static($command);
|
||||
}
|
||||
}
|
||||
30
vendor/laravel/pail/src/ValueObjects/Origin/Http.php
vendored
Normal file
30
vendor/laravel/pail/src/ValueObjects/Origin/Http.php
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Pail\ValueObjects\Origin;
|
||||
|
||||
class Http
|
||||
{
|
||||
/**
|
||||
* Creates a new instance of the http origin.
|
||||
*/
|
||||
public function __construct(
|
||||
public string $method,
|
||||
public string $path,
|
||||
public ?string $authId,
|
||||
public ?string $authEmail,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance of the http origin from the given json string.
|
||||
*
|
||||
* @param array{method: string, path: string, auth_id: ?string, auth_email: ?string} $array
|
||||
*/
|
||||
public static function fromArray(array $array): static
|
||||
{
|
||||
['method' => $method, 'path' => $path, 'auth_id' => $authId, 'auth_email' => $authEmail] = $array;
|
||||
|
||||
return new static($method, $path, $authId, $authEmail);
|
||||
}
|
||||
}
|
||||
33
vendor/laravel/pail/src/ValueObjects/Origin/Queue.php
vendored
Normal file
33
vendor/laravel/pail/src/ValueObjects/Origin/Queue.php
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Pail\ValueObjects\Origin;
|
||||
|
||||
class Queue
|
||||
{
|
||||
/**
|
||||
* Creates a new instance of the console origin.
|
||||
*/
|
||||
public function __construct(
|
||||
public string $queue,
|
||||
public string $job,
|
||||
public ?string $command,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance of the queue origin from the given json string.
|
||||
*
|
||||
* @param array{queue: string, job: string, command: ?string} $array
|
||||
*/
|
||||
public static function fromArray(array $array): static
|
||||
{
|
||||
[
|
||||
'queue' => $queue,
|
||||
'job' => $job,
|
||||
'command' => $command,
|
||||
] = $array;
|
||||
|
||||
return new static($queue, $job, $command);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user