Skip to content

Commit 40834ec

Browse files
committed
Implement error logging system with database storage and clear command
1 parent ad9c8a3 commit 40834ec

File tree

17 files changed

+600
-283
lines changed

17 files changed

+600
-283
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
namespace App\Console\Commands;
4+
5+
use App\Models\ErrorLog;
6+
use Illuminate\Console\Command;
7+
8+
class ClearErrorLog extends Command
9+
{
10+
/**
11+
* The name and signature of the console command.
12+
*
13+
* @var string
14+
*/
15+
protected $signature = 'simpede:clear-error-log';
16+
17+
/**
18+
* The console command description.
19+
*
20+
* @var string
21+
*/
22+
protected $description = 'Clear the error log';
23+
24+
/**
25+
* Execute the console command.
26+
*/
27+
public function handle()
28+
{
29+
ErrorLog::where('resolved', true)->delete();
30+
$this->info('Error log cleared successfully.');
31+
}
32+
}

app/Helpers/Api.php

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,40 +2,11 @@
22

33
namespace App\Helpers;
44

5-
use GuzzleHttp\Client;
65
use Illuminate\Support\Facades\Storage;
76
use Symfony\Component\Process\Process;
87

98
class Api
109
{
11-
/**
12-
* Get unresolved issues from Sentry.
13-
*
14-
* @return array
15-
*/
16-
public static function getSentryUnresolvedIssues()
17-
{
18-
$organization = config('app.sentry_organization');
19-
$project = config('app.sentry_project');
20-
$token = config('app.sentry_token');
21-
22-
$client = new Client;
23-
try {
24-
$response = $client->request('GET', 'https://sentry.io/api/0/projects/'.$organization.'/'.$project.'/issues/', [
25-
'headers' => [
26-
'Authorization' => 'Bearer '.$token,
27-
],
28-
'query' => [
29-
'query' => 'is:unresolved',
30-
],
31-
]);
32-
33-
return json_decode($response->getBody()->getContents(), true);
34-
} catch (\Exception $e) {
35-
return [];
36-
}
37-
}
38-
3910
/**
4011
* Get outdated packages from Composer.
4112
*

app/Logging/DatabaseLogger.php

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php
2+
3+
namespace App\Logging;
4+
5+
use App\Models\ErrorLog;
6+
use Monolog\Handler\AbstractProcessingHandler;
7+
use Monolog\Logger;
8+
use Monolog\LogRecord;
9+
10+
class DatabaseLogger extends AbstractProcessingHandler
11+
{
12+
public function __construct()
13+
{
14+
$envLevel = config('logging.channels.database.level', env('LOG_LEVEL', 'warning'));
15+
parent::__construct(Logger::toMonologLevel($envLevel));
16+
}
17+
18+
protected function write(LogRecord $record): void
19+
{
20+
$message = $record->message;
21+
$level = $record->level->getName();
22+
$context = $record->context;
23+
24+
$file = null;
25+
$line = null;
26+
27+
if (isset($context['exception']) && $context['exception'] instanceof \Throwable) {
28+
$e = $context['exception'];
29+
$trace = $e->getTrace();
30+
31+
// cari yang pertama dari /app/
32+
$topAppTrace = collect($trace)->first(fn ($t) => isset($t['file']) && str_contains($t['file'], '/app/') && ! str_contains($t['file'], '/app/public/'));
33+
34+
if ($topAppTrace) {
35+
$file = $topAppTrace['file'];
36+
$line = $topAppTrace['line'];
37+
} else {
38+
// fallback ke file & line utama exception
39+
$file = $e->getFile();
40+
$line = $e->getLine();
41+
}
42+
43+
}
44+
45+
// Cek duplikat log
46+
$existing = ErrorLog::where('message', $message)
47+
->where('level', $level)
48+
->where('file', $file)
49+
->where('line', $line)
50+
->where('resolved', false)
51+
->latest('id')
52+
->first();
53+
54+
if ($existing) {
55+
$existing->increment('count');
56+
$existing->touch();
57+
} else {
58+
ErrorLog::create([
59+
'message' => $message,
60+
'context' => get_class($e),
61+
'level' => $level,
62+
'file' => $this->pathToClass($file),
63+
'line' => $line,
64+
'resolved' => false,
65+
'count' => 1,
66+
]);
67+
}
68+
}
69+
70+
private function pathToClass(string $path): ?string
71+
{
72+
$appPath = realpath(app_path());
73+
$path = realpath($path);
74+
75+
if (! str_starts_with($path, $appPath)) {
76+
return $path; // file bukan di dalam app/
77+
}
78+
79+
$relative = str_replace($appPath.DIRECTORY_SEPARATOR, '', $path);
80+
$withoutExt = preg_replace('/\.php$/', '', $relative);
81+
82+
return 'App\\'.str_replace(DIRECTORY_SEPARATOR, '\\', $withoutExt);
83+
}
84+
}

app/Models/ErrorLog.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
namespace App\Models;
4+
5+
use Illuminate\Database\Eloquent\Model;
6+
7+
class ErrorLog extends Model
8+
{
9+
protected $fillable = [
10+
'message', 'context', 'level', 'file', 'line', 'resolved', 'count',
11+
];
12+
}

app/Nova/Dashboards/SystemHealth.php

Lines changed: 0 additions & 47 deletions
This file was deleted.

app/Nova/ErrorLog.php

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
<?php
2+
3+
namespace App\Nova;
4+
5+
use App\Nova\Lenses\SystemReport;
6+
use Laravel\Nova\Fields\Badge;
7+
use Laravel\Nova\Fields\Boolean;
8+
use Laravel\Nova\Fields\Line;
9+
use Laravel\Nova\Fields\Stack;
10+
use Laravel\Nova\Fields\Text;
11+
use Laravel\Nova\Fields\Textarea;
12+
use Laravel\Nova\Http\Requests\NovaRequest;
13+
use Laravelwebdev\Numeric\Numeric;
14+
15+
class ErrorLog extends Resource
16+
{
17+
/**
18+
* The model the resource corresponds to.
19+
*
20+
* @var class-string<\App\Models\ErrorLog>
21+
*/
22+
public static $model = \App\Models\ErrorLog::class;
23+
24+
public static function label()
25+
{
26+
return 'Error Log';
27+
}
28+
29+
public static $displayInNavigation = false;
30+
31+
public static $globallySearchable = false;
32+
33+
/**
34+
* The single value that should be used to represent the resource when being displayed.
35+
*
36+
* @var string
37+
*/
38+
public static $title = 'level';
39+
40+
public function subtitle()
41+
{
42+
return $this->file;
43+
}
44+
45+
/**
46+
* The columns that should be searched.
47+
*
48+
* @var array
49+
*/
50+
public static $search = [
51+
'message', 'context', 'file',
52+
];
53+
54+
/**
55+
* Get the fields displayed by the resource.
56+
*
57+
* @return array
58+
*/
59+
public function fields(NovaRequest $request)
60+
{
61+
return [
62+
63+
Badge::make('Level')
64+
->map([
65+
'EMERGENCY' => 'danger',
66+
'ALERT' => 'danger',
67+
'CRITICAL' => 'danger',
68+
'ERROR' => 'danger',
69+
'WARNING' => 'warning',
70+
'NOTICE' => 'info',
71+
'INFO' => 'info',
72+
'DEBUG' => 'info',
73+
])
74+
->withIcons()
75+
->filterable(),
76+
Textarea::make('Context')->alwaysShow()->onlyOnDetail(),
77+
Text::make('File', function ($model) {
78+
return $model->file.' :'.$model->line;
79+
})->onlyOnDetail(),
80+
Stack::make('Details', [
81+
Line::make('File', function ($model) {
82+
return $model->file.' :'.$model->line;
83+
})->asBase(),
84+
Line::make('Message', function ($model) {
85+
return strlen($model->message) > 175
86+
? substr($model->message, 0, 175).'...'
87+
: $model->message;
88+
})->asSmall(),
89+
])->onlyOnIndex(),
90+
Textarea::make('Message')->alwaysShow()->onlyOnDetail(),
91+
Numeric::make('Count')->sortable(),
92+
Boolean::make('Resolved')->filterable(),
93+
94+
];
95+
}
96+
97+
/**
98+
* Get the cards available for the request.
99+
*
100+
* @return array
101+
*/
102+
public function cards(NovaRequest $request)
103+
{
104+
return [];
105+
}
106+
107+
/**
108+
* Get the filters available for the resource.
109+
*
110+
* @return array
111+
*/
112+
public function filters(NovaRequest $request)
113+
{
114+
return [];
115+
}
116+
117+
/**
118+
* Get the lenses available for the resource.
119+
*
120+
* @return array
121+
*/
122+
public function lenses(NovaRequest $request)
123+
{
124+
return [
125+
SystemReport::make(),
126+
];
127+
}
128+
129+
/**
130+
* Get the actions available for the resource.
131+
*
132+
* @return array
133+
*/
134+
public function actions(NovaRequest $request)
135+
{
136+
return [];
137+
}
138+
}

0 commit comments

Comments
 (0)