Add database logging

This commit is contained in:
myrmidex 2025-06-29 18:33:18 +02:00
parent 4453c882e6
commit 6e1affeda3
18 changed files with 290 additions and 24 deletions

View file

@ -0,0 +1,17 @@
<?php
namespace App\Events;
use App\Models\Log;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class ExceptionLogged
{
use Dispatchable, SerializesModels;
public function __construct(
public Log $log
) {
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace App\Events;
use App\LogLevelEnum;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Throwable;
class ExceptionOccurred
{
use Dispatchable, SerializesModels;
public function __construct(
public Throwable $exception,
public LogLevelEnum $level,
public string $message,
public array $context = []
) {
}
}

View file

@ -12,6 +12,6 @@ public function __invoke(Request $request): View
{ {
$articles = Article::all()->sortByDesc('created_at'); $articles = Article::all()->sortByDesc('created_at');
return view('articles.index', compact('articles')); return view('pages.articles.index', compact('articles'));
} }
} }

View file

@ -0,0 +1,17 @@
<?php
namespace App\Http\Controllers;
use App\Models\Log;
use Illuminate\Contracts\View\View;
use Illuminate\Http\Request;
class LogsController extends Controller
{
public function __invoke(Request $request): View
{
$logs = Log::all()->sortByDesc('created_at');
return view('pages.logs.index', compact('logs'));
}
}

View file

@ -0,0 +1,31 @@
<?php
namespace App\Listeners;
use App\Events\ExceptionLogged;
use App\Events\ExceptionOccurred;
use App\Models\Log;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class LogExceptionToDatabase implements ShouldQueue
{
use InteractsWithQueue;
public function handle(ExceptionOccurred $event): void
{
$log = Log::create([
'level' => $event->level,
'message' => $event->message,
'context' => [
'exception_class' => get_class($event->exception),
'file' => $event->exception->getFile(),
'line' => $event->exception->getLine(),
'trace' => $event->exception->getTraceAsString(),
...$event->context
]
]);
ExceptionLogged::dispatch($log);
}
}

23
app/LogLevelEnum.php Normal file
View file

@ -0,0 +1,23 @@
<?php
namespace App;
enum LogLevelEnum: string
{
case DEBUG = 'debug';
case INFO = 'info';
case WARNING = 'warning';
case ERROR = 'error';
case CRITICAL = 'critical';
public static function toArray(): array
{
return [
self::DEBUG->value,
self::INFO->value,
self::WARNING->value,
self::ERROR->value,
self::CRITICAL->value,
];
}
}

24
app/Models/Log.php Normal file
View file

@ -0,0 +1,24 @@
<?php
namespace App\Models;
use App\LogLevelEnum;
use Illuminate\Database\Eloquent\Model;
class Log extends Model
{
protected $table = 'logs';
protected $fillable = [
'level',
'message',
'context',
];
protected $casts = [
'level' => LogLevelEnum::class,
'context' => 'array',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
}

View file

@ -4,10 +4,15 @@
use App\Events\ArticleFetched; use App\Events\ArticleFetched;
use App\Events\ArticleReadyToPublish; use App\Events\ArticleReadyToPublish;
use App\Events\ExceptionOccurred;
use App\Listeners\CheckArticleKeywords; use App\Listeners\CheckArticleKeywords;
use App\Listeners\LogExceptionToDatabase;
use App\Listeners\PublishArticle; use App\Listeners\PublishArticle;
use App\LogLevelEnum;
use Illuminate\Contracts\Debug\ExceptionHandler;
use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Event;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use Throwable;
class AppServiceProvider extends ServiceProvider class AppServiceProvider extends ServiceProvider
{ {
@ -22,10 +27,32 @@ public function boot(): void
ArticleFetched::class, ArticleFetched::class,
CheckArticleKeywords::class, CheckArticleKeywords::class,
); );
Event::listen( Event::listen(
ArticleReadyToPublish::class, ArticleReadyToPublish::class,
PublishArticle::class, PublishArticle::class,
); );
Event::listen(
ExceptionOccurred::class,
LogExceptionToDatabase::class,
);
app()->make(ExceptionHandler::class)
->reportable(function (Throwable $e) {
$level = $this->mapExceptionToLogLevel($e);
ExceptionOccurred::dispatch($e, $level, $e->getMessage(), []);
});
}
private function mapExceptionToLogLevel(Throwable $exception): LogLevelEnum
{
return match (true) {
$exception instanceof \Error => LogLevelEnum::CRITICAL,
$exception instanceof \RuntimeException => LogLevelEnum::ERROR,
$exception instanceof \InvalidArgumentException => LogLevelEnum::WARNING,
default => LogLevelEnum::ERROR,
};
} }
} }

