Skip to content

Commit

Permalink
Merge pull request #412 from facade/collect-job-info
Browse files Browse the repository at this point in the history
Add support for collecting job info
  • Loading branch information
rubenvanassche committed Aug 24, 2021
2 parents 4f50500 + 883b04e commit 58c6e9c
Show file tree
Hide file tree
Showing 5 changed files with 360 additions and 1 deletion.
26 changes: 25 additions & 1 deletion src/IgnitionServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@
use Facade\Ignition\Http\Controllers\StyleController;
use Facade\Ignition\Http\Middleware\IgnitionConfigValueEnabled;
use Facade\Ignition\Http\Middleware\IgnitionEnabled;
use Facade\Ignition\JobRecorder\JobRecorder;
use Facade\Ignition\Logger\FlareHandler;
use Facade\Ignition\LogRecorder\LogRecorder;
use Facade\Ignition\Middleware\AddDumps;
use Facade\Ignition\Middleware\AddEnvironmentInformation;
use Facade\Ignition\Middleware\AddGitInformation;
use Facade\Ignition\Middleware\AddJobInformation;
use Facade\Ignition\Middleware\AddLogs;
use Facade\Ignition\Middleware\AddQueries;
use Facade\Ignition\Middleware\AddSolutions;
Expand Down Expand Up @@ -87,6 +89,8 @@ public function boot()
if (isset($_SERVER['argv']) && ['artisan', 'tinker'] === $_SERVER['argv']) {
Api::sendReportsInBatches(false);
}

$this->app->make(JobRecorder::class)->register();
}

$this
Expand Down Expand Up @@ -125,7 +129,8 @@ public function register()
->registerExceptionRenderer()
->registerIgnitionConfig()
->registerFlare()
->registerDumpCollector();
->registerDumpCollector()
->registerJobRecorder();

if (config('flare.reporting.report_logs')) {
$this->registerLogRecorder();
Expand Down Expand Up @@ -334,6 +339,17 @@ protected function registerDumpCollector()
return $this;
}

protected function registerJobRecorder()
{
if (! $this->app->runningInConsole()) {
return $this;
}

$this->app->singleton(JobRecorder::class);

return $this;
}

