diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index b729062b..cc5f91d6 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -9,7 +9,7 @@ jobs: strategy: fail-fast: true matrix: - php: [8.2, 8.3] + php: [7.4,8.0,8.2,8.3] dependency-version: [prefer-lowest, prefer-stable] name: PHP ${{ matrix.php }} - ${{ matrix.dependency-version }} diff --git a/composer.json b/composer.json index 0c23d399..59f91249 100644 --- a/composer.json +++ b/composer.json @@ -9,21 +9,21 @@ "email": "arcanedev.maroc@gmail.com", "homepage": "https://github.com/arcanedev-maroc", "role": "Developer" - } - ], + } + ], "type": "library", "license": "MIT", - "require": { - "php": "^8.2", + "require": { + "php": ">=7.3", "ext-json": "*", - "arcanedev/support": "^11.0", + "arcanedev/support": ">=8.0", "psr/log": "^1.0|^2.0|^3.0" }, "require-dev": { - "laravel/framework": "^11.0", - "mockery/mockery": "^1.6", - "orchestra/testbench-core": "^9.0", - "phpunit/phpunit": "^10.5" + "laravel/framework": ">=8.0", + "mockery/mockery": ">=1.4.2", + "orchestra/testbench-core": ">=6.27", + "phpunit/phpunit": ">=9.5.10" }, "autoload": { "psr-4": { diff --git a/config/log-viewer.php b/config/log-viewer.php index 18ced1d8..e4222f55 100644 --- a/config/log-viewer.php +++ b/config/log-viewer.php @@ -70,6 +70,15 @@ 'per-page' => 30, + /* ----------------------------------------------------------------- + | Type of log formatter used + | ----------------------------------------------------------------- + | Supported formatters : + | 'LineFormatter', 'JsonFormatter' + */ + + 'formatter' => 'default', + /* ----------------------------------------------------------------- | Download settings | ----------------------------------------------------------------- diff --git a/src/Contracts/LogEntryInterface.php b/src/Contracts/LogEntryInterface.php new file mode 100644 index 00000000..a83e9351 --- /dev/null +++ b/src/Contracts/LogEntryInterface.php @@ -0,0 +1,28 @@ + + */ +interface LogEntryInterface { + + /** + * Check if same log level. + * + * @param string $level + * + * @return bool + */ + public function isSameLevel($level); + + + /** + * Get the entry context as json pretty print. + */ + public function context(int $options = JSON_PRETTY_PRINT): string; +} \ No newline at end of file diff --git a/src/Entities/JsonLogEntry.php b/src/Entities/JsonLogEntry.php new file mode 100644 index 00000000..5b5f1f13 --- /dev/null +++ b/src/Entities/JsonLogEntry.php @@ -0,0 +1,291 @@ + + */ +class JsonLogEntry implements Arrayable, Jsonable, JsonSerializable, LogEntryInterface +{ + /* ----------------------------------------------------------------- + | Properties + | ----------------------------------------------------------------- + */ + + /** @var string */ + public $env; + + /** @var string */ + public $level; + + /** @var \Carbon\Carbon */ + public $datetime; + + /** @var string */ + public $header; + + /** @var string */ + public $stack; + + /** @var array */ + public $context = []; + + /* ----------------------------------------------------------------- + | Constructor + | ----------------------------------------------------------------- + */ + + /** + * Construct the log entry instance. + * + * @param string $level + * @param string $header + * @param string|null $stack + */ + public function __construct($data) + { + $this->setLevel($data); + $this->setHeader($data); + $this->setStack($data); + } + + /* ----------------------------------------------------------------- + | Getters & Setters + | ----------------------------------------------------------------- + */ + + /** + * Set the entry level. + * + * @param string $level + * + * @return self + */ + private function setLevel($data) + { + $this->level = strtolower($data['level_name'] ?? ''); + + return $this; + } + + /** + * Set the entry header. + * + * @param string $header + * + * @return self + */ + private function setHeader($data) + { + $this->setDatetime($data['datetime'] ?? ''); + + $header = $data['message']; + + $this->header = trim($header); + + $this->setContext($data['context']); + + $this->setEnv($data['channel']); + + return $this; + } + + /** + * Set the context. + * + * @param array $context + * + * @return $this + */ + private function setContext(array $context) + { + $this->context = $context; + + return $this; + } + + /** + * Set entry environment. + * + * @param string $env + * + * @return self + */ + private function setEnv($env) + { + $this->env = head(explode('.', $env)); + + return $this; + } + + /** + * Set the entry date time. + * + * @param string $datetime + * + * @return \Arcanedev\LogViewer\Entities\LogEntry + */ + private function setDatetime($datetime) + { + $this->datetime = Carbon::createFromFormat('Y-m-d H:i:s', date('Y-m-d H:i:s',strtotime($datetime))); + + return $this; + } + + /** + * Set the entry stack. + * + * @param string $stack + * + * @return self + */ + private function setStack($data) + { + $this->stack = isset($data['stack']) ? json_encode($data['stack']): ""; + + return $this; + } + + /** + * Get translated level name with icon. + * + * @return string + */ + public function level() + { + return $this->icon()->toHtml().' '.$this->name(); + } + + /** + * Get translated level name. + * + * @return string + */ + public function name() + { + return log_levels()->get($this->level); + } + + /** + * Get level icon. + * + * @return \Illuminate\Support\HtmlString + */ + public function icon() + { + return log_styler()->icon($this->level); + } + + /** + * Get the entry stack. + * + * @return string + */ + public function stack() + { + return trim(htmlentities($this->stack)); + } + + /** + * Get the entry context as json pretty print. + * + * @return string + */ + public function context(int $options = JSON_PRETTY_PRINT):string + { + return json_encode($this->context, JSON_PRETTY_PRINT); + } + + /* ----------------------------------------------------------------- + | Check Methods + | ----------------------------------------------------------------- + */ + + /** + * Check if same log level. + * + * @param string $level + * + * @return bool + */ + public function isSameLevel($level) + { + return $this->level === $level; + } + + /* ----------------------------------------------------------------- + | Convert Methods + | ----------------------------------------------------------------- + */ + + /** + * Get the log entry as an array. + * + * @return array + */ + public function toArray() + { + return [ + 'level' => $this->level, + 'datetime' => $this->datetime->format('Y-m-d H:i:s'), + 'header' => $this->header, + 'stack' => $this->stack + ]; + } + + /** + * Convert the log entry to its JSON representation. + * + * @param int $options + * + * @return string + */ + public function toJson($options = 0) + { + return json_encode($this->toArray(), $options); + } + + /** + * Serialize the log entry object to json data. + * + * @return array + */ + public function jsonSerialize(): array + { + return $this->toArray(); + } + + /* ----------------------------------------------------------------- + | Check Methods + | ----------------------------------------------------------------- + */ + + /** + * Check if the entry has a stack. + * + * @return bool + */ + public function hasStack() + { + return $this->stack !== "\n"; + } + + /** + * Check if the entry has a context. + * + * @return bool + */ + public function hasContext() + { + return ! empty($this->context); + } +} diff --git a/src/Entities/LogEntry.php b/src/Entities/LogEntry.php index 9cb58c77..d09b4416 100644 --- a/src/Entities/LogEntry.php +++ b/src/Entities/LogEntry.php @@ -4,6 +4,7 @@ namespace Arcanedev\LogViewer\Entities; +use Arcanedev\LogViewer\Contracts\LogEntryInterface; use Arcanedev\LogViewer\Helpers\LogParser; use Carbon\Carbon; use Illuminate\Contracts\Support\{Arrayable, Jsonable}; @@ -14,7 +15,7 @@ * * @author ARCANEDEV */ -class LogEntry implements Arrayable, Jsonable, JsonSerializable +class LogEntry implements Arrayable, Jsonable, JsonSerializable,LogEntryInterface { /* ----------------------------------------------------------------- | Properties diff --git a/src/Entities/LogEntryCollection.php b/src/Entities/LogEntryCollection.php index 4c534609..4f0b7fd4 100644 --- a/src/Entities/LogEntryCollection.php +++ b/src/Entities/LogEntryCollection.php @@ -4,6 +4,7 @@ namespace Arcanedev\LogViewer\Entities; +use Arcanedev\LogViewer\Contracts\LogEntryInterface; use Arcanedev\LogViewer\Helpers\LogParser; use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Support\LazyCollection; @@ -29,13 +30,33 @@ class LogEntryCollection extends LazyCollection */ public static function load($raw) { - return new static(function () use ($raw) { - foreach (LogParser::parse($raw) as $entry) { - list($level, $header, $stack) = array_values($entry); + $formatter = config('log-viewer.formatter', 'default'); - yield new LogEntry($level, $header, $stack); - } - }); + if ( $formatter == 'default' ) + { + return new static(function () use ($raw ) { + foreach (LogParser::parse($raw) as $entry) { + list($level, $header, $stack) = array_values($entry); + + yield new LogEntry($level, $header, $stack); + } + }); + + } else { + $rows = explode("\n",$raw); + return new static(function () use ($rows ) { + foreach( $rows as $row) { + + $entry = json_decode($row,true); + + if( $entry && gettype($entry) == 'array') { + yield new JsonLogEntry($entry); + } + + } + + }); + } } /** @@ -68,7 +89,7 @@ public function paginate($perPage = 20) */ public function filterByLevel($level) { - return $this->filter(function(LogEntry $entry) use ($level) { + return $this->filter(function(LogEntryInterface $entry) use ($level) { return $entry->isSameLevel($level); }); } diff --git a/src/Http/Controllers/LogViewerController.php b/src/Http/Controllers/LogViewerController.php index 389dc7ad..792c0cb7 100644 --- a/src/Http/Controllers/LogViewerController.php +++ b/src/Http/Controllers/LogViewerController.php @@ -4,8 +4,9 @@ namespace Arcanedev\LogViewer\Http\Controllers; +use Arcanedev\LogViewer\Contracts\LogEntryInterface; use Arcanedev\LogViewer\Contracts\LogViewer as LogViewerContract; -use Arcanedev\LogViewer\Entities\{LogEntry, LogEntryCollection}; +use Arcanedev\LogViewer\Entities\LogEntryCollection; use Arcanedev\LogViewer\Exceptions\LogNotFoundException; use Arcanedev\LogViewer\Tables\StatsTable; use Illuminate\Http\Request; @@ -104,7 +105,7 @@ public function show(Request $request, $date) $log = $this->getLogOrFail($date); $query = $request->get('query'); $levels = $this->logViewer->levelsNames(); - $entries = $log->entries($level)->paginate($this->perPage); + $entries = $log->entries($level)->sortDesc()->paginate($this->perPage); return $this->view('show', compact('level', 'log', 'query', 'levels', 'entries')); } @@ -126,7 +127,7 @@ public function showByLevel(Request $request, $date, $level) $log = $this->getLogOrFail($date); $query = $request->get('query'); $levels = $this->logViewer->levelsNames(); - $entries = $this->logViewer->entries($date, $level)->paginate($this->perPage); + $entries = $this->logViewer->entries($date, $level)->sortDesc()->paginate($this->perPage); return $this->view('show', compact('level', 'log', 'query', 'levels', 'entries')); } @@ -144,9 +145,10 @@ public function search(Request $request, $date, $level = 'all') { $query = $request->get('query'); - if (is_null($query)) + if (is_null($query)){ return redirect()->route($this->showRoute, [$date]); - + } + $log = $this->getLogOrFail($date); $levels = $this->logViewer->levelsNames(); $needles = array_map(function ($needle) { @@ -154,15 +156,17 @@ public function search(Request $request, $date, $level = 'all') }, array_filter(explode(' ', $query))); $entries = $log->entries($level) ->unless(empty($needles), function (LogEntryCollection $entries) use ($needles) { - return $entries->filter(function (LogEntry $entry) use ($needles) { + return $entries->filter(function (LogEntryInterface $entry) use ($needles) { foreach ([$entry->header, $entry->stack, $entry->context()] as $subject) { - if (Str::containsAll(Str::lower($subject), $needles)) + if (Str::containsAll(Str::lower($subject), $needles)){ return true; + } + } return false; }); - }) + })->sortDesc() ->paginate($this->perPage); return $this->view('show', compact('level', 'log', 'query', 'levels', 'entries')); diff --git a/views/bootstrap-3/show.blade.php b/views/bootstrap-3/show.blade.php index d2467e78..1687345f 100644 --- a/views/bootstrap-3/show.blade.php +++ b/views/bootstrap-3/show.blade.php @@ -124,7 +124,7 @@ @lang('ENV') @lang('Level') @lang('Time') - @lang('Header') + @lang('Message') @lang('Actions') @@ -165,14 +165,19 @@ @if ($entry->hasStack()) -
+
{!! $entry->stack() !!}
@endif @if ($entry->hasContext()) -
-
{{ $entry->context() }}
+
+ +
{{ $entry->context() }}
@endif @@ -282,5 +287,20 @@ }); @endunless }); + function copyToClipboard(key) { + const content = document.getElementById("log-content-" + key).innerText; + navigator.clipboard.writeText(content).then(() => { + // Optional: Replace icon with checkmark for 1.5s + const btn = event.currentTarget; + const icon = btn.querySelector("i"); + icon.classList.replace("bi-clipboard", "bi-clipboard-check"); + setTimeout(() => { + icon.classList.replace("bi-clipboard-check", "bi-clipboard"); + }, 1500); + + }).catch(err => { + console.error("Failed to copy: ", err); + }); + } @endsection diff --git a/views/bootstrap-4/show.blade.php b/views/bootstrap-4/show.blade.php index a3e2e468..c4a5622a 100644 --- a/views/bootstrap-4/show.blade.php +++ b/views/bootstrap-4/show.blade.php @@ -116,7 +116,7 @@ @lang('ENV') @lang('Level') @lang('Time') - @lang('Header') + @lang('Message') @lang('Actions') @@ -159,14 +159,19 @@ @if ($entry->hasStack()) -
+
{!! $entry->stack() !!}
@endif @if ($entry->hasContext()) -
-
{{ $entry->context() }}
+
+ +
{{ $entry->context() }}
@endif @@ -269,5 +274,20 @@ }); @endunless }); + function copyToClipboard(key) { + const content = document.getElementById("log-content-" + key).innerText; + navigator.clipboard.writeText(content).then(() => { + // Optional: Replace icon with checkmark for 1.5s + const btn = event.currentTarget; + const icon = btn.querySelector("i"); + icon.classList.replace("bi-clipboard", "bi-clipboard-check"); + setTimeout(() => { + icon.classList.replace("bi-clipboard-check", "bi-clipboard"); + }, 1500); + + }).catch(err => { + console.error("Failed to copy: ", err); + }); + } @endsection diff --git a/views/bootstrap-5/show.blade.php b/views/bootstrap-5/show.blade.php index 6b5f695d..cb83329d 100644 --- a/views/bootstrap-5/show.blade.php +++ b/views/bootstrap-5/show.blade.php @@ -114,7 +114,7 @@ @lang('ENV') @lang('Level') @lang('Time') - @lang('Header') + @lang('Message') @lang('Actions') @@ -158,14 +158,19 @@ @if ($entry->hasStack()) -
+
{!! $entry->stack() !!}
@endif @if ($entry->hasContext()) -
-
{{ $entry->context() }}
+
+ +
{{ $entry->context() }}
@endif @@ -267,5 +272,20 @@ }) @endunless }); + function copyToClipboard(key) { + const content = document.getElementById("log-content-" + key).innerText; + navigator.clipboard.writeText(content).then(() => { + // Optional: Replace icon with checkmark for 1.5s + const btn = event.currentTarget; + const icon = btn.querySelector("i"); + icon.classList.replace("bi-clipboard", "bi-clipboard-check"); + setTimeout(() => { + icon.classList.replace("bi-clipboard-check", "bi-clipboard"); + }, 1500); + + }).catch(err => { + console.error("Failed to copy: ", err); + }); + } @endsection