View file

@ -37,6 +37,10 @@ private static function fetchArticles(): Collection
return collect($responses) return collect($responses)
->map(function ($response, $index) use ($urls) { ->map(function ($response, $index) use ($urls) {
if (!isset($urls[$index])) {
return null;
}
$url = $urls[$index]; $url = $urls[$index];
try { try {
@ -51,7 +55,7 @@ private static function fetchArticles(): Collection
}) })
->filter(fn($article) => !empty($article)); ->filter(fn($article) => !empty($article));
} catch (Exception $e) { } catch (Exception $e) {
logger('article_fetcher')->error("Failed to fetch VRT homepage", ['error' => $e->getMessage()]); logger()->error("Failed to fetch VRT homepage", ['error' => $e->getMessage()]);
return new Collection([]); return new Collection([]);
} }
} }

View file

@ -0,0 +1,11 @@
<?php
namespace App\Services\Log;
class LogSaver
{
public function log()
{
}
}

View file

@ -23,5 +23,19 @@
]); ]);
}) })
->withExceptions(function (Exceptions $exceptions) { ->withExceptions(function (Exceptions $exceptions) {
// $exceptions->reportable(function (Throwable $e) {
$level = match (true) {
$e instanceof Error => App\LogLevelEnum::CRITICAL,
$e instanceof RuntimeException => App\LogLevelEnum::ERROR,
$e instanceof InvalidArgumentException => App\LogLevelEnum::WARNING,
default => App\LogLevelEnum::ERROR,
};
App\Events\ExceptionOccurred::dispatch(
$e,
$level,
$e->getMessage(),
[]
);
});
})->create(); })->create();

View file

@ -0,0 +1,25 @@
<?php
use App\LogLevelEnum;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('logs', function (Blueprint $table) {
$table->id();
$table->enum('level', LogLevelEnum::toArray());
$table->string('message');
$table->json('context')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('logs');
}
};

View file

@ -1,20 +0,0 @@
<h1>Articles</h1>
<table>
<thead>
<tr>
<th>ID</th>
<th>URL</th>
<th>Created At</th>
</tr>
</thead>
<tbody>
@foreach($articles as $article)
<tr>
<td>{{ $article->id }}</td>
<td>{{ $article->url }}</td>
<td>{{ $article->created_at->format('Y-m-d H:i') }}</td>
</tr>
@endforeach
</tbody>
</table>

View file

@ -0,0 +1,10 @@
<html>
<head>
<title>Lemmy Poster</title>
</head>
<body>
@include('partials.navbar')
@yield('content')
</body>
</html>

View file

@ -0,0 +1,24 @@
@extends('layouts.app')
@section('content')
<h1>Articles</h1>
<table>
<thead>
<tr>
<th>ID</th>
<th>URL</th>
<th>Created At</th>
</tr>
</thead>
<tbody>
@foreach($articles as $article)
<tr>
<td>{{ $article->id }}</td>
<td>{{ $article->url }}</td>
<td>{{ $article->created_at->format('Y-m-d H:i') }}</td>
</tr>
@endforeach
</tbody>
</table>
@endsection

View file

@ -0,0 +1,26 @@
@extends('layouts.app')
@section('content')
<h1>Logs</h1>
<table>
<thead>
<tr>
<th>ID</th>
<th>Level</th>
<th>Message</th>
<th>Created At</th>
</tr>
</thead>
<tbody>
@foreach($logs as $log)
<tr>
<td>{{ $log->id }}</td>
<td>{{ ucfirst($log->level->value) }}</td>
<td>{{ $log->message }}</td>
<td>{{ $log->created_at->format('Y-m-d H:i') }}</td>
</tr>
@endforeach
</tbody>
</table>
@endsection

View file

@ -0,0 +1,10 @@
<nav>
<ul>
<li>
<a href="/articles">Articles</a>
</li>
<li>
<a href="/logs">Logs</a>
</li>
</ul>
</nav>

View file

@ -1,6 +1,7 @@
<?php <?php
use App\Http\Controllers\ArticlesController; use App\Http\Controllers\ArticlesController;
use App\Http\Controllers\LogsController;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Inertia\Inertia; use Inertia\Inertia;
@ -18,3 +19,4 @@
require __DIR__.'/auth.php'; require __DIR__.'/auth.php';
Route::get('/articles', ArticlesController::class)->name('articles'); Route::get('/articles', ArticlesController::class)->name('articles');
Route::get('/logs', LogsController::class)->name('logs');