protected function registerCommands()
{
$this->app->bind('command.flare:test', TestCommand::class);
Expand Down Expand Up @@ -384,6 +400,10 @@ protected function registerBuiltInMiddleware()

$middlewares[] = AddSolutions::class;

if ($this->app->runningInConsole()) {
$middlewares[] = AddJobInformation::class;
}

$middleware = collect($middlewares)
->map(function (string $middlewareClass) {
return $this->app->make($middlewareClass);
Expand Down Expand Up @@ -485,6 +505,10 @@ protected function resetFlare()
$this->app->make(QueryRecorder::class)->reset();
}

if ($this->app->runningInConsole()) {
$this->app->make(JobRecorder::class)->reset();
}

$this->app->make(DumpRecorder::class)->reset();
}

Expand Down
110 changes: 110 additions & 0 deletions src/JobRecorder/JobRecorder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<?php

namespace Facade\Ignition\JobRecorder;

use Exception;
use Illuminate\Contracts\Encryption\Encrypter;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Queue\Events\JobExceptionOccurred;
use Illuminate\Queue\Jobs\Job;
use Illuminate\Support\Str;
use ReflectionClass;
use ReflectionProperty;
use RuntimeException;

class JobRecorder
{
/** @var \Illuminate\Contracts\Foundation\Application */
protected $app;

/** @var \Illuminate\Queue\Jobs\Job|null */
protected $job = null;

public function __construct(Application $app)
{
$this->app = $app;
}

public function register(): self
{
$this->app['events']->listen(JobExceptionOccurred::class, [$this, 'record']);

return $this;
}

public function record(JobExceptionOccurred $event): void
{
$this->job = $event->job;
}

public function toArray(): array
{
if ($this->job === null) {
return [];
}

return array_filter([
'name' => $this->job->resolveName(),
'connection' => $this->job->getConnectionName(),
'queue' => $this->job->getQueue(),
'properties' => $this->getJobProperties(),
]);
}

public function getJob(): ?Job
{
return $this->job;
}

public function reset(): void
{
$this->job = null;
}

protected function getJobProperties(): array
{
$payload = $this->job->payload();

if (! array_key_exists('data', $payload)) {
return [];
}

try {
$job = $this->getCommand($payload['data']);
} catch (Exception $exception) {
return [];
}

$defaultProperties = [
'job',
'closure',
'connection',
'queue',
];

return collect((new ReflectionClass($job))->getProperties())
->reject(function (ReflectionProperty $property) use ($defaultProperties) {
return in_array($property->name, $defaultProperties);
})
->mapWithKeys(function (ReflectionProperty $property) use ($job) {
$property->setAccessible(true);

return [$property->name => $property->getValue($job)];
})
->toArray();
}

// Taken from Illuminate\Queue\CallQueuedHandler
protected function getCommand(array $data): object
{
if (Str::startsWith($data['command'], 'O:')) {
return unserialize($data['command']);
}

if ($this->app->bound(Encrypter::class)) {
return unserialize($this->app[Encrypter::class]->decrypt($data['command']));
}

throw new RuntimeException('Unable to extract job payload.');
}
}
26 changes: 26 additions & 0 deletions src/Middleware/AddJobInformation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace Facade\Ignition\Middleware;

use Facade\FlareClient\Report;
use Facade\Ignition\JobRecorder\JobRecorder;

class AddJobInformation
{
/** @var \Facade\Ignition\JobRecorder\JobRecorder */
protected $jobRecorder;

public function __construct(JobRecorder $jobRecorder)
{
$this->jobRecorder = $jobRecorder;
}

public function handle(Report $report, $next)
{
if ($this->jobRecorder->getJob()) {
$report->group('job', $this->jobRecorder->toArray());
}

return $next($report);
}
}
158 changes: 158 additions & 0 deletions tests/JobRecorder/JobRecorderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
<?php

namespace Facade\Ignition\Tests\JobRecorder;

use Exception;
use Facade\Ignition\JobRecorder\JobRecorder;
use Facade\Ignition\Tests\stubs\jobs\QueueableJob;
use Facade\Ignition\Tests\TestCase;
use Illuminate\Container\Container;
use Illuminate\Queue\Events\JobExceptionOccurred;
use Illuminate\Queue\Jobs\RedisJob;
use Illuminate\Queue\Queue;
use Illuminate\Queue\RedisQueue;

class JobRecorderTest extends TestCase
{
/** @test */
public function it_can_record_a_failed_job()
{
$recorder = (new JobRecorder($this->app));

$job = new QueueableJob([]);

$recorder->record($this->createEvent(
'redis',
'default',
$job
));

$recorded = $recorder->toArray();

$this->assertEquals('Facade\Ignition\Tests\stubs\jobs\QueueableJob', $recorded['name']);
$this->assertEquals('redis', $recorded['connection']);
$this->assertEquals('default', $recorded['queue']);
$this->assertNotEmpty($recorded['properties']);
$this->assertEquals([], $recorded['properties']['data']);
}

/** @test */
public function it_can_record_a_failed_job_with_data()
{
$recorder = (new JobRecorder($this->app));

$job = new QueueableJob([
'int' => 42,
'boolean' => true,
]);

$recorder->record($this->createEvent(
'redis',
'default',
$job
));

$recorded = $recorder->toArray();

$this->assertEquals('Facade\Ignition\Tests\stubs\jobs\QueueableJob', $recorded['name']);
$this->assertEquals('redis', $recorded['connection']);
$this->assertEquals('default', $recorded['queue']);
$this->assertNotEmpty($recorded['properties']);
$this->assertEquals([
'int' => 42,
'boolean' => true,
], $recorded['properties']['data']);
}

/** @test */
public function it_can_record_a_closure_job()
{
$recorder = (new JobRecorder($this->app));

$data = [
'int' => 42,
'boolean' => true,
];

$job = function () use ($data) {
};

$recorder->record($this->createEvent(
'redis',
'default',
$job
));

$recorded = $recorder->toArray();

$this->assertEquals('Closure (JobRecorderTest.php:77)', $recorded['name']);
$this->assertEquals('redis', $recorded['connection']);
$this->assertEquals('default', $recorded['queue']);
$this->assertNotEmpty($recorded['properties']);
}

/** @test */
public function it_can_handle_a_job_with_an_unserializeable_payload()
{
$recorder = (new JobRecorder($this->app));

$payload = json_encode([
'job' => 'Fake Job Name',
]);

$event = new JobExceptionOccurred(
'redis',
new RedisJob(
app(Container::class),
app(RedisQueue::class),
$payload,
$payload,
'redis',
'default'
),
new Exception()
);

$recorder->record($event);

$recorded = $recorder->toArray();

$this->assertEquals('Fake Job Name', $recorded['name']);
$this->assertEquals('redis', $recorded['connection']);
$this->assertEquals('default', $recorded['queue']);
}

/**
* @param string $connection
* @param \Illuminate\Contracts\Queue\ShouldQueue|\Closure $job
*
* @return \Illuminate\Queue\Events\JobExceptionOccurred
*/
private function createEvent(
string $connection,
string $queue,
$job
): JobExceptionOccurred {
$fakeQueue = new class extends Queue {
public function getPayload($job, $connection): string
{
return $this->createPayload($job, $connection);
}
};

$payload = $fakeQueue->getPayload($job, $connection);

return new JobExceptionOccurred(
$connection,
new RedisJob(
app(Container::class),
app(RedisQueue::class),
$payload,
$payload,
$connection,
$queue
),
new Exception()
);
}
}
Loading

0 comments on commit 58c6e9c

Please sign in to comment.