50 - Add ActionPerformed event and LogActionListener for centralized DB logging
This commit is contained in:
parent
0bb10729de
commit
bf96489362
10 changed files with 197 additions and 8 deletions
18
app/Events/ActionPerformed.php
Normal file
18
app/Events/ActionPerformed.php
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use App\Enums\LogLevelEnum;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
|
||||
class ActionPerformed
|
||||
{
|
||||
use Dispatchable;
|
||||
|
||||
public function __construct(
|
||||
public string $message,
|
||||
public LogLevelEnum $level = LogLevelEnum::INFO,
|
||||
/** @var array<string, mixed> */
|
||||
public array $context = [],
|
||||
) {}
|
||||
}
|
||||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Enums\LogLevelEnum;
|
||||
use App\Events\ActionPerformed;
|
||||
use App\Exceptions\PublishException;
|
||||
use App\Models\Article;
|
||||
use App\Models\ArticlePublication;
|
||||
|
|
@ -53,7 +55,7 @@ public function handle(ArticleFetcher $articleFetcher, ArticlePublishingService
|
|||
return;
|
||||
}
|
||||
|
||||
logger()->info('Publishing next article from scheduled job', [
|
||||
ActionPerformed::dispatch('Publishing next article from scheduled job', LogLevelEnum::INFO, [
|
||||
'article_id' => $article->id,
|
||||
'title' => $article->title,
|
||||
'url' => $article->url,
|
||||
|
|
@ -66,12 +68,12 @@ public function handle(ArticleFetcher $articleFetcher, ArticlePublishingService
|
|||
try {
|
||||
$publishingService->publishToRoutedChannels($article, $extractedData);
|
||||
|
||||
logger()->info('Successfully published article', [
|
||||
ActionPerformed::dispatch('Successfully published article', LogLevelEnum::INFO, [
|
||||
'article_id' => $article->id,
|
||||
'title' => $article->title,
|
||||
]);
|
||||
} catch (PublishException $e) {
|
||||
logger()->error('Failed to publish article', [
|
||||
ActionPerformed::dispatch('Failed to publish article', LogLevelEnum::ERROR, [
|
||||
'article_id' => $article->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
|
|
|||
21
app/Listeners/LogActionListener.php
Normal file
21
app/Listeners/LogActionListener.php
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace App\Listeners;
|
||||
|
||||
use App\Events\ActionPerformed;
|
||||
use App\Services\Log\LogSaver;
|
||||
use Exception;
|
||||
|
||||
class LogActionListener
|
||||
{
|
||||
public function __construct(private LogSaver $logSaver) {}
|
||||
|
||||
public function handle(ActionPerformed $event): void
|
||||
{
|
||||
try {
|
||||
$this->logSaver->log($event->level, $event->message, context: $event->context);
|
||||
} catch (Exception $e) {
|
||||
error_log('Failed to log action to database: '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
namespace App\Listeners;
|
||||
|
||||
use App\Enums\LogLevelEnum;
|
||||
use App\Events\ActionPerformed;
|
||||
use App\Events\ArticleApproved;
|
||||
use App\Services\Article\ArticleFetcher;
|
||||
use App\Services\Publishing\ArticlePublishingService;
|
||||
|
|
@ -40,14 +42,14 @@ public function handle(ArticleApproved $event): void
|
|||
if ($publications->isNotEmpty()) {
|
||||
$article->update(['publish_status' => 'published']);
|
||||
|
||||
logger()->info('Published approved article', [
|
||||
ActionPerformed::dispatch('Published approved article', LogLevelEnum::INFO, [
|
||||
'article_id' => $article->id,
|
||||
'title' => $article->title,
|
||||
]);
|
||||
} else {
|
||||
$article->update(['publish_status' => 'error']);
|
||||
|
||||
logger()->warning('No publications created for approved article', [
|
||||
ActionPerformed::dispatch('No publications created for approved article', LogLevelEnum::WARNING, [
|
||||
'article_id' => $article->id,
|
||||
'title' => $article->title,
|
||||
]);
|
||||
|
|
@ -55,7 +57,7 @@ public function handle(ArticleApproved $event): void
|
|||
} catch (Exception $e) {
|
||||
$article->update(['publish_status' => 'error']);
|
||||
|
||||
logger()->error('Failed to publish approved article', [
|
||||
ActionPerformed::dispatch('Failed to publish approved article', LogLevelEnum::ERROR, [
|
||||
'article_id' => $article->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
namespace App\Listeners;
|
||||
|
||||
use App\Enums\LogLevelEnum;
|
||||
use App\Events\ActionPerformed;
|
||||
use App\Events\NewArticleFetched;
|
||||
use App\Models\Setting;
|
||||
use App\Services\Article\ValidationService;
|
||||
|
|
@ -37,7 +39,7 @@ public function handle(NewArticleFetched $event): void
|
|||
try {
|
||||
$article = $this->validationService->validate($article);
|
||||
} catch (Exception $e) {
|
||||
logger()->error('Article validation failed', [
|
||||
ActionPerformed::dispatch('Article validation failed', LogLevelEnum::ERROR, [
|
||||
'article_id' => $article->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@
|
|||
namespace App\Providers;
|
||||
|
||||
use App\Enums\LogLevelEnum;
|
||||
use App\Events\ActionPerformed;
|
||||
use App\Events\ExceptionOccurred;
|
||||
use App\Listeners\LogActionListener;
|
||||
use App\Listeners\LogExceptionToDatabase;
|
||||
use Error;
|
||||
use Illuminate\Contracts\Debug\ExceptionHandler;
|
||||
|
|
@ -18,6 +20,11 @@ public function register(): void {}
|
|||
|
||||
public function boot(): void
|
||||
{
|
||||
Event::listen(
|
||||
ActionPerformed::class,
|
||||
LogActionListener::class,
|
||||
);
|
||||
|
||||
Event::listen(
|
||||
ExceptionOccurred::class,
|
||||
LogExceptionToDatabase::class,
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ public function debug(string $message, ?PlatformChannel $channel = null, array $
|
|||
/**
|
||||
* @param array<string, mixed> $context
|
||||
*/
|
||||
private function log(LogLevelEnum $level, string $message, ?PlatformChannel $channel = null, array $context = []): void
|
||||
public function log(LogLevelEnum $level, string $message, ?PlatformChannel $channel = null, array $context = []): void
|
||||
{
|
||||
$logContext = $context;
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Events\ActionPerformed;
|
||||
use App\Events\ArticleApproved;
|
||||
// use App\Events\ArticleReadyToPublish; // Class no longer exists
|
||||
use App\Events\ExceptionLogged;
|
||||
|
|
@ -268,6 +269,9 @@ public function test_log_exception_to_database_listener_creates_log(): void
|
|||
public function test_event_listener_registration_works(): void
|
||||
{
|
||||
// Test that events are properly bound to listeners
|
||||
$listeners = Event::getListeners(ActionPerformed::class);
|
||||
$this->assertNotEmpty($listeners);
|
||||
|
||||
$listeners = Event::getListeners(NewArticleFetched::class);
|
||||
$this->assertNotEmpty($listeners);
|
||||
|
||||
|
|
|
|||
48
tests/Unit/Events/ActionPerformedTest.php
Normal file
48
tests/Unit/Events/ActionPerformedTest.php
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Unit\Events;
|
||||
|
||||
use App\Enums\LogLevelEnum;
|
||||
use App\Events\ActionPerformed;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ActionPerformedTest extends TestCase
|
||||
{
|
||||
public function test_event_can_be_constructed_with_defaults(): void
|
||||
{
|
||||
$event = new ActionPerformed('Test message');
|
||||
|
||||
$this->assertEquals('Test message', $event->message);
|
||||
$this->assertEquals(LogLevelEnum::INFO, $event->level);
|
||||
$this->assertEquals([], $event->context);
|
||||
}
|
||||
|
||||
public function test_event_can_be_constructed_with_custom_level_and_context(): void
|
||||
{
|
||||
$context = ['article_id' => 1, 'error' => 'Something failed'];
|
||||
|
||||
$event = new ActionPerformed(
|
||||
'Article validation failed',
|
||||
LogLevelEnum::ERROR,
|
||||
$context,
|
||||
);
|
||||
|
||||
$this->assertEquals('Article validation failed', $event->message);
|
||||
$this->assertEquals(LogLevelEnum::ERROR, $event->level);
|
||||
$this->assertEquals($context, $event->context);
|
||||
}
|
||||
|
||||
public function test_event_uses_dispatchable_trait(): void
|
||||
{
|
||||
$this->assertContains(Dispatchable::class, class_uses(ActionPerformed::class));
|
||||
}
|
||||
|
||||
public function test_event_does_not_use_serializes_models_trait(): void
|
||||
{
|
||||
$this->assertNotContains(
|
||||
\Illuminate\Queue\SerializesModels::class,
|
||||
class_uses(ActionPerformed::class),
|
||||
);
|
||||
}
|
||||
}
|
||||
85
tests/Unit/Listeners/LogActionListenerTest.php
Normal file
85
tests/Unit/Listeners/LogActionListenerTest.php
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Unit\Listeners;
|
||||
|
||||
use App\Enums\LogLevelEnum;
|
||||
use App\Events\ActionPerformed;
|
||||
use App\Listeners\LogActionListener;
|
||||
use App\Models\Log;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class LogActionListenerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_listener_creates_log_entry_with_correct_data(): void
|
||||
{
|
||||
$event = new ActionPerformed(
|
||||
'Article published successfully',
|
||||
LogLevelEnum::INFO,
|
||||
['article_id' => 42],
|
||||
);
|
||||
|
||||
$listener = app(LogActionListener::class);
|
||||
$listener->handle($event);
|
||||
|
||||
$this->assertDatabaseHas('logs', [
|
||||
'level' => 'info',
|
||||
'message' => 'Article published successfully',
|
||||
]);
|
||||
|
||||
$log = Log::where('message', 'Article published successfully')->first();
|
||||
$this->assertEquals(['article_id' => 42], $log->context);
|
||||
}
|
||||
|
||||
public function test_listener_creates_log_with_warning_level(): void
|
||||
{
|
||||
$event = new ActionPerformed(
|
||||
'No publications created',
|
||||
LogLevelEnum::WARNING,
|
||||
['article_id' => 7],
|
||||
);
|
||||
|
||||
$listener = app(LogActionListener::class);
|
||||
$listener->handle($event);
|
||||
|
||||
$this->assertDatabaseHas('logs', [
|
||||
'level' => 'warning',
|
||||
'message' => 'No publications created',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_listener_creates_log_with_error_level(): void
|
||||
{
|
||||
$event = new ActionPerformed(
|
||||
'Publishing failed',
|
||||
LogLevelEnum::ERROR,
|
||||
['error' => 'Connection timeout'],
|
||||
);
|
||||
|
||||
$listener = app(LogActionListener::class);
|
||||
$listener->handle($event);
|
||||
|
||||
$this->assertDatabaseHas('logs', [
|
||||
'level' => 'error',
|
||||
'message' => 'Publishing failed',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_listener_creates_log_with_empty_context(): void
|
||||
{
|
||||
$event = new ActionPerformed('Simple action');
|
||||
|
||||
$listener = app(LogActionListener::class);
|
||||
$listener->handle($event);
|
||||
|
||||
$this->assertDatabaseHas('logs', [
|
||||
'level' => 'info',
|
||||
'message' => 'Simple action',
|
||||
]);
|
||||
|
||||
$log = Log::where('message', 'Simple action')->first();
|
||||
$this->assertEquals([], $log->context);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue