From 05f2d92ba5ae15c0a8166ee2def8e76170634ec6 Mon Sep 17 00:00:00 2001 From: Joshua Dias Date: Thu, 5 Jun 2025 14:30:44 -0400 Subject: [PATCH 1/4] New log-viewer:summary command that summarizes laravel logs and LogSummary class to display them --- src/Console/Commands/LogSummaryCommand.php | 109 +++++++++++++++++++++ src/Logs/LogSummary.php | 89 +++++++++++++++++ 2 files changed, 198 insertions(+) create mode 100644 src/Console/Commands/LogSummaryCommand.php create mode 100644 src/Logs/LogSummary.php diff --git a/src/Console/Commands/LogSummaryCommand.php b/src/Console/Commands/LogSummaryCommand.php new file mode 100644 index 00000000..3e9f18b9 --- /dev/null +++ b/src/Console/Commands/LogSummaryCommand.php @@ -0,0 +1,109 @@ +comment('Deleted error log summary file'); + $this->comment('Generating error log summary file'); + static::generateSummaryFile(); + $this->comment('Done'); + } + + protected static function deleteSummaryFile(): void + { + Storage::disk('logs') + ->delete('log-summary.log'); + } + + protected static function generateSummaryFile(): void + { + $summary = []; + + /** @var LogFileCollection $files */ + $files = LogViewer::getFiles(); + + foreach ($files as $file) { + if ($file->name === 'log-summary.log') { + continue; + } + + $paginator = $file + ->logs() + ->paginate(1000); + + foreach ($paginator->items() as $log) { + $level = strtoupper($log->level); + $message = $log->getOriginalText(); + $context = $log->context; + $env = $log->extra['environment']; + $ts = $log->datetime->format('Y-m-d H:i:s'); + + if (! isset($summary[$message])) { + $summary[$message] = [ + 'first' => $ts, + 'last' => $ts, + 'count' => 1, + 'level' => $level, + 'context' => $context, + 'env' => $env, + ]; + } else { + $summary[$message]['count']++; + $summary[$message]['last'] = max( + $ts, + $summary[$message]['last'] + ); + $summary[$message]['first'] = min( + $ts, + $summary[$message]['first'] + ); + } + } + } + + $lines = []; + foreach ($summary as $message => $data) { + $lines[] = trim(sprintf( + '[%s] - [%s] %s.%s: %d | %s %s', + $data['first'], + $data['last'], + $data['env'], + $data['level'], + $data['count'], + $message, + count($data['context']) > 0 ? Str::replace('\\n', "\n", '\n' . json_encode($data['context'], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)) : '' + )); + + } + + Storage::disk('logs') + ->put('log-summary.log', implode("\n", $lines)); + } +} diff --git a/src/Logs/LogSummary.php b/src/Logs/LogSummary.php new file mode 100644 index 00000000..e7fef7a0 --- /dev/null +++ b/src/Logs/LogSummary.php @@ -0,0 +1,89 @@ +[^\]]+)\]\s*-\s*\[(?P[^\]]+)\]\s+(?P\S+)\.(?P\S+):\s+(?P\d+)\s*\|\s*(?.*)/x'; + + public static array $columns = [ + ['label' => 'Severity', 'data_path' => 'level'], + ['label' => 'First', 'data_path' => 'extra.first_datetime'], + ['label' => 'Last', 'data_path' => 'datetime'], + ['label' => 'Env', 'data_path' => 'extra.environment'], + ['label' => 'Count', 'data_path' => 'extra.count'], + ['label' => 'Message', 'data_path' => 'message'], + ]; + + public static string $regexFirstDatetimeKey = 'first_datetime'; + public static string $regexCountKey = 'count'; + public static string $regexEnvironmentKey = 'environment'; + + public function fillMatches(array $matches = []): void + { + parent::fillMatches($matches); + + $this->message = $matches[static::$regexMessageKey] ?? ''; + $this->extra = [ + 'environment' => $matches[static::$regexEnvironmentKey] ?? '', + 'count' => $matches[static::$regexCountKey] ?? 0, + 'first_datetime' => Carbon::parse($matches[static::$regexFirstDatetimeKey] ?? '') + ->setTimezone(LogViewer::timezone()) + ->format("Y\u{2011}m\u{2011}d\u{00A0}H:i:s"), + ]; + + + $raw = $this->text; + + if (preg_match('/\{".*}/s', $raw, $m)) { + $jsonString = $m[0]; + $pos = Str::position($raw, '{"'); + if ($pos !== false) { + $cleanText = substr($raw, 0, $pos); + $this->text = $cleanText; + } + + $escaped = $this->sanitizeJson($jsonString); + + $contextArray = json_decode($escaped, true); + + if (json_last_error() !== JSON_ERROR_NONE) { + $contextArray = []; + } + } else { + $contextArray = []; + } + + $this->context = $contextArray; + } + + public function getOriginalText(): ?string + { + if (! $this->text) { + return null; + } + + $pos = Str::position($this->text, '| '); + if ($pos === false) { + return $this->text; + } + + return Str::substr($this->text, $pos + 2); + } + + protected function sanitizeJson(string $json): string + { + return Str::replace( + ["\n", "\t"], + ['\\n', '\\t'], + $json + ); + } +} From 52bc7fa5702ae3a7825e7ddc684bade7f4f935c8 Mon Sep 17 00:00:00 2001 From: Joshua Dias Date: Thu, 5 Jun 2025 14:57:42 -0400 Subject: [PATCH 2/4] composer format --- src/Console/Commands/LogSummaryCommand.php | 8 ++++---- src/LogViewerServiceProvider.php | 2 ++ src/Logs/LogSummary.php | 7 +------ 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/Console/Commands/LogSummaryCommand.php b/src/Console/Commands/LogSummaryCommand.php index 3e9f18b9..1729766d 100644 --- a/src/Console/Commands/LogSummaryCommand.php +++ b/src/Console/Commands/LogSummaryCommand.php @@ -1,6 +1,6 @@ $ts, - 'last' => $ts, + 'last' => $ts, 'count' => 1, 'level' => $level, 'context' => $context, @@ -98,7 +98,7 @@ protected static function generateSummaryFile(): void $data['level'], $data['count'], $message, - count($data['context']) > 0 ? Str::replace('\\n', "\n", '\n' . json_encode($data['context'], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)) : '' + count($data['context']) > 0 ? Str::replace('\\n', "\n", '\n'.json_encode($data['context'], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)) : '' )); } diff --git a/src/LogViewerServiceProvider.php b/src/LogViewerServiceProvider.php index 8e69eb82..9a75898e 100644 --- a/src/LogViewerServiceProvider.php +++ b/src/LogViewerServiceProvider.php @@ -14,6 +14,7 @@ use Illuminate\Support\Str; use Laravel\Octane\Events\RequestTerminated; use Opcodes\LogViewer\Console\Commands\GenerateDummyLogsCommand; +use Opcodes\LogViewer\Console\Commands\LogSummaryCommand; use Opcodes\LogViewer\Console\Commands\PublishCommand; use Opcodes\LogViewer\Events\LogFileDeleted; use Opcodes\LogViewer\Facades\LogViewer; @@ -60,6 +61,7 @@ public function boot() $this->commands([ PublishCommand::class, GenerateDummyLogsCommand::class, + LogSummaryCommand::class, ]); } diff --git a/src/Logs/LogSummary.php b/src/Logs/LogSummary.php index e7fef7a0..ddd1556d 100644 --- a/src/Logs/LogSummary.php +++ b/src/Logs/LogSummary.php @@ -1,18 +1,15 @@ [^\]]+)\]\s*-\s*\[(?P[^\]]+)\]\s+(?P\S+)\.(?P\S+):\s+(?P\d+)\s*\|\s*(?.*)/x'; - public static array $columns = [ ['label' => 'Severity', 'data_path' => 'level'], ['label' => 'First', 'data_path' => 'extra.first_datetime'], @@ -21,7 +18,6 @@ class LogSummary extends Log ['label' => 'Count', 'data_path' => 'extra.count'], ['label' => 'Message', 'data_path' => 'message'], ]; - public static string $regexFirstDatetimeKey = 'first_datetime'; public static string $regexCountKey = 'count'; public static string $regexEnvironmentKey = 'environment'; @@ -39,7 +35,6 @@ public function fillMatches(array $matches = []): void ->format("Y\u{2011}m\u{2011}d\u{00A0}H:i:s"), ]; - $raw = $this->text; if (preg_match('/\{".*}/s', $raw, $m)) { From 59347aa437b7c870e1f612d105c9d5c0fcfea38a Mon Sep 17 00:00:00 2001 From: Joshua Dias Date: Fri, 13 Jun 2025 16:28:10 -0400 Subject: [PATCH 3/4] Fixes duplicates bug, parsing, and summarizes latest entries in laravel.log --- ...mmaryCommand.php => SummaryLogCommand.php} | 66 +++++++-------- src/LogViewerServiceProvider.php | 4 +- src/Logs/LogSummary.php | 84 ------------------- src/Logs/SummaryLog.php | 55 ++++++++++++ 4 files changed, 89 insertions(+), 120 deletions(-) rename src/Console/Commands/{LogSummaryCommand.php => SummaryLogCommand.php} (52%) delete mode 100644 src/Logs/LogSummary.php create mode 100644 src/Logs/SummaryLog.php diff --git a/src/Console/Commands/LogSummaryCommand.php b/src/Console/Commands/SummaryLogCommand.php similarity index 52% rename from src/Console/Commands/LogSummaryCommand.php rename to src/Console/Commands/SummaryLogCommand.php index 1729766d..8ca68fea 100644 --- a/src/Console/Commands/LogSummaryCommand.php +++ b/src/Console/Commands/SummaryLogCommand.php @@ -1,6 +1,6 @@ option('logs'); + static::deleteSummaryFile(); $this->comment('Deleted error log summary file'); $this->comment('Generating error log summary file'); - static::generateSummaryFile(); + static::generateSummaryFile($number); $this->comment('Done'); } protected static function deleteSummaryFile(): void { Storage::disk('logs') - ->delete('log-summary.log'); + ->delete('summary.log'); } - protected static function generateSummaryFile(): void + protected static function generateSummaryFile(int $number): void { $summary = []; @@ -50,60 +53,55 @@ protected static function generateSummaryFile(): void $files = LogViewer::getFiles(); foreach ($files as $file) { - if ($file->name === 'log-summary.log') { + if (!Str::endsWith($file->name, 'laravel.log')) { continue; } - $paginator = $file - ->logs() - ->paginate(1000); + $logs = collect($file->logs()->get()) + ->reverse() + ->take($number); - foreach ($paginator->items() as $log) { - $level = strtoupper($log->level); - $message = $log->getOriginalText(); + foreach ($logs as $log) { + $level = $log->level; + $message = $log->message; $context = $log->context; $env = $log->extra['environment']; $ts = $log->datetime->format('Y-m-d H:i:s'); - if (! isset($summary[$message])) { + if (! isset($summary[trim($message)])) { $summary[$message] = [ 'first' => $ts, - 'last' => $ts, + 'last' => $ts, 'count' => 1, 'level' => $level, - 'context' => $context, 'env' => $env, + 'message' => $message, + 'context' => $context, ]; } else { - $summary[$message]['count']++; - $summary[$message]['last'] = max( + $summary[trim($message)]['count']++; + $summary[trim($message)]['last'] = max( $ts, - $summary[$message]['last'] + $summary[trim($message)]['last'] ); - $summary[$message]['first'] = min( + $summary[trim($message)]['first'] = min( $ts, - $summary[$message]['first'] + $summary[trim($message)]['first'] ); } } } - $lines = []; - foreach ($summary as $message => $data) { - $lines[] = trim(sprintf( - '[%s] - [%s] %s.%s: %d | %s %s', - $data['first'], - $data['last'], - $data['env'], - $data['level'], - $data['count'], - $message, - count($data['context']) > 0 ? Str::replace('\\n', "\n", '\n'.json_encode($data['context'], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)) : '' - )); + $lines = array_map(function(array $data) { + return json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + }, array_values($summary)); + $uniqueLines = []; + foreach ($lines as $json) { + $uniqueLines[$json] = true; } Storage::disk('logs') - ->put('log-summary.log', implode("\n", $lines)); + ->put('summary.log', implode("\n", array_reverse(array_keys($uniqueLines)))); } } diff --git a/src/LogViewerServiceProvider.php b/src/LogViewerServiceProvider.php index 9a75898e..e75077e6 100644 --- a/src/LogViewerServiceProvider.php +++ b/src/LogViewerServiceProvider.php @@ -14,7 +14,7 @@ use Illuminate\Support\Str; use Laravel\Octane\Events\RequestTerminated; use Opcodes\LogViewer\Console\Commands\GenerateDummyLogsCommand; -use Opcodes\LogViewer\Console\Commands\LogSummaryCommand; +use Opcodes\LogViewer\Console\Commands\SummaryLogCommand; use Opcodes\LogViewer\Console\Commands\PublishCommand; use Opcodes\LogViewer\Events\LogFileDeleted; use Opcodes\LogViewer\Facades\LogViewer; @@ -61,7 +61,7 @@ public function boot() $this->commands([ PublishCommand::class, GenerateDummyLogsCommand::class, - LogSummaryCommand::class, + SummaryLogCommand::class, ]); } diff --git a/src/Logs/LogSummary.php b/src/Logs/LogSummary.php deleted file mode 100644 index ddd1556d..00000000 --- a/src/Logs/LogSummary.php +++ /dev/null @@ -1,84 +0,0 @@ -[^\]]+)\]\s*-\s*\[(?P[^\]]+)\]\s+(?P\S+)\.(?P\S+):\s+(?P\d+)\s*\|\s*(?.*)/x'; - public static array $columns = [ - ['label' => 'Severity', 'data_path' => 'level'], - ['label' => 'First', 'data_path' => 'extra.first_datetime'], - ['label' => 'Last', 'data_path' => 'datetime'], - ['label' => 'Env', 'data_path' => 'extra.environment'], - ['label' => 'Count', 'data_path' => 'extra.count'], - ['label' => 'Message', 'data_path' => 'message'], - ]; - public static string $regexFirstDatetimeKey = 'first_datetime'; - public static string $regexCountKey = 'count'; - public static string $regexEnvironmentKey = 'environment'; - - public function fillMatches(array $matches = []): void - { - parent::fillMatches($matches); - - $this->message = $matches[static::$regexMessageKey] ?? ''; - $this->extra = [ - 'environment' => $matches[static::$regexEnvironmentKey] ?? '', - 'count' => $matches[static::$regexCountKey] ?? 0, - 'first_datetime' => Carbon::parse($matches[static::$regexFirstDatetimeKey] ?? '') - ->setTimezone(LogViewer::timezone()) - ->format("Y\u{2011}m\u{2011}d\u{00A0}H:i:s"), - ]; - - $raw = $this->text; - - if (preg_match('/\{".*}/s', $raw, $m)) { - $jsonString = $m[0]; - $pos = Str::position($raw, '{"'); - if ($pos !== false) { - $cleanText = substr($raw, 0, $pos); - $this->text = $cleanText; - } - - $escaped = $this->sanitizeJson($jsonString); - - $contextArray = json_decode($escaped, true); - - if (json_last_error() !== JSON_ERROR_NONE) { - $contextArray = []; - } - } else { - $contextArray = []; - } - - $this->context = $contextArray; - } - - public function getOriginalText(): ?string - { - if (! $this->text) { - return null; - } - - $pos = Str::position($this->text, '| '); - if ($pos === false) { - return $this->text; - } - - return Str::substr($this->text, $pos + 2); - } - - protected function sanitizeJson(string $json): string - { - return Str::replace( - ["\n", "\t"], - ['\\n', '\\t'], - $json - ); - } -} diff --git a/src/Logs/SummaryLog.php b/src/Logs/SummaryLog.php new file mode 100644 index 00000000..ffc8394a --- /dev/null +++ b/src/Logs/SummaryLog.php @@ -0,0 +1,55 @@ + 'Severity', 'data_path' => 'level'], + ['label' => 'First', 'data_path' => 'extra.first_datetime'], + ['label' => 'Last', 'data_path' => 'datetime'], + ['label' => 'Env', 'data_path' => 'extra.environment'], + ['label' => 'Count', 'data_path' => 'extra.count'], + ['label' => 'Message', 'data_path' => 'message'], + ]; + protected static string $regexContextKey = 'context'; + + protected function parseText(array &$matches = []): void + { + $data = json_decode($this->text, true) ?: []; + + $matches[static::$regexDatetimeKey] = $data['last'] ?? ''; + $matches[static::$regexLevelKey] = $data['level'] ?? ''; + $matches[static::$regexMessageKey] = $data['message'] ?? ''; + $matches[static::$regexContextKey] = $data['context'] ?? []; + + $this->extra = [ + 'first_datetime' => Carbon::parse($data['first'] ?? null) + ->setTimezone(LogViewer::timezone()) + ->format("Y\u{2011}m\u{2011}d\u{00A0}H:i:s"), + 'environment' => $data['env'] ?? null, + 'count' => $data['count'] ?? null, + 'context' => $data['context'] ?? [], + ]; + + $this->text = $data['message'] ?? ''; + } + + protected function fillMatches(array $matches = []): void + { + parent::fillMatches($matches); + + $this->context = $matches[static::$regexContextKey]; + } +} From 53175b63b2e87fcea6f5e6f5ea6fcd807ad7eb93 Mon Sep 17 00:00:00 2001 From: Joshua Dias Date: Fri, 13 Jun 2025 16:56:09 -0400 Subject: [PATCH 4/4] Fixes namespaces --- src/Console/Commands/SummaryLogCommand.php | 8 ++++---- src/Logs/SummaryLog.php | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Console/Commands/SummaryLogCommand.php b/src/Console/Commands/SummaryLogCommand.php index 8ca68fea..edb91ac5 100644 --- a/src/Console/Commands/SummaryLogCommand.php +++ b/src/Console/Commands/SummaryLogCommand.php @@ -1,6 +1,6 @@ name, 'laravel.log')) { + if (! Str::endsWith($file->name, 'laravel.log')) { continue; } @@ -71,7 +71,7 @@ protected static function generateSummaryFile(int $number): void if (! isset($summary[trim($message)])) { $summary[$message] = [ 'first' => $ts, - 'last' => $ts, + 'last' => $ts, 'count' => 1, 'level' => $level, 'env' => $env, @@ -92,7 +92,7 @@ protected static function generateSummaryFile(int $number): void } } - $lines = array_map(function(array $data) { + $lines = array_map(function (array $data) { return json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); }, array_values($summary)); diff --git a/src/Logs/SummaryLog.php b/src/Logs/SummaryLog.php index ffc8394a..c59a1a3e 100644 --- a/src/Logs/SummaryLog.php +++ b/src/Logs/SummaryLog.php @@ -1,6 +1,6 @@