Uploaded From CV. Swandhana Server

This commit is contained in:
Duidev Software House
2025-01-27 08:16:55 +07:00
commit 6b3be42361
15186 changed files with 2328862 additions and 0 deletions

No files matched your search

+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) ARCANEDEV <arcanedev.maroc@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+118
View File
@@ -0,0 +1,118 @@
# LogViewer [![Packagist License][badge_license]](LICENSE.md) [![For Laravel][badge_laravel]][link-github-repo]
[![Github Workflow Status][badge_build]][link-github-status]
[![Coverage Status][badge_coverage]][link-scrutinizer]
[![Scrutinizer Code Quality][badge_quality]][link-scrutinizer]
[![SensioLabs Insight][badge_insight]][link-insight]
[![Github Issues][badge_issues]][link-github-issues]
[![Packagist][badge_package]][link-packagist]
[![Packagist Release][badge_release]][link-packagist]
[![Packagist Downloads][badge_downloads]][link-packagist]
*By [ARCANEDEV&copy;](http://www.arcanedev.net/)*
This package allows you to manage and keep track of each one of your log files.
> **NOTE: You can also use LogViewer as an API.**
Official documentation for LogViewer can be found at the [_docs folder](_docs/1.Installation-and-Setup.md).
Feel free to check out the [releases](https://github.com/ARCANEDEV/LogViewer/releases), [license](LICENSE.md), and [contribution guidelines](CONTRIBUTING.md).
## Features
- A great Log viewer API.
- Laravel `5.x` to `10.x` are supported.
- Ready to use (Views, Routes, controllers &hellip; Out of the box) [Note: No need to publish assets]
- View, paginate, filter, download and delete logs.
- Load a custom logs storage path.
- Localized log levels.
- Logs menu/tree generator.
- Grouped logs by dates and levels.
- Customized log levels icons (font awesome by default).
- Works great with big logs !!
- Well documented package (IDE Friendly).
- Well tested (100% code coverage with maximum code quality).
## Table of contents
1. [Installation and Setup](_docs/1.Installation-and-Setup.md)
2. [Configuration](_docs/2.Configuration.md)
3. [Usage](_docs/3.Usage.md)
### Supported localizations
> Dear artisans, i'm counting on you to help me out to add more translations ( ^_^)b
| Local | Language |
|---------|-----------------------|
| `ar` | Arabic |
| `bg` | Bulgarian |
| `bn` | Bengali |
| `de` | German |
| `en` | English |
| `es` | Spanish |
| `et` | Estonian |
| `fa` | Farsi |
| `fr` | French |
| `he` | Hebrew |
| `hu` | Hungarian |
| `hy` | Armenian |
| `id` | Indonesian |
| `it` | Italian |
| `ja` | Japanese |
| `ko` | Korean |
| `ms` | Malay |
| `nl` | Dutch |
| `pl` | Polish |
| `pt-BR` | Brazilian Portuguese |
| `ro` | Romanian |
| `ru` | Russian |
| `si` | Sinhalese |
| `sv` | Swedish |
| `th` | Thai |
| `tr` | Turkish |
| `uz` | Uzbek |
| `uk` | Ukrainian |
| `zh` | Chinese (Simplified) |
| `zh-TW` | Chinese (Traditional) |
## Contribution
Any ideas are welcome. Feel free to submit any issues or pull requests, please check the [contribution guidelines](CONTRIBUTING.md).
## Security
If you discover any security related issues, please email arcanedev.maroc@gmail.com instead of using the issue tracker.
## Credits
- [ARCANEDEV][link-author]
- [All Contributors][link-contributors]
## PREVIEW
![Dashboard](https://raw.githubusercontent.com/ARCANEDEV/LogViewer/master/_screenshots/1-dashboard.jpg)
![Logs list](https://raw.githubusercontent.com/ARCANEDEV/LogViewer/master/_screenshots/2-logs-list.jpg)
![Single log](https://raw.githubusercontent.com/ARCANEDEV/LogViewer/master/_screenshots/3-single-log.jpg)
[badge_laravel]: https://img.shields.io/badge/Laravel-5.x%20to%2010.x-orange.svg?style=flat-square
[badge_license]: https://img.shields.io/packagist/l/arcanedev/log-viewer.svg?style=flat-square
[badge_build]: https://img.shields.io/github/workflow/status/ARCANEDEV/LogViewer/run-tests?style=flat-square
[badge_coverage]: https://img.shields.io/scrutinizer/coverage/g/ARCANEDEV/LogViewer.svg?style=flat-square
[badge_quality]: https://img.shields.io/scrutinizer/g/ARCANEDEV/LogViewer.svg?style=flat-square
[badge_insight]: https://img.shields.io/sensiolabs/i/d6b4587a-e00d-4dac-98db-f979731fdc4d.svg?style=flat-square
[badge_issues]: https://img.shields.io/github/issues/ARCANEDEV/LogViewer.svg?style=flat-square
[badge_package]: https://img.shields.io/badge/package-arcanedev/log--viewer-blue.svg?style=flat-square
[badge_release]: https://img.shields.io/packagist/v/arcanedev/log-viewer.svg?style=flat-square
[badge_downloads]: https://img.shields.io/packagist/dt/arcanedev/log-viewer.svg?style=flat-square
[link-author]: https://github.com/arcanedev-maroc
[link-github-status]: https://github.com/ARCANEDEV/LogViewer/actions
[link-github-repo]: https://github.com/ARCANEDEV/LogViewer
[link-github-issues]: https://github.com/ARCANEDEV/LogViewer/issues
[link-contributors]: https://github.com/ARCANEDEV/LogViewer/graphs/contributors
[link-packagist]: https://packagist.org/packages/arcanedev/log-viewer
[link-scrutinizer]: https://scrutinizer-ci.com/g/ARCANEDEV/LogViewer/?branch=master
[link-insight]: https://insight.sensiolabs.com/projects/d6b4587a-e00d-4dac-98db-f979731fdc4d
+60
View File
@@ -0,0 +1,60 @@
{
"name": "arcanedev/log-viewer",
"description": "Provides a Log Viewer for Laravel",
"keywords": ["arcanedev", "arcanesoft", "laravel", "log", "log viewer", "log-viewer", "logviewer"],
"homepage": "https://github.com/ARCANEDEV/LogViewer",
"authors": [
{
"name": "ARCANEDEV",
"email": "[email protected]",
"homepage": "https://github.com/arcanedev-maroc",
"role": "Developer"
}
],
"type": "library",
"license": "MIT",
"require": {
"php": "^8.1",
"ext-json": "*",
"arcanedev/support": "^10.0",
"psr/log": "^1.0|^2.0|^3.0"
},
"require-dev": {
"laravel/framework": "^10.0",
"mockery/mockery": "^1.5.1",
"orchestra/testbench-core": "^8.0",
"phpunit/phpunit": "^10.1"
},
"autoload": {
"psr-4": {
"Arcanedev\\LogViewer\\": "src/"
},
"files": ["helpers.php"]
},
"autoload-dev": {
"psr-4": {
"Arcanedev\\LogViewer\\Tests\\": "tests/"
}
},
"scripts": {
"test": "phpunit --colors=always",
"test:dox": "phpunit --testdox --colors=always",
"test:cov": "phpunit --coverage-html build/coverage"
},
"extra": {
"branch-alias": {
"dev-develop": "10.x-dev"
},
"laravel": {
"providers": [
"Arcanedev\\LogViewer\\LogViewerServiceProvider",
"Arcanedev\\LogViewer\\Providers\\DeferredServicesProvider"
]
}
},
"config": {
"sort-packages": true
},
"minimum-stability": "dev",
"prefer-stable": true
}
+146
View File
@@ -0,0 +1,146 @@
<?php
use Arcanedev\LogViewer\Contracts\Utilities\Filesystem;
return [
/* -----------------------------------------------------------------
| Log files storage path
| -----------------------------------------------------------------
*/
'storage-path' => storage_path('logs'),
/* -----------------------------------------------------------------
| Log files pattern
| -----------------------------------------------------------------
*/
'pattern' => [
'prefix' => Filesystem::PATTERN_PREFIX, // 'laravel-'
'date' => Filesystem::PATTERN_DATE, // '[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]'
'extension' => Filesystem::PATTERN_EXTENSION, // '.log'
],
/* -----------------------------------------------------------------
| Locale
| -----------------------------------------------------------------
| Supported locales :
| 'auto', 'ar', 'bg', 'de', 'en', 'es', 'et', 'fa', 'fr', 'hu', 'hy', 'id', 'it', 'ja', 'ko', 'nl',
| 'pl', 'pt-BR', 'ro', 'ru', 'sv', 'th', 'tr', 'zh-TW', 'zh'
*/
'locale' => 'auto',
/* -----------------------------------------------------------------
| Theme
| -----------------------------------------------------------------
| Supported themes :
| 'bootstrap-5'
| 'bootstrap-4'
| 'bootstrap-3'
|
| You can make your own theme by adding a folder to the views directory and specifying it here.
*/
'theme' => 'bootstrap-5',
/* -----------------------------------------------------------------
| Route settings
| -----------------------------------------------------------------
*/
'route' => [
'enabled' => true,
'attributes' => [
'prefix' => 'log-viewer',
'middleware' => env('ARCANEDEV_LOGVIEWER_MIDDLEWARE') ? explode(',', env('ARCANEDEV_LOGVIEWER_MIDDLEWARE')) : null,
],
'show' => 'log-viewer::logs.show'
],
/* -----------------------------------------------------------------
| Log entries per page
| -----------------------------------------------------------------
| This defines how many logs & entries are displayed per page.
*/
'per-page' => 30,
/* -----------------------------------------------------------------
| Download settings
| -----------------------------------------------------------------
*/
'download' => [
'prefix' => 'laravel-',
'extension' => 'log',
],
/* -----------------------------------------------------------------
| Menu settings
| -----------------------------------------------------------------
*/
'menu' => [
'filter-route' => 'log-viewer::logs.filter',
'icons-enabled' => true,
],
/* -----------------------------------------------------------------
| Icons
| -----------------------------------------------------------------
*/
'icons' => [
/**
* Font awesome >= 4.3
* http://fontawesome.io/icons/
*/
'all' => 'fa fa-fw fa-list', // http://fontawesome.io/icon/list/
'emergency' => 'fa fa-fw fa-bug', // http://fontawesome.io/icon/bug/
'alert' => 'fa fa-fw fa-bullhorn', // http://fontawesome.io/icon/bullhorn/
'critical' => 'fa fa-fw fa-heartbeat', // http://fontawesome.io/icon/heartbeat/
'error' => 'fa fa-fw fa-times-circle', // http://fontawesome.io/icon/times-circle/
'warning' => 'fa fa-fw fa-exclamation-triangle', // http://fontawesome.io/icon/exclamation-triangle/
'notice' => 'fa fa-fw fa-exclamation-circle', // http://fontawesome.io/icon/exclamation-circle/
'info' => 'fa fa-fw fa-info-circle', // http://fontawesome.io/icon/info-circle/
'debug' => 'fa fa-fw fa-life-ring', // http://fontawesome.io/icon/life-ring/
],
/* -----------------------------------------------------------------
| Colors
| -----------------------------------------------------------------
*/
'colors' => [
'levels' => [
'empty' => '#D1D1D1',
'all' => '#8A8A8A',
'emergency' => '#B71C1C',
'alert' => '#D32F2F',
'critical' => '#F44336',
'error' => '#FF5722',
'warning' => '#FF9100',
'notice' => '#4CAF50',
'info' => '#1976D2',
'debug' => '#90CAF9',
],
],
/* -----------------------------------------------------------------
| Strings to highlight in stack trace
| -----------------------------------------------------------------
*/
'highlight' => [
'^#\d+',
'^Stack trace:',
],
];
+51
View File
@@ -0,0 +1,51 @@
<?php
use Arcanedev\LogViewer\Contracts;
if ( ! function_exists('log_viewer')) {
/**
* Get the LogViewer instance.
*
* @return Arcanedev\LogViewer\Contracts\LogViewer
*/
function log_viewer()
{
return app(Contracts\LogViewer::class);
}
}
if ( ! function_exists('log_levels')) {
/**
* Get the LogLevels instance.
*
* @return Arcanedev\LogViewer\Contracts\Utilities\LogLevels
*/
function log_levels()
{
return app(Contracts\Utilities\LogLevels::class);
}
}
if ( ! function_exists('log_menu')) {
/**
* Get the LogMenu instance.
*
* @return Arcanedev\LogViewer\Contracts\Utilities\LogMenu
*/
function log_menu()
{
return app(Contracts\Utilities\LogMenu::class);
}
}
if ( ! function_exists('log_styler')) {
/**
* Get the LogStyler instance.
*
* @return Arcanedev\LogViewer\Contracts\Utilities\LogStyler
*/
function log_styler()
{
return app(Contracts\Utilities\LogStyler::class);
}
}
@@ -0,0 +1,112 @@
<?php
declare(strict_types=1);
namespace Arcanedev\LogViewer\Commands;
use Arcanedev\LogViewer\Contracts\Utilities\LogChecker as LogCheckerContract;
/**
* Class CheckCommand
*
* @author ARCANEDEV <[email protected]>
*/
class CheckCommand extends Command
{
/* -----------------------------------------------------------------
| Properties
| -----------------------------------------------------------------
*/
/**
* The console command name.
*
* @var string
*/
protected $name = 'log-viewer:check';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Check all LogViewer requirements.';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'log-viewer:check';
/* -----------------------------------------------------------------
| Getter & Setters
| -----------------------------------------------------------------
*/
/**
* Get the Log Checker instance.
*
* @return \Arcanedev\LogViewer\Contracts\Utilities\LogChecker
*/
protected function getChecker()
{
return $this->laravel[LogCheckerContract::class];
}
/* -----------------------------------------------------------------
| Main Methods
| -----------------------------------------------------------------
*/
/**
* Execute the console command.
*/
public function handle(): int
{
$this->displayLogViewer();
$this->displayRequirements();
$this->displayMessages();
return static::SUCCESS;
}
/* -----------------------------------------------------------------
| Other Methods
| -----------------------------------------------------------------
*/
/**
* Display LogViewer requirements.
*/
private function displayRequirements()
{
$requirements = $this->getChecker()->requirements();
$this->frame('Application requirements');
$this->table([
'Status', 'Message'
], [
[$requirements['status'], $requirements['message']]
]);
}
/**
* Display LogViewer messages.
*/
private function displayMessages()
{
$messages = $this->getChecker()->messages();
$rows = [];
foreach ($messages['files'] as $file => $message) {
$rows[] = [$file, $message];
}
if ( ! empty($rows)) {
$this->frame('LogViewer messages');
$this->table(['File', 'Message'], $rows);
}
}
}
@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Arcanedev\LogViewer\Commands;
/**
* Class ClearCommand
*
* @author ARCANEDEV <[email protected]>
*/
class ClearCommand extends Command
{
/* -----------------------------------------------------------------
| Properties
| -----------------------------------------------------------------
*/
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'log-viewer:clear';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Clear all generated log files';
/* -----------------------------------------------------------------
| Main Methods
| -----------------------------------------------------------------
*/
/**
* Execute the console command.
*/
public function handle(): int
{
if ($this->confirm('This will delete all the log files, Do you wish to continue?')) {
$this->logViewer->clear();
$this->info('Successfully cleared the logs!');
}
return static::SUCCESS;
}
}
+65
View File
@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace Arcanedev\LogViewer\Commands;
use Arcanedev\LogViewer\Contracts\LogViewer as LogViewerContract;
use Arcanedev\Support\Console\Command as BaseCommand;
/**
* Class Command
*
* @author ARCANEDEV <[email protected]>
*/
abstract class Command extends BaseCommand
{
/* -----------------------------------------------------------------
| Properties
| -----------------------------------------------------------------
*/
/** @var \Arcanedev\LogViewer\Contracts\LogViewer */
protected $logViewer;
/* -----------------------------------------------------------------
| Constructor
| -----------------------------------------------------------------
*/
/**
* Create the command instance.
*
* @param \Arcanedev\LogViewer\Contracts\LogViewer $logViewer
*/
public function __construct(LogViewerContract $logViewer)
{
parent::__construct();
$this->logViewer = $logViewer;
}
/* -----------------------------------------------------------------
| Other Methods
| -----------------------------------------------------------------
*/
/**
* Display LogViewer Logo and Copyrights.
*/
protected function displayLogViewer()
{
// LOGO
$this->comment(' __ _ ');
$this->comment(' / / ___ __ _/\ /(_) _____ _____ _ __ ');
$this->comment(' / / / _ \ / _` \ \ / / |/ _ \ \ /\ / / _ \ \'__|');
$this->comment('/ /__| (_) | (_| |\ V /| | __/\ V V / __/ | ');
$this->comment('\____/\___/ \__, | \_/ |_|\___| \_/\_/ \___|_| ');
$this->comment(' |___/ ');
$this->line('');
// Copyright
$this->comment('Version '.$this->logViewer->version().' - Created by ARCANEDEV'.chr(169));
$this->line('');
}
}
@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace Arcanedev\LogViewer\Commands;
use Arcanedev\LogViewer\LogViewerServiceProvider;
use Symfony\Component\Console\Input\InputOption;
/**
* Class PublishCommand
*
* @author ARCANEDEV <[email protected]>
*/
class PublishCommand extends Command
{
/* -----------------------------------------------------------------
| Properties
| -----------------------------------------------------------------
*/
/**
* The console command name.
*
* @var string
*/
protected $name = 'log-viewer:publish';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Publish all LogViewer resources and config files';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'log-viewer:publish
{--tag= : One or many tags that have assets you want to publish.}
{--force : Overwrite any existing files.}';
/* -----------------------------------------------------------------
| Main Methods
| -----------------------------------------------------------------
*/
/**
* Execute the console command.
*/
public function handle(): int
{
$args = [
'--provider' => LogViewerServiceProvider::class,
];
if ((bool) $this->option('force')) {
$args['--force'] = true;
}
$args['--tag'] = [$this->option('tag')];
$this->displayLogViewer();
$this->call('vendor:publish', $args);
return static::SUCCESS;
}
/**
* Get the console command options.
*
* @return array
*
* @codeCoverageIgnore
*/
protected function getOptions()
{
return [
['tag', 't', InputOption::VALUE_OPTIONAL, 'One or many tags that have assets you want to publish.', ''],
['force', 'f', InputOption::VALUE_OPTIONAL, 'Overwrite any existing files.', false],
];
}
}
@@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace Arcanedev\LogViewer\Commands;
use Arcanedev\LogViewer\Tables\StatsTable;
/**
* Class StatsCommand
*
* @author ARCANEDEV <[email protected]>
*/
class StatsCommand extends Command
{
/* -----------------------------------------------------------------
| Properties
| -----------------------------------------------------------------
*/
/**
* The console command name.
*
* @var string
*/
protected $name = 'log-viewer:stats';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Display stats of all logs.';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'log-viewer:stats';
/* -----------------------------------------------------------------
| Main Methods
| -----------------------------------------------------------------
*/
/**
* Execute the console command.
*/
public function handle()
{
// Load Data
$stats = $this->logViewer->statsTable('en');
$rows = $stats->rows();
$rows[] = $this->tableSeparator();
$rows[] = $this->prepareFooter($stats);
// Display Data
$this->displayLogViewer();
$this->table($stats->header(), $rows);
return static::SUCCESS;
}
/* -----------------------------------------------------------------
| Other Methods
| -----------------------------------------------------------------
*/
/**
* Prepare footer.
*
* @param \Arcanedev\LogViewer\Tables\StatsTable $stats
*
* @return array
*/
private function prepareFooter(StatsTable $stats)
{
$files = [
'count' => count($stats->rows()).' log file(s)'
];
return $files + $stats->footer();
}
}
+202
View File
@@ -0,0 +1,202 @@
<?php
declare(strict_types=1);
namespace Arcanedev\LogViewer\Contracts;
/**
* Interface LogViewer
*
* @author ARCANEDEV <[email protected]>
*/
interface LogViewer extends Patternable
{
/* -----------------------------------------------------------------
| Getters & Methods
| -----------------------------------------------------------------
*/
/**
* Get the log levels.
*
* @param bool|false $flip
*
* @return array
*/
public function levels($flip = false);
/**
* Get the translated log levels.
*
* @param string|null $locale
*
* @return array
*/
public function levelsNames($locale = null);
/**
* Set the log storage path.
*
* @param string $path
*
* @return self
*/
public function setPath($path);
/* -----------------------------------------------------------------
| Main Methods
| -----------------------------------------------------------------
*/
/**
* Get all logs.
*
* @return \Arcanedev\LogViewer\Entities\LogCollection|\Arcanedev\LogViewer\Entities\Log[]
*/
public function all();
/**
* Paginate all logs.
*
* @param int $perPage
*
* @return \Illuminate\Pagination\LengthAwarePaginator
*/
public function paginate($perPage = 30);
/**
* Get a log.
*
* @param string $date
*
* @return \Arcanedev\LogViewer\Entities\Log
*/
public function get($date);
/**
* Get the log entries.
*
* @param string $date
* @param string $level
*
* @return \Arcanedev\LogViewer\Entities\LogEntryCollection
*/
public function entries($date, $level = 'all');
/**
* Download a log file.
*
* @param string $date
* @param string|null $filename
* @param array $headers
*
* @return \Symfony\Component\HttpFoundation\BinaryFileResponse
*/
public function download($date, $filename = null, $headers = []);
/**
* Get logs statistics.
*
* @return array
*/
public function stats();
/**
* Get logs statistics table.
*
* @param string|null $locale
*
* @return \Arcanedev\LogViewer\Tables\StatsTable
*/
public function statsTable($locale = null);
/**
* Delete the log.
*
* @param string $date
*
* @return bool
*
* @throws \Arcanedev\LogViewer\Exceptions\FilesystemException
*/
public function delete($date);
/**
* Clear the log files.
*
* @return bool
*/
public function clear();
/**
* List the log files.
*
* @return array
*/
public function files();
/**
* List the log files (only dates).
*
* @return array
*/
public function dates();
/**
* Get logs count.
*
* @return int
*/
public function count();
/**
* Get entries total from all logs.
*
* @param string $level
*
* @return int
*/
public function total($level = 'all');
/**
* Get logs tree.
*
* @param bool|false $trans
*
* @return array
*/
public function tree($trans = false);
/**
* Get logs menu.
*
* @param bool|true $trans
*
* @return array
*/
public function menu($trans = true);
/* -----------------------------------------------------------------
| Check Methods
| -----------------------------------------------------------------
*/
/**
* Determine if the log folder is empty or not.
*
* @return bool
*/
public function isEmpty();
/* -----------------------------------------------------------------
| Other Methods
| -----------------------------------------------------------------
*/
/**
* Get the LogViewer version.
*
* @return string
*/
public function version();
}
@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Arcanedev\LogViewer\Contracts;
use Arcanedev\LogViewer\Contracts\Utilities\Filesystem;
/**
* Interface Patternable
*
* @author ARCANEDEV <[email protected]>
*/
interface Patternable
{
/* -----------------------------------------------------------------
| Getters & Setters
| -----------------------------------------------------------------
*/
/**
* Get the log pattern.
*
* @return string
*/
public function getPattern();
/**
* Set the log pattern.
*
* @param string $date
* @param string $prefix
* @param string $extension
*
* @return self
*/
public function setPattern(
$prefix = Filesystem::PATTERN_PREFIX,
$date = Filesystem::PATTERN_DATE,
$extension = Filesystem::PATTERN_EXTENSION
);
}
+39
View File
@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Arcanedev\LogViewer\Contracts;
/**
* Interface Table
*
* @author ARCANEDEV <[email protected]>
*/
interface Table
{
/* -----------------------------------------------------------------
| Getters & Setters
| -----------------------------------------------------------------
*/
/**
* Get table header.
*
* @return array
*/
public function header();
/**
* Get table rows.
*
* @return array
*/
public function rows();
/**
* Get table footer.
*
* @return array
*/
public function footer();
}
@@ -0,0 +1,188 @@
<?php
declare(strict_types=1);
namespace Arcanedev\LogViewer\Contracts\Utilities;
use Arcanedev\LogViewer\Contracts\Patternable;
/**
* Interface Factory
*
* @author ARCANEDEV <[email protected]>
*/
interface Factory extends Patternable
{
/* -----------------------------------------------------------------
| Getters & Setters
| -----------------------------------------------------------------
*/
/**
* Get the filesystem instance.
*
* @return \Arcanedev\LogViewer\Contracts\Utilities\Filesystem
*/
public function getFilesystem();
/**
* Set the filesystem instance.
*
* @param \Arcanedev\LogViewer\Contracts\Utilities\Filesystem $filesystem
*
* @return self
*/
public function setFilesystem(Filesystem $filesystem);
/**
* Get the log levels instance.
*
* @return \Arcanedev\LogViewer\Contracts\Utilities\LogLevels $levels
*/
public function getLevels();
/**
* Set the log levels instance.
*
* @param \Arcanedev\LogViewer\Contracts\Utilities\LogLevels $levels
*
* @return self
*/
public function setLevels(LogLevels $levels);
/**
* Set the log storage path.
*
* @param string $storagePath
*
* @return self
*/
public function setPath($storagePath);
/**
* Get all logs.
*
* @return \Arcanedev\LogViewer\Entities\LogCollection
*/
public function logs();
/* -----------------------------------------------------------------
| Main Methods
| -----------------------------------------------------------------
*/
/**
* Get all logs (alias).
*
* @see logs
*
* @return \Arcanedev\LogViewer\Entities\LogCollection
*/
public function all();
/**
* Paginate all logs.
*
* @param int $perPage
*
* @return \Illuminate\Pagination\LengthAwarePaginator
*/
public function paginate($perPage = 30);
/**
* Get a log by date.
*
* @param string $date
*
* @return \Arcanedev\LogViewer\Entities\Log
*/
public function log($date);
/**
* Get a log by date (alias).
*
* @param string $date
*
* @return \Arcanedev\LogViewer\Entities\Log
*/
public function get($date);
/**
* Get log entries.
*
* @param string $date
* @param string $level
*
* @return \Arcanedev\LogViewer\Entities\LogEntryCollection
*/
public function entries($date, $level = 'all');
/**
* List the log files (dates).
*
* @return array
*/
public function dates();
/**
* Get logs count.
*
* @return int
*/
public function count();
/**
* Get total log entries.
*
* @param string $level
*
* @return int
*/
public function total($level = 'all');
/**
* Get tree menu.
*
* @param bool $trans
*
* @return array
*/
public function tree($trans = false);
/**
* Get tree menu.
*
* @param bool $trans
*
* @return array
*/
public function menu($trans = true);
/**
* Get logs statistics.
*
* @return array
*/
public function stats();
/**
* Get logs statistics table.
*
* @param string|null $locale
*
* @return \Arcanedev\LogViewer\Tables\StatsTable
*/
public function statsTable($locale = null);
/* -----------------------------------------------------------------
| Check Methods
| -----------------------------------------------------------------
*/
/**
* Determine if the log folder is empty or not.
*
* @return bool
*/
public function isEmpty();
}
@@ -0,0 +1,138 @@
<?php
declare(strict_types=1);
namespace Arcanedev\LogViewer\Contracts\Utilities;
use Arcanedev\LogViewer\Contracts\Patternable;
/**
* Interface Filesystem
*
* @author ARCANEDEV <[email protected]>
*/
interface Filesystem extends Patternable
{
/* -----------------------------------------------------------------
| Constants
| -----------------------------------------------------------------
*/
const PATTERN_PREFIX = 'laravel-';
const PATTERN_DATE = '[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]';
const PATTERN_EXTENSION = '.log';
/* -----------------------------------------------------------------
| Getters & Setters
| -----------------------------------------------------------------
*/
/**
* Get the files instance.
*
* @return \Illuminate\Filesystem\Filesystem
*/
public function getInstance();
/**
* Set the log storage path.
*
* @param string $storagePath
*
* @return $this
*/
public function setPath($storagePath);
/**
* Set the log date pattern.
*
* @param string $datePattern
*
* @return $this
*/
public function setDatePattern($datePattern);
/**
* Set the log prefix pattern.
*
* @param string $prefixPattern
*
* @return $this
*/
public function setPrefixPattern($prefixPattern);
/**
* Set the log extension.
*
* @param string $extension
*
* @return $this
*/
public function setExtension($extension);
/* -----------------------------------------------------------------
| Main Methods
| -----------------------------------------------------------------
*/
/**
* Get all log files.
*
* @return array
*/
public function all();
/**
* Get all valid log files.
*
* @return array
*/
public function logs();
/**
* List the log files (Only dates).
*
* @param bool $withPaths
*
* @return array
*/
public function dates($withPaths = false);
/**
* Read the log.
*
* @param string $date
*
* @return string
*
* @throws \Arcanedev\LogViewer\Exceptions\FilesystemException
*/
public function read($date);
/**
* Delete the log.
*
* @param string $date
*
* @return bool
*
* @throws \Arcanedev\LogViewer\Exceptions\FilesystemException
*/
public function delete(string $date);
/**
* Clear the log files.
*
* @return bool
*/
public function clear();
/**
* Get the log file path.
*
* @param string $date
*
* @return string
*/
public function path($date);
}
@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace Arcanedev\LogViewer\Contracts\Utilities;
use Illuminate\Contracts\Config\Repository as ConfigContract;
/**
* Interface LogChecker
*
* @author ARCANEDEV <[email protected]>
*/
interface LogChecker
{
/* -----------------------------------------------------------------
| Constants
| -----------------------------------------------------------------
*/
/**
* @link http://laravel.com/docs/5.4/errors#configuration
* @link https://github.com/Seldaek/monolog/blob/master/doc/02-handlers-formatters-processors.md#log-to-files-and-syslog
*/
const HANDLER_DAILY = 'daily';
const HANDLER_SINGLE = 'single';
const HANDLER_SYSLOG = 'syslog';
const HANDLER_ERRORLOG = 'errorlog';
/* -----------------------------------------------------------------
| Getters & Setters
| -----------------------------------------------------------------
*/
/**
* Set the config instance.
*
* @param \Illuminate\Contracts\Config\Repository $config
*
* @return self
*/
public function setConfig(ConfigContract $config);
/**
* Set the Filesystem instance.
*
* @param \Arcanedev\LogViewer\Contracts\Utilities\Filesystem $filesystem
*
* @return self
*/
public function setFilesystem(Filesystem $filesystem);
/* -----------------------------------------------------------------
| Main Methods
| -----------------------------------------------------------------
*/
/**
* Get messages.
*
* @return array
*/
public function messages();
/**
* Check passes ??
*
* @return bool
*/
public function passes();
/**
* Check fails ??
*
* @return bool
*/
public function fails();
/**
* Get the requirements
*
* @return array
*/
public function requirements();
}
@@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace Arcanedev\LogViewer\Contracts\Utilities;
use Illuminate\Translation\Translator;
/**
* Interface LogLevels
*
* @author ARCANEDEV <[email protected]>
*/
interface LogLevels
{
/* -----------------------------------------------------------------
| Getters & Setters
| -----------------------------------------------------------------
*/
/**
* Set the Translator instance.
*
* @param \Illuminate\Translation\Translator $translator
*
* @return self
*/
public function setTranslator(Translator $translator);
/**
* Get the selected locale.
*
* @return string
*/
public function getLocale();
/**
* Set the selected locale.
*
* @param string $locale
*
* @return self
*/
public function setLocale($locale);
/* -----------------------------------------------------------------
| Main Methods
| -----------------------------------------------------------------
*/
/**
* Get the log levels.
*
* @param bool $flip
*
* @return array
*/
public function lists($flip = false);
/**
* Get translated levels.
*
* @param string|null $locale
*
* @return array
*/
public function names($locale = null);
/**
* Get PSR log levels.
*
* @param bool $flip
*
* @return array
*/
public static function all($flip = false);
/**
* Get the translated level.
*
* @param string $key
* @param string|null $locale
*
* @return string
*/
public function get($key, $locale = null);
}
@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace Arcanedev\LogViewer\Contracts\Utilities;
use Arcanedev\LogViewer\Entities\Log;
use Illuminate\Contracts\Config\Repository as ConfigContract;
/**
* Interface LogMenu
*
* @author ARCANEDEV <[email protected]>
*/
interface LogMenu
{
/* -----------------------------------------------------------------
| Getters & Setters
| -----------------------------------------------------------------
*/
/**
* Set the config instance.
*
* @param \Illuminate\Contracts\Config\Repository $config
*
* @return self
*/
public function setConfig(ConfigContract $config);
/**
* Set the log styler instance.
*
* @param \Arcanedev\LogViewer\Contracts\Utilities\LogStyler $styler
*
* @return self
*/
public function setLogStyler(LogStyler $styler);
/* -----------------------------------------------------------------
| Main Methods
| -----------------------------------------------------------------
*/
/**
* Make log menu.
*
* @param \Arcanedev\LogViewer\Entities\Log $log
* @param bool $trans
*
* @return array
*/
public function make(Log $log, $trans = true);
}
@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Arcanedev\LogViewer\Contracts\Utilities;
/**
* Interface LogStyler
*
* @author ARCANEDEV <[email protected]>
*/
interface LogStyler
{
/* -----------------------------------------------------------------
| Main Methods
| -----------------------------------------------------------------
*/
/**
* Make level icon.
*
* @param string $level
* @param string|null $default
*
* @return \Illuminate\Support\HtmlString
*/
public function icon($level, $default = null);
/**
* Get level color.
*
* @param string $level
* @param string|null $default
*
* @return string
*/
public function color($level, $default = null);
/**
* Get strings to highlight.
*
* @param array $default
*
* @return array
*/
public function toHighlight(array $default = []);
}
+254
View File
@@ -0,0 +1,254 @@
<?php
declare(strict_types=1);
namespace Arcanedev\LogViewer\Entities;
use Illuminate\Contracts\Support\{Arrayable, Jsonable};
use Illuminate\Support\Carbon;
use JsonSerializable;
use SplFileInfo;
/**
* Class Log
*
* @author ARCANEDEV <[email protected]>
*/
class Log implements Arrayable, Jsonable, JsonSerializable
{
/* -----------------------------------------------------------------
| Properties
| -----------------------------------------------------------------
*/
/** @var string */
public $date;
/** @var string */
private $path;
/** @var \Arcanedev\LogViewer\Entities\LogEntryCollection */
private $entries;
/** @var \SplFileInfo */
private $file;
/* -----------------------------------------------------------------
| Constructor
| -----------------------------------------------------------------
*/
/**
* Log constructor.
*
* @param string $date
* @param string $path
* @param string $raw
*/
public function __construct($date, $path, $raw)
{
$this->date = $date;
$this->path = $path;
$this->file = new SplFileInfo($path);
$this->entries = LogEntryCollection::load($raw);
}
/* -----------------------------------------------------------------
| Getters & Setters
| -----------------------------------------------------------------
*/
/**
* Get log path.
*
* @return string
*/
public function getPath()
{
return $this->path;
}
/**
* Get file info.
*
* @return \SplFileInfo
*/
public function file()
{
return $this->file;
}
/**
* Get file size.
*
* @return string
*/
public function size()
{
return $this->formatSize($this->file->getSize());
}
/**
* Get file creation date.
*
* @return \Carbon\Carbon
*/
public function createdAt()
{
return Carbon::createFromTimestamp($this->file()->getATime());
}
/**
* Get file modification date.
*
* @return \Carbon\Carbon
*/
public function updatedAt()
{
return Carbon::createFromTimestamp($this->file()->getMTime());
}
/* -----------------------------------------------------------------
| Main Methods
| -----------------------------------------------------------------
*/
/**
* Make a log object.
*
* @param string $date
* @param string $path
* @param string $raw
*
* @return self
*/
public static function make($date, $path, $raw)
{
return new self($date, $path, $raw);
}
/**
* Get log entries.
*
* @param string $level
*
* @return \Arcanedev\LogViewer\Entities\LogEntryCollection
*/
public function entries($level = 'all')
{
return $level === 'all'
? $this->entries
: $this->getByLevel($level);
}
/**
* Get filtered log entries by level.
*
* @param string $level
*
* @return \Arcanedev\LogViewer\Entities\LogEntryCollection
*/
public function getByLevel($level)
{
return $this->entries->filterByLevel($level);
}
/**
* Get log stats.
*
* @return array
*/
public function stats()
{
return $this->entries->stats();
}
/**
* Get the log navigation tree.
*
* @param bool $trans
*
* @return array
*/
public function tree($trans = false)
{
return $this->entries->tree($trans);
}
/**
* Get log entries menu.
*
* @param bool $trans
*
* @return array
*/
public function menu($trans = true)
{
return log_menu()->make($this, $trans);
}
/* -----------------------------------------------------------------
| Convert Methods
| -----------------------------------------------------------------
*/
/**
* Get the log as a plain array.
*
* @return array
*/
public function toArray()
{
return [
'date' => $this->date,
'path' => $this->path,
'entries' => $this->entries->toArray()
];
}
/**
* Convert the object to its JSON representation.
*
* @param int $options
*
* @return string
*/
public function toJson($options = 0)
{
return json_encode($this->toArray(), $options);
}
/**
* Serialize the log object to json data.
*
* @return array
*/
public function jsonSerialize(): array
{
return $this->toArray();
}
/* -----------------------------------------------------------------
| Other Methods
| -----------------------------------------------------------------
*/
/**
* Format the file size.
*
* @param int $bytes
* @param int $precision
*
* @return string
*/
private function formatSize($bytes, $precision = 2)
{
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
return round($bytes / pow(1024, $pow), $precision).' '.$units[$pow];
}
}
@@ -0,0 +1,220 @@
<?php
declare(strict_types=1);
namespace Arcanedev\LogViewer\Entities;
use Arcanedev\LogViewer\Contracts\Utilities\Filesystem as FilesystemContract;
use Arcanedev\LogViewer\Exceptions\LogNotFoundException;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\LazyCollection;
/**
* Class LogCollection
*
* @author ARCANEDEV <[email protected]>
*/
class LogCollection extends LazyCollection
{
/* -----------------------------------------------------------------
| Properties
| -----------------------------------------------------------------
*/
/** @var \Arcanedev\LogViewer\Contracts\Utilities\Filesystem */
private $filesystem;
/* -----------------------------------------------------------------
| Constructor
| -----------------------------------------------------------------
*/
/**
* LogCollection constructor.
*
* @param mixed $source
*/
public function __construct($source = null)
{
$this->setFilesystem(app(FilesystemContract::class));
if (is_null($source))
$source = function () {
foreach($this->filesystem->dates(true) as $date => $path) {
yield $date => Log::make($date, $path, $this->filesystem->read($date));
}
};
parent::__construct($source);
}
/* -----------------------------------------------------------------
| Getters & Setters
| -----------------------------------------------------------------
*/
/**
* Set the filesystem instance.
*
* @param \Arcanedev\LogViewer\Contracts\Utilities\Filesystem $filesystem
*
* @return \Arcanedev\LogViewer\Entities\LogCollection
*/
public function setFilesystem(FilesystemContract $filesystem)
{
$this->filesystem = $filesystem;
return $this;
}
/* -----------------------------------------------------------------
| Main Methods
| -----------------------------------------------------------------
*/
/**
* Get a log.
*
* @param string $date
* @param mixed|null $default
*
* @return \Arcanedev\LogViewer\Entities\Log
*
* @throws \Arcanedev\LogViewer\Exceptions\LogNotFoundException
*/
public function get($date, $default = null)
{
if ( ! $this->has($date))
throw LogNotFoundException::make($date);
return parent::get($date, $default);
}
/**
* Paginate logs.
*
* @param int $perPage
*
* @return \Illuminate\Pagination\LengthAwarePaginator
*/
public function paginate($perPage = 30)
{
$page = request()->get('page', 1);
$path = request()->url();
return new LengthAwarePaginator(
$this->forPage($page, $perPage),
$this->count(),
$perPage,
$page,
compact('path')
);
}
/**
* Get a log (alias).
*
* @see get()
*
* @param string $date
*
* @return \Arcanedev\LogViewer\Entities\Log
*/
public function log($date)
{
return $this->get($date);
}
/**
* Get log entries.
*
* @param string $date
* @param string $level
*
* @return \Arcanedev\LogViewer\Entities\LogEntryCollection
*/
public function entries($date, $level = 'all')
{
return $this->get($date)->entries($level);
}
/**
* Get logs statistics.
*
* @return array
*/
public function stats()
{
$stats = [];
foreach ($this->all() as $date => $log) {
/** @var \Arcanedev\LogViewer\Entities\Log $log */
$stats[$date] = $log->stats();
}
return $stats;
}
/**
* List the log files (dates).
*
* @return array
*/
public function dates()
{
return $this->keys()->toArray();
}
/**
* Get entries total.
*
* @param string $level
*
* @return int
*/
public function total($level = 'all')
{
return (int) $this->sum(function (Log $log) use ($level) {
return $log->entries($level)->count();
});
}
/**
* Get logs tree.
*
* @param bool $trans
*
* @return array
*/
public function tree($trans = false)
{
$tree = [];
foreach ($this->all() as $date => $log) {
/** @var \Arcanedev\LogViewer\Entities\Log $log */
$tree[$date] = $log->tree($trans);
}
return $tree;
}
/**
* Get logs menu.
*
* @param bool $trans
*
* @return array
*/
public function menu($trans = true)
{
$menu = [];
foreach ($this->all() as $date => $log) {
/** @var \Arcanedev\LogViewer\Entities\Log $log */
$menu[$date] = $log->menu($trans);
}
return $menu;
}
}
+328
View File
@@ -0,0 +1,328 @@
<?php
declare(strict_types=1);
namespace Arcanedev\LogViewer\Entities;
use Arcanedev\LogViewer\Helpers\LogParser;
use Carbon\Carbon;
use Illuminate\Contracts\Support\{Arrayable, Jsonable};
use JsonSerializable;
/**
* Class LogEntry
*
* @author ARCANEDEV <[email protected]>
*/
class LogEntry implements Arrayable, Jsonable, JsonSerializable
{
/* -----------------------------------------------------------------
| 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($level, $header, $stack = null)
{
$this->setLevel($level);
$this->setHeader($header);
$this->setStack($stack);
}
/* -----------------------------------------------------------------
| Getters & Setters
| -----------------------------------------------------------------
*/
/**
* Set the entry level.
*
* @param string $level
*
* @return self
*/
private function setLevel($level)
{
$this->level = $level;
return $this;
}
/**
* Set the entry header.
*
* @param string $header
*
* @return self
*/
private function setHeader($header)
{
$this->setDatetime($this->extractDatetime($header));
$header = $this->cleanHeader($header);
$this->header = trim($header);
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', $datetime);
return $this;
}
/**
* Set the entry stack.
*
* @param string $stack
*
* @return self
*/
private function setStack($stack)
{
$this->stack = $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.
*/
public function context(int $options = JSON_PRETTY_PRINT): string
{
return json_encode($this->context, $options);
}
/* -----------------------------------------------------------------
| 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.
*/
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);
}
/* -----------------------------------------------------------------
| Other Methods
| -----------------------------------------------------------------
*/
/**
* Clean the entry header.
*
* @param string $header
*
* @return string
*/
private function cleanHeader($header)
{
// REMOVE THE DATE
$header = preg_replace('/\['.LogParser::REGEX_DATETIME_PATTERN.'\][ ]/', '', $header);
// EXTRACT ENV
if (preg_match('/^[a-z]+.[A-Z]+:/', $header, $out)) {
$this->setEnv($out[0]);
$header = trim(str_replace($out[0], '', $header));
}
// EXTRACT CONTEXT (Regex from https://stackoverflow.com/a/21995025)
preg_match_all('/{(?:[^{}]|(?R))*}/x', $header, $out);
if (isset($out[0][0]) && ! is_null($context = json_decode($out[0][0], true))) {
$header = str_replace($out[0][0], '', $header);
$this->setContext($context);
}
return $header;
}
/**
* Extract datetime from the header.
*
* @param string $header
*
* @return string
*/
private function extractDatetime($header)
{
return preg_replace('/^\[('.LogParser::REGEX_DATETIME_PATTERN.')\].*/', '$1', $header);
}
}
@@ -0,0 +1,135 @@
<?php
declare(strict_types=1);
namespace Arcanedev\LogViewer\Entities;
use Arcanedev\LogViewer\Helpers\LogParser;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\LazyCollection;
/**
* Class LogEntryCollection
*
* @author ARCANEDEV <[email protected]>
*/
class LogEntryCollection extends LazyCollection
{
/* -----------------------------------------------------------------
| Main Methods
| -----------------------------------------------------------------
*/
/**
* Load raw log entries.
*
* @param string $raw
*
* @return self
*/
public static function load($raw)
{
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);
}
});
}
/**
* Paginate log entries.
*
* @param int $perPage
*
* @return \Illuminate\Pagination\LengthAwarePaginator
*/
public function paginate($perPage = 20)
{
$page = request()->get('page', 1);
$path = request()->url();
return new LengthAwarePaginator(
$this->forPage($page, $perPage),
$this->count(),
$perPage,
$page,
compact('path')
);
}
/**
* Get filtered log entries by level.
*
* @param string $level
*
* @return self
*/
public function filterByLevel($level)
{
return $this->filter(function(LogEntry $entry) use ($level) {
return $entry->isSameLevel($level);
});
}
/**
* Get log entries stats.
*
* @return array
*/
public function stats()
{
$counters = $this->initStats();
foreach ($this->groupBy('level') as $level => $entries) {
$counters[$level] = $count = count($entries);
$counters['all'] += $count;
}
return $counters;
}
/**
* Get the log entries navigation tree.
*
* @param bool|false $trans
*
* @return array
*/
public function tree($trans = false)
{
$tree = $this->stats();
array_walk($tree, function(&$count, $level) use ($trans) {
$count = [
'name' => $trans ? log_levels()->get($level) : $level,
'count' => $count,
];
});
return $tree;
}
/* -----------------------------------------------------------------
| Other Methods
| -----------------------------------------------------------------
*/
/**
* Init stats counters.
*
* @return array
*/
private function initStats()
{
$levels = array_merge_recursive(
['all'],
array_keys(log_viewer()->levels(true))
);
return array_map(function () {
return 0;
}, array_flip($levels));
}
}
@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Arcanedev\LogViewer\Exceptions;
/**
* Class FilesystemException
*
* @author ARCANEDEV <[email protected]>
*/
class FilesystemException extends LogViewerException
{
public static function cannotDeleteLog()
{
return new static('There was an error deleting the log.');
}
public static function invalidPath(string $path)
{
return new static("The log(s) could not be located at : $path");
}
}
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Arcanedev\LogViewer\Exceptions;
/**
* Class LogNotFoundException
*
* @author ARCANEDEV <[email protected]>
*/
class LogNotFoundException extends LogViewerException
{
/**
* Make the exception.
*
* @param string $date
*
* @return static
*/
public static function make(string $date)
{
return new static("Log not found in this date [{$date}]");
}
}
@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Arcanedev\LogViewer\Exceptions;
/**
* Class LogViewerException
*
* @author ARCANEDEV <[email protected]>
*/
class LogViewerException extends \Exception {}
+142
View File
@@ -0,0 +1,142 @@
<?php
declare(strict_types=1);
namespace Arcanedev\LogViewer\Helpers;
use Arcanedev\LogViewer\Utilities\LogLevels;
use Illuminate\Support\Str;
/**
* Class LogParser
*
* @author ARCANEDEV <[email protected]>
*/
class LogParser
{
/* -----------------------------------------------------------------
| Constants
| -----------------------------------------------------------------
*/
const REGEX_DATE_PATTERN = '\d{4}(-\d{2}){2}';
const REGEX_TIME_PATTERN = '\d{2}(:\d{2}){2}';
const REGEX_DATETIME_PATTERN = self::REGEX_DATE_PATTERN.' '.self::REGEX_TIME_PATTERN;
/* -----------------------------------------------------------------
| Properties
| -----------------------------------------------------------------
*/
/**
* Parsed data.
*
* @var array
*/
protected static $parsed = [];
/* -----------------------------------------------------------------
| Main Methods
| -----------------------------------------------------------------
*/
/**
* Parse file content.
*
* @param string $raw
*
* @return array
*/
public static function parse($raw)
{
static::$parsed = [];
list($headings, $data) = static::parseRawData($raw);
// @codeCoverageIgnoreStart
if ( ! is_array($headings)) {
return static::$parsed;
}
// @codeCoverageIgnoreEnd
foreach ($headings as $heading) {
for ($i = 0, $j = count($heading); $i < $j; $i++) {
static::populateEntries($heading, $data, $i);
}
};
unset($headings, $data);
return array_reverse(static::$parsed);
}
/* -----------------------------------------------------------------
| Other Methods
| -----------------------------------------------------------------
*/
/**
* Extract the date.
*
* @param string $string
*
* @return string
*/
public static function extractDate(string $string): string
{
return preg_replace('/.*('.self::REGEX_DATE_PATTERN.').*/', '$1', $string);
}
/**
* Parse raw data.
*
* @param string $raw
*
* @return array
*/
private static function parseRawData($raw)
{
$pattern = '/\['.self::REGEX_DATETIME_PATTERN.'\].*/';
preg_match_all($pattern, $raw, $headings);
$data = preg_split($pattern, $raw);
if ($data[0] < 1) {
$trash = array_shift($data);
unset($trash);
}
return [$headings, $data];
}
/**
* Populate entries.
*
* @param array $heading
* @param array $data
* @param int $key
*/
private static function populateEntries($heading, $data, $key)
{
foreach (LogLevels::all() as $level) {
if (static::hasLogLevel($heading[$key], $level)) {
static::$parsed[] = [
'level' => $level,
'header' => $heading[$key],
'stack' => $data[$key]
];
}
}
}
/**
* Check if header has a log level.
*
* @param string $heading
* @param string $level
*
* @return bool
*/
private static function hasLogLevel($heading, $level)
{
return Str::contains($heading, strtoupper(".{$level}:"));
}
}
@@ -0,0 +1,312 @@
<?php
declare(strict_types=1);
namespace Arcanedev\LogViewer\Http\Controllers;
use Arcanedev\LogViewer\Contracts\LogViewer as LogViewerContract;
use Arcanedev\LogViewer\Entities\{LogEntry, LogEntryCollection};
use Arcanedev\LogViewer\Exceptions\LogNotFoundException;
use Arcanedev\LogViewer\Tables\StatsTable;
use Illuminate\Http\Request;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Routing\Controller;
use Illuminate\Support\{Arr, Collection, Str};
/**
* Class LogViewerController
*
* @author ARCANEDEV <[email protected]>
*/
class LogViewerController extends Controller
{
/* -----------------------------------------------------------------
| Properties
| -----------------------------------------------------------------
*/
/**
* The log viewer instance
*
* @var \Arcanedev\LogViewer\Contracts\LogViewer
*/
protected $logViewer;
/** @var int */
protected $perPage = 30;
/** @var string */
protected $showRoute = 'log-viewer::logs.show';
/* -----------------------------------------------------------------
| Constructor
| -----------------------------------------------------------------
*/
/**
* LogViewerController constructor.
*
* @param \Arcanedev\LogViewer\Contracts\LogViewer $logViewer
*/
public function __construct(LogViewerContract $logViewer)
{
$this->logViewer = $logViewer;
$this->perPage = config('log-viewer.per-page', $this->perPage);
$this->showRoute = config('log-viewer.route.show', $this->showRoute);
}
/* -----------------------------------------------------------------
| Main Methods
| -----------------------------------------------------------------
*/
/**
* Show the dashboard.
*
* @return \Illuminate\View\View
*/
public function index()
{
$stats = $this->logViewer->statsTable();
$chartData = $this->prepareChartData($stats);
$percents = $this->calcPercentages($stats->footer(), $stats->header());
return $this->view('dashboard', compact('chartData', 'percents'));
}
/**
* List all logs.
*
* @param \Illuminate\Http\Request $request
*
* @return \Illuminate\View\View
*/
public function listLogs(Request $request)
{
$stats = $this->logViewer->statsTable();
$headers = $stats->header();
$rows = $this->paginate($stats->rows(), $request);
return $this->view('logs', compact('headers', 'rows'));
}
/**
* Show the log.
*
* @param \Illuminate\Http\Request $request
* @param string $date
*
* @return \Illuminate\View\View
*/
public function show(Request $request, $date)
{
$level = 'all';
$log = $this->getLogOrFail($date);
$query = $request->get('query');
$levels = $this->logViewer->levelsNames();
$entries = $log->entries($level)->paginate($this->perPage);
return $this->view('show', compact('level', 'log', 'query', 'levels', 'entries'));
}
/**
* Filter the log entries by level.
*
* @param \Illuminate\Http\Request $request
* @param string $date
* @param string $level
*
* @return \Illuminate\View\View|\Illuminate\Http\RedirectResponse
*/
public function showByLevel(Request $request, $date, $level)
{
if ($level === 'all')
return redirect()->route($this->showRoute, [$date]);
$log = $this->getLogOrFail($date);
$query = $request->get('query');
$levels = $this->logViewer->levelsNames();
$entries = $this->logViewer->entries($date, $level)->paginate($this->perPage);
return $this->view('show', compact('level', 'log', 'query', 'levels', 'entries'));
}
/**
* Show the log with the search query.
*
* @param \Illuminate\Http\Request $request
* @param string $date
* @param string $level
*
* @return \Illuminate\View\View|\Illuminate\Http\RedirectResponse
*/
public function search(Request $request, $date, $level = 'all')
{
$query = $request->get('query');
if (is_null($query))
return redirect()->route($this->showRoute, [$date]);
$log = $this->getLogOrFail($date);
$levels = $this->logViewer->levelsNames();
$needles = array_map(function ($needle) {
return Str::lower($needle);
}, array_filter(explode(' ', $query)));
$entries = $log->entries($level)
->unless(empty($needles), function (LogEntryCollection $entries) use ($needles) {
return $entries->filter(function (LogEntry $entry) use ($needles) {
foreach ([$entry->header, $entry->stack, $entry->context()] as $subject) {
if (Str::containsAll(Str::lower($subject), $needles))
return true;
}
return false;
});
})
->paginate($this->perPage);
return $this->view('show', compact('level', 'log', 'query', 'levels', 'entries'));
}
/**
* Download the log
*
* @param string $date
*
* @return \Symfony\Component\HttpFoundation\BinaryFileResponse
*/
public function download($date)
{
return $this->logViewer->download($date);
}
/**
* Delete a log.
*
* @param \Illuminate\Http\Request $request
*
* @return \Illuminate\Http\JsonResponse
*/
public function delete(Request $request)
{
abort_unless($request->ajax(), 405, 'Method Not Allowed');
$date = $request->input('date');
return response()->json([
'result' => $this->logViewer->delete($date) ? 'success' : 'error'
]);
}
/* -----------------------------------------------------------------
| Other Methods
| -----------------------------------------------------------------
*/
/**
* Get the evaluated view contents for the given view.
*
* @param string $view
* @param array $data
* @param array $mergeData
*
* @return \Illuminate\View\View
*/
protected function view($view, $data = [], $mergeData = [])
{
$theme = config('log-viewer.theme');
return view()->make("log-viewer::{$theme}.{$view}", $data, $mergeData);
}
/**
* Paginate logs.
*
* @param array $data
* @param \Illuminate\Http\Request $request
*
* @return \Illuminate\Pagination\LengthAwarePaginator
*/
protected function paginate(array $data, Request $request)
{
$data = new Collection($data);
$page = $request->get('page', 1);
$path = $request->url();
return new LengthAwarePaginator(
$data->forPage($page, $this->perPage),
$data->count(),
$this->perPage,
$page,
compact('path')
);
}
/**
* Get a log or fail
*
* @param string $date
*
* @return \Arcanedev\LogViewer\Entities\Log|null
*/
protected function getLogOrFail($date)
{
$log = null;
try {
$log = $this->logViewer->get($date);
}
catch (LogNotFoundException $e) {
abort(404, $e->getMessage());
}
return $log;
}
/**
* Prepare chart data.
*
* @param \Arcanedev\LogViewer\Tables\StatsTable $stats
*
* @return string
*/
protected function prepareChartData(StatsTable $stats)
{
$totals = $stats->totals()->all();
return json_encode([
'labels' => Arr::pluck($totals, 'label'),
'datasets' => [
[
'data' => Arr::pluck($totals, 'value'),
'backgroundColor' => Arr::pluck($totals, 'color'),
'hoverBackgroundColor' => Arr::pluck($totals, 'highlight'),
],
],
]);
}
/**
* Calculate the percentage.
*
* @param array $total
* @param array $names
*
* @return array
*/
protected function calcPercentages(array $total, array $names)
{
$percents = [];
$all = Arr::get($total, 'all');
foreach ($total as $level => $count) {
$percents[$level] = [
'name' => $names[$level],
'count' => $count,
'percent' => $all ? round(($count / $all) * 100, 2) : 0,
];
}
return $percents;
}
}
@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace Arcanedev\LogViewer\Http\Routes;
use Arcanedev\LogViewer\Http\Controllers\LogViewerController;
use Arcanedev\Support\Routing\RouteRegistrar;
/**
* Class LogViewerRoute
*
* @author ARCANEDEV <[email protected]>
*/
class LogViewerRoute extends RouteRegistrar
{
/* -----------------------------------------------------------------
| Main Methods
| -----------------------------------------------------------------
*/
/**
* Map all routes.
*/
public function map(): void
{
$attributes = (array) config('log-viewer.route.attributes');
$this->group($attributes, function() {
$this->name('log-viewer::')->group(function () {
$this->get('/', [LogViewerController::class, 'index'])
->name('dashboard'); // log-viewer::dashboard
$this->mapLogsRoutes();
});
});
}
/**
* Map the logs routes.
*/
private function mapLogsRoutes(): void
{
$this->prefix('logs')->name('logs.')->group(function() {
$this->get('/', [LogViewerController::class, 'listLogs'])
->name('list'); // log-viewer::logs.list
$this->delete('delete', [LogViewerController::class, 'delete'])
->name('delete'); // log-viewer::logs.delete
$this->prefix('{date}')->group(function() {
$this->get('/', [LogViewerController::class, 'show'])
->name('show'); // log-viewer::logs.show
$this->get('download', [LogViewerController::class, 'download'])
->name('download'); // log-viewer::logs.download
$this->get('{level}', [LogViewerController::class, 'showByLevel'])
->name('filter'); // log-viewer::logs.filter
$this->get('{level}/search', [LogViewerController::class, 'search'])
->name('search'); // log-viewer::logs.search
});
});
}
}
+364
View File
@@ -0,0 +1,364 @@
<?php
declare(strict_types=1);
namespace Arcanedev\LogViewer;
use Arcanedev\LogViewer\Contracts\Utilities\Filesystem as FilesystemContract;
use Arcanedev\LogViewer\Contracts\Utilities\Factory as FactoryContract;
use Arcanedev\LogViewer\Contracts\Utilities\LogLevels as LogLevelsContract;
use Arcanedev\LogViewer\Contracts\LogViewer as LogViewerContract;
/**
* Class LogViewer
*
* @author ARCANEDEV <[email protected]>
*/
class LogViewer implements LogViewerContract
{
/* -----------------------------------------------------------------
| Constants
| -----------------------------------------------------------------
*/
/**
* LogViewer Version
*/
const VERSION = '10.1.1';
/* -----------------------------------------------------------------
| Properties
| -----------------------------------------------------------------
*/
/**
* The factory instance.
*
* @var \Arcanedev\LogViewer\Contracts\Utilities\Factory
*/
protected $factory;
/**
* The filesystem instance.
*
* @var \Arcanedev\LogViewer\Contracts\Utilities\Filesystem
*/
protected $filesystem;
/**
* The log levels instance.
*
* @var \Arcanedev\LogViewer\Contracts\Utilities\LogLevels
*/
protected $levels;
/* -----------------------------------------------------------------
| Constructor
| -----------------------------------------------------------------
*/
/**
* Create a new instance.
*
* @param \Arcanedev\LogViewer\Contracts\Utilities\Factory $factory
* @param \Arcanedev\LogViewer\Contracts\Utilities\Filesystem $filesystem
* @param \Arcanedev\LogViewer\Contracts\Utilities\LogLevels $levels
*/
public function __construct(
FactoryContract $factory,
FilesystemContract $filesystem,
LogLevelsContract $levels
) {
$this->factory = $factory;
$this->filesystem = $filesystem;
$this->levels = $levels;
}
/* -----------------------------------------------------------------
| Getters & Setters
| -----------------------------------------------------------------
*/
/**
* Get the log levels.
*
* @param bool $flip
*
* @return array
*/
public function levels($flip = false)
{
return $this->levels->lists($flip);
}
/**
* Get the translated log levels.
*
* @param string|null $locale
*
* @return array
*/
public function levelsNames($locale = null)
{
return $this->levels->names($locale);
}
/**
* Set the log storage path.
*
* @param string $path
*
* @return self
*/
public function setPath($path)
{
$this->factory->setPath($path);
return $this;
}
/**
* Get the log pattern.
*
* @return string
*/
public function getPattern()
{
return $this->factory->getPattern();
}
/**
* Set the log pattern.
*
* @param string $date
* @param string $prefix
* @param string $extension
*
* @return self
*/
public function setPattern(
$prefix = FilesystemContract::PATTERN_PREFIX,
$date = FilesystemContract::PATTERN_DATE,
$extension = FilesystemContract::PATTERN_EXTENSION
) {
$this->factory->setPattern($prefix, $date, $extension);
return $this;
}
/* -----------------------------------------------------------------
| Main Methods
| -----------------------------------------------------------------
*/
/**
* Get all logs.
*
* @return \Arcanedev\LogViewer\Entities\LogCollection
*/
public function all()
{
return $this->factory->all();
}
/**
* Paginate all logs.
*
* @param int $perPage
*
* @return \Illuminate\Pagination\LengthAwarePaginator
*/
public function paginate($perPage = 30)
{
return $this->factory->paginate($perPage);
}
/**
* Get a log.
*
* @param string $date
*
* @return \Arcanedev\LogViewer\Entities\Log
*/
public function get($date)
{
return $this->factory->log($date);
}
/**
* Get the log entries.
*
* @param string $date
* @param string $level
*
* @return \Arcanedev\LogViewer\Entities\LogEntryCollection
*/
public function entries($date, $level = 'all')
{
return $this->factory->entries($date, $level);
}
/**
* Download a log file.
*
* @param string $date
* @param string|null $filename
* @param array $headers
*
* @return \Symfony\Component\HttpFoundation\BinaryFileResponse
*/
public function download($date, $filename = null, $headers = [])
{
if (is_null($filename)) {
$filename = sprintf(
"%s{$date}.%s",
config('log-viewer.download.prefix', 'laravel-'),
config('log-viewer.download.extension', 'log')
);
}
$path = $this->filesystem->path($date);
return response()->download($path, $filename, $headers);
}
/**
* Get logs statistics.
*
* @return array
*/
public function stats()
{
return $this->factory->stats();
}
/**
* Get logs statistics table.
*
* @param string|null $locale
*
* @return \Arcanedev\LogViewer\Tables\StatsTable
*/
public function statsTable($locale = null)
{
return $this->factory->statsTable($locale);
}
/**
* Delete the log.
*
* @param string $date
*
* @return bool
*/
public function delete($date)
{
return $this->filesystem->delete($date);
}
/**
* Clear the log files.
*
* @return bool
*/
public function clear()
{
return $this->filesystem->clear();
}
/**
* Get all valid log files.
*
* @return array
*/
public function files()
{
return $this->filesystem->logs();
}
/**
* List the log files (only dates).
*
* @return array
*/
public function dates()
{
return $this->factory->dates();
}
/**
* Get logs count.
*
* @return int
*/
public function count()
{
return $this->factory->count();
}
/**
* Get entries total from all logs.
*
* @param string $level
*
* @return int
*/
public function total($level = 'all')
{
return $this->factory->total($level);
}
/**
* Get logs tree.
*
* @param bool $trans
*
* @return array
*/
public function tree($trans = false)
{
return $this->factory->tree($trans);
}
/**
* Get logs menu.
*
* @param bool $trans
*
* @return array
*/
public function menu($trans = true)
{
return $this->factory->menu($trans);
}
/* -----------------------------------------------------------------
| Check Methods
| -----------------------------------------------------------------
*/
/**
* Determine if the log folder is empty or not.
*
* @return bool
*/
public function isEmpty()
{
return $this->factory->isEmpty();
}
/* -----------------------------------------------------------------
| Other Methods
| -----------------------------------------------------------------
*/
/**
* Get the LogViewer version.
*
* @return string
*/
public function version()
{
return self::VERSION;
}
}
@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace Arcanedev\LogViewer;
use Arcanedev\Support\Providers\PackageServiceProvider;
/**
* Class LogViewerServiceProvider
*
* @author ARCANEDEV <[email protected]>
*/
class LogViewerServiceProvider extends PackageServiceProvider
{
/* -----------------------------------------------------------------
| Properties
| -----------------------------------------------------------------
*/
/**
* Package name.
*
* @var string
*/
protected $package = 'log-viewer';
/* -----------------------------------------------------------------
| Main Methods
| -----------------------------------------------------------------
*/
/**
* Register the service provider.
*/
public function register(): void
{
parent::register();
$this->registerConfig();
$this->registerProvider(Providers\RouteServiceProvider::class);
$this->registerCommands([
Commands\PublishCommand::class,
Commands\StatsCommand::class,
Commands\CheckCommand::class,
Commands\ClearCommand::class,
]);
}
/**
* Boot the service provider.
*/
public function boot(): void
{
$this->loadTranslations();
$this->loadViews();
if ($this->app->runningInConsole()) {
$this->publishConfig();
$this->publishTranslations();
$this->publishViews();
}
}
}
@@ -0,0 +1,138 @@
<?php
declare(strict_types=1);
namespace Arcanedev\LogViewer\Providers;
use Arcanedev\LogViewer\Contracts\LogViewer as LogViewerContract;
use Arcanedev\LogViewer\Contracts\Utilities\Factory as FactoryContract;
use Arcanedev\LogViewer\Contracts\Utilities\Filesystem as FilesystemContract;
use Arcanedev\LogViewer\Contracts\Utilities\LogChecker as LogCheckerContract;
use Arcanedev\LogViewer\Contracts\Utilities\LogLevels as LogLevelsContract;
use Arcanedev\LogViewer\Contracts\Utilities\LogMenu as LogMenuContract;
use Arcanedev\LogViewer\Contracts\Utilities\LogStyler as LogStylerContract;
use Arcanedev\LogViewer\LogViewer;
use Arcanedev\LogViewer\Utilities;
use Arcanedev\Support\Providers\ServiceProvider;
use Illuminate\Contracts\Support\DeferrableProvider;
/**
* Class DeferredServicesProvider
*
* @author ARCANEDEV <[email protected]>
*/
class DeferredServicesProvider extends ServiceProvider implements DeferrableProvider
{
/* -----------------------------------------------------------------
| Main Methods
| -----------------------------------------------------------------
*/
/**
* Register the service provider.
*/
public function register(): void
{
$this->registerLogViewer();
$this->registerLogLevels();
$this->registerStyler();
$this->registerLogMenu();
$this->registerFilesystem();
$this->registerFactory();
$this->registerChecker();
}
/**
* Get the services provided by the provider.
*
* @return array
*/
public function provides(): array
{
return [
LogViewerContract::class,
LogLevelsContract::class,
LogStylerContract::class,
LogMenuContract::class,
FilesystemContract::class,
FactoryContract::class,
LogCheckerContract::class,
];
}
/* -----------------------------------------------------------------
| LogViewer Utilities
| -----------------------------------------------------------------
*/
/**
* Register the log viewer service.
*/
private function registerLogViewer(): void
{
$this->singleton(LogViewerContract::class, LogViewer::class);
}
/**
* Register the log levels.
*/
private function registerLogLevels(): void
{
$this->singleton(LogLevelsContract::class, function ($app) {
return new Utilities\LogLevels(
$app['translator'],
$app['config']->get('log-viewer.locale')
);
});
}
/**
* Register the log styler.
*/
private function registerStyler(): void
{
$this->singleton(LogStylerContract::class, Utilities\LogStyler::class);
}
/**
* Register the log menu builder.
*/
private function registerLogMenu(): void
{
$this->singleton(LogMenuContract::class, Utilities\LogMenu::class);
}
/**
* Register the log filesystem.
*/
private function registerFilesystem(): void
{
$this->singleton(FilesystemContract::class, function ($app) {
/** @var \Illuminate\Config\Repository $config */
$config = $app['config'];
$filesystem = new Utilities\Filesystem($app['files'], $config->get('log-viewer.storage-path'));
return $filesystem->setPattern(
$config->get('log-viewer.pattern.prefix', FilesystemContract::PATTERN_PREFIX),
$config->get('log-viewer.pattern.date', FilesystemContract::PATTERN_DATE),
$config->get('log-viewer.pattern.extension', FilesystemContract::PATTERN_EXTENSION)
);
});
}
/**
* Register the log factory class.
*/
private function registerFactory(): void
{
$this->singleton(FactoryContract::class, Utilities\Factory::class);
}
/**
* Register the log checker service.
*/
private function registerChecker(): void
{
$this->singleton(LogCheckerContract::class, Utilities\LogChecker::class);
}
}
@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace Arcanedev\LogViewer\Providers;
use Arcanedev\LogViewer\Http\Routes\LogViewerRoute;
use Arcanedev\Support\Providers\RouteServiceProvider as ServiceProvider;
/**
* Class RouteServiceProvider
*
* @author ARCANEDEV <[email protected]>
*/
class RouteServiceProvider extends ServiceProvider
{
/* -----------------------------------------------------------------
| Getters & Setters
| -----------------------------------------------------------------
*/
/**
* Check if routes is enabled
*
* @return bool
*/
public function isEnabled(): bool
{
return (bool) $this->config('enabled', false);
}
/* -----------------------------------------------------------------
| Main Methods
| -----------------------------------------------------------------
*/
/**
* Boot the service provider.
*/
public function boot(): void
{
if ($this->isEnabled()) {
$this->routes(function () {
static::mapRouteClasses([LogViewerRoute::class]);
});
}
}
/* -----------------------------------------------------------------
| Other Methods
| -----------------------------------------------------------------
*/
/**
* Get config value by key
*
* @param string $key
* @param mixed|null $default
*
* @return mixed
*/
private function config($key, $default = null)
{
return $this->app['config']->get("log-viewer.route.$key", $default);
}
}
+209
View File
@@ -0,0 +1,209 @@
<?php
declare(strict_types=1);
namespace Arcanedev\LogViewer\Tables;
use Arcanedev\LogViewer\Contracts\Table as TableContract;
use Arcanedev\LogViewer\Contracts\Utilities\LogLevels as LogLevelsContract;
/**
* Class AbstractTable
*
* @author ARCANEDEV <[email protected]>
*/
abstract class AbstractTable implements TableContract
{
/* -----------------------------------------------------------------
| Properties
| -----------------------------------------------------------------
*/
/** @var array */
private $header = [];
/** @var array */
private $rows = [];
/** @var array */
private $footer = [];
/** @var \Arcanedev\LogViewer\Contracts\Utilities\LogLevels */
protected $levels;
/** @var string|null */
protected $locale;
/** @var array */
private $data = [];
/* -----------------------------------------------------------------
| Constructor
| -----------------------------------------------------------------
*/
/**
* Create a table instance.
*
* @param array $data
* @param \Arcanedev\LogViewer\Contracts\Utilities\LogLevels $levels
* @param string|null $locale
*/
public function __construct(array $data, LogLevelsContract $levels, $locale = null)
{
$this->setLevels($levels);
$this->setLocale(is_null($locale) ? config('log-viewer.locale') : $locale);
$this->setData($data);
$this->init();
}
/* -----------------------------------------------------------------
| Getters & Setters
| -----------------------------------------------------------------
*/
/**
* Set LogLevels instance.
*
* @param \Arcanedev\LogViewer\Contracts\Utilities\LogLevels $levels
*
* @return $this
*/
protected function setLevels(LogLevelsContract $levels)
{
$this->levels = $levels;
return $this;
}
/**
* Set table locale.
*
* @param string|null $locale
*
* @return $this
*/
protected function setLocale($locale)
{
if (is_null($locale) || $locale === 'auto') {
$locale = app()->getLocale();
}
$this->locale = $locale;
return $this;
}
/**
* Get table header.
*
* @return array
*/
public function header()
{
return $this->header;
}
/**
* Get table rows.
*
* @return array
*/
public function rows()
{
return $this->rows;
}
/**
* Get table footer.
*
* @return array
*/
public function footer()
{
return $this->footer;
}
/**
* Get raw data.
*
* @return array
*/
public function data()
{
return $this->data;
}
/**
* Set table data.
*
* @param array $data
*
* @return $this
*/
private function setData(array $data)
{
$this->data = $data;
return $this;
}
/* -----------------------------------------------------------------
| Main Methods
| -----------------------------------------------------------------
*/
/**
* Prepare the table.
*/
private function init()
{
$this->header = $this->prepareHeader($this->data);
$this->rows = $this->prepareRows($this->data);
$this->footer = $this->prepareFooter($this->data);
}
/**
* Prepare table header.
*
* @param array $data
*
* @return array
*/
abstract protected function prepareHeader(array $data);
/**
* Prepare table rows.
*
* @param array $data
*
* @return array
*/
abstract protected function prepareRows(array $data);
/**
* Prepare table footer.
*
* @param array $data
*
* @return array
*/
abstract protected function prepareFooter(array $data);
/* -----------------------------------------------------------------
| Other Methods
| -----------------------------------------------------------------
*/
/**
* Get log level color.
*
* @param string $level
*
* @return string
*/
protected function color($level)
{
return log_styler()->color($level);
}
}
+135
View File
@@ -0,0 +1,135 @@
<?php
declare(strict_types=1);
namespace Arcanedev\LogViewer\Tables;
use Arcanedev\LogViewer\Contracts\Utilities\LogLevels as LogLevelsContract;
use Illuminate\Support\{Arr, Collection};
/**
* Class StatsTable
*
* @author ARCANEDEV <[email protected]>
*/
class StatsTable extends AbstractTable
{
/* -----------------------------------------------------------------
| Main Methods
| -----------------------------------------------------------------
*/
/**
* Make a stats table instance.
*
* @param array $data
* @param \Arcanedev\LogViewer\Contracts\Utilities\LogLevels $levels
* @param string|null $locale
*
* @return $this
*/
public static function make(array $data, LogLevelsContract $levels, $locale = null)
{
return new static($data, $levels, $locale);
}
/* -----------------------------------------------------------------
| Other Methods
| -----------------------------------------------------------------
*/
/**
* Prepare table header.
*
* @param array $data
*
* @return array
*/
protected function prepareHeader(array $data)
{
return array_merge_recursive(
[
'date' => __('Date'),
'all' => __('All'),
],
$this->levels->names($this->locale)
);
}
/**
* Prepare table rows.
*
* @param array $data
*
* @return array
*/
protected function prepareRows(array $data)
{
$rows = [];
foreach ($data as $date => $levels) {
$rows[$date] = array_merge(compact('date'), $levels);
}
return $rows;
}
/**
* Prepare table footer.
*
* @param array $data
*
* @return array
*/
protected function prepareFooter(array $data)
{
$footer = [];
foreach ($data as $date => $levels) {
foreach ($levels as $level => $count) {
if ( ! isset($footer[$level])) {
$footer[$level] = 0;
}
$footer[$level] += $count;
}
}
return $footer;
}
/**
* Get totals.
*
* @param string|null $locale
*
* @return \Illuminate\Support\Collection
*/
public function totals($locale = null)
{
$totals = Collection::make();
foreach (Arr::except($this->footer(), 'all') as $level => $count) {
$totals->put($level, [
'label' => log_levels()->get($level, $locale),
'value' => $count,
'color' => $this->color($level),
'highlight' => $this->color($level),
]);
}
return $totals;
}
/**
* Get json totals data.
*
* @param string|null $locale
*
* @return string
*/
public function totalsJson($locale = null)
{
return $this->totals($locale)->toJson(JSON_PRETTY_PRINT);
}
}
+326
View File
@@ -0,0 +1,326 @@
<?php
declare(strict_types=1);
namespace Arcanedev\LogViewer\Utilities;
use Arcanedev\LogViewer\Contracts\Utilities\Factory as FactoryContract;
use Arcanedev\LogViewer\Contracts\Utilities\Filesystem as FilesystemContract;
use Arcanedev\LogViewer\Contracts\Utilities\LogLevels as LogLevelsContract;
use Arcanedev\LogViewer\Entities\LogCollection;
use Arcanedev\LogViewer\Entities\Log;
use Arcanedev\LogViewer\Exceptions\LogNotFoundException;
use Arcanedev\LogViewer\Tables\StatsTable;
/**
* Class Factory
*
* @author ARCANEDEV <[email protected]>
*/
class Factory implements FactoryContract
{
/* -----------------------------------------------------------------
| Properties
| -----------------------------------------------------------------
*/
/**
* The filesystem instance.
*
* @var \Arcanedev\LogViewer\Contracts\Utilities\Filesystem
*/
protected $filesystem;
/**
* The log levels instance.
*
* @var \Arcanedev\LogViewer\Contracts\Utilities\LogLevels
*/
private $levels;
/* -----------------------------------------------------------------
| Constructor
| -----------------------------------------------------------------
*/
/**
* Create a new instance.
*
* @param \Arcanedev\LogViewer\Contracts\Utilities\Filesystem $filesystem
* @param \Arcanedev\LogViewer\Contracts\Utilities\LogLevels $levels
*/
public function __construct(FilesystemContract $filesystem, LogLevelsContract $levels) {
$this->setFilesystem($filesystem);
$this->setLevels($levels);
}
/* -----------------------------------------------------------------
| Getter & Setters
| -----------------------------------------------------------------
*/
/**
* Get the filesystem instance.
*
* @return \Arcanedev\LogViewer\Contracts\Utilities\Filesystem
*/
public function getFilesystem()
{
return $this->filesystem;
}
/**
* Set the filesystem instance.
*
* @param \Arcanedev\LogViewer\Contracts\Utilities\Filesystem $filesystem
*
* @return self
*/
public function setFilesystem(FilesystemContract $filesystem)
{
$this->filesystem = $filesystem;
return $this;
}
/**
* Get the log levels instance.
*
* @return \Arcanedev\LogViewer\Contracts\Utilities\LogLevels
*/
public function getLevels()
{
return $this->levels;
}
/**
* Set the log levels instance.
*
* @param \Arcanedev\LogViewer\Contracts\Utilities\LogLevels $levels
*
* @return self
*/
public function setLevels(LogLevelsContract $levels)
{
$this->levels = $levels;
return $this;
}
/**
* Set the log storage path.
*
* @param string $storagePath
*
* @return self
*/
public function setPath($storagePath)
{
$this->filesystem->setPath($storagePath);
return $this;
}
/**
* Get the log pattern.
*
* @return string
*/
public function getPattern()
{
return $this->filesystem->getPattern();
}
/**
* Set the log pattern.
*
* @param string $date
* @param string $prefix
* @param string $extension
*
* @return self
*/
public function setPattern(
$prefix = FilesystemContract::PATTERN_PREFIX,
$date = FilesystemContract::PATTERN_DATE,
$extension = FilesystemContract::PATTERN_EXTENSION
) {
$this->filesystem->setPattern($prefix, $date, $extension);
return $this;
}
/**
* Get all logs.
*
* @return \Arcanedev\LogViewer\Entities\LogCollection
*/
public function logs()
{
return (new LogCollection)->setFilesystem($this->filesystem);
}
/* -----------------------------------------------------------------
| Main Methods
| -----------------------------------------------------------------
*/
/**
* Get all logs (alias).
*
* @see logs
*
* @return \Arcanedev\LogViewer\Entities\LogCollection
*/
public function all()
{
return $this->logs();
}
/**
* Paginate all logs.
*
* @param int $perPage
*
* @return \Illuminate\Pagination\LengthAwarePaginator
*/
public function paginate($perPage = 30)
{
return $this->logs()->paginate($perPage);
}
/**
* Get a log by date.
*
* @param string $date
*
* @return \Arcanedev\LogViewer\Entities\Log
*/
public function log($date)
{
$dates = $this->filesystem->dates(true);
if (!isset($dates[$date])) {
throw new LogNotFoundException("Log not found in this date [$date]");
}
return new Log($date, $dates[$date], $this->filesystem->read($date));
}
/**
* Get a log by date (alias).
*
* @param string $date
*
* @return \Arcanedev\LogViewer\Entities\Log
*/
public function get($date)
{
return $this->log($date);
}
/**
* Get log entries.
*
* @param string $date
* @param string $level
*
* @return \Arcanedev\LogViewer\Entities\LogEntryCollection
*/
public function entries($date, $level = 'all')
{
return $this->log($date)->entries($level);
}
/**
* Get logs statistics.
*
* @return array
*/
public function stats()
{
return $this->logs()->stats();
}
/**
* Get logs statistics table.
*
* @param string|null $locale
*
* @return \Arcanedev\LogViewer\Tables\StatsTable
*/
public function statsTable($locale = null)
{
return StatsTable::make($this->stats(), $this->levels, $locale);
}
/**
* List the log files (dates).
*
* @return array
*/
public function dates()
{
return $this->filesystem->dates();
}
/**
* Get logs count.
*
* @return int
*/
public function count()
{
return $this->logs()->count();
}
/**
* Get total log entries.
*
* @param string $level
*
* @return int
*/
public function total($level = 'all')
{
return $this->logs()->total($level);
}
/**
* Get tree menu.
*
* @param bool $trans
*
* @return array
*/
public function tree($trans = false)
{
return $this->logs()->tree($trans);
}
/**
* Get tree menu.
*
* @param bool $trans
*
* @return array
*/
public function menu($trans = true)
{
return $this->logs()->menu($trans);
}
/* -----------------------------------------------------------------
| Check Methods
| -----------------------------------------------------------------
*/
/**
* Determine if the log folder is empty or not.
*
* @return bool
*/
public function isEmpty()
{
return $this->logs()->isEmpty();
}
}
+341
View File
@@ -0,0 +1,341 @@
<?php
declare(strict_types=1);
namespace Arcanedev\LogViewer\Utilities;
use Arcanedev\LogViewer\Contracts\Utilities\Filesystem as FilesystemContract;
use Arcanedev\LogViewer\Exceptions\FilesystemException;
use Arcanedev\LogViewer\Helpers\LogParser;
use Exception;
use Illuminate\Filesystem\Filesystem as IlluminateFilesystem;
/**
* Class Filesystem
*
* @author ARCANEDEV <[email protected]>
*/
class Filesystem implements FilesystemContract
{
/* -----------------------------------------------------------------
| Properties
| -----------------------------------------------------------------
*/
/**
* The filesystem instance.
*
* @var \Illuminate\Filesystem\Filesystem
*/
protected $filesystem;
/**
* The base storage path.
*
* @var string
*/
protected $storagePath;
/**
* The log files prefix pattern.
*
* @var string
*/
protected $prefixPattern;
/**
* The log files date pattern.
*
* @var string
*/
protected $datePattern;
/**
* The log files extension.
*
* @var string
*/
protected $extension;
/* -----------------------------------------------------------------
| Constructor
| -----------------------------------------------------------------
*/
/**
* Filesystem constructor.
*
* @param \Illuminate\Filesystem\Filesystem $files
* @param string $storagePath
*/
public function __construct(IlluminateFilesystem $files, $storagePath)
{
$this->filesystem = $files;
$this->setPath($storagePath);
$this->setPattern();
}
/* -----------------------------------------------------------------
| Getters & Setters
| -----------------------------------------------------------------
*/
/**
* Get the files instance.
*
* @return \Illuminate\Filesystem\Filesystem
*/
public function getInstance()
{
return $this->filesystem;
}
/**
* Set the log storage path.
*
* @param string $storagePath
*
* @return $this
*/
public function setPath($storagePath)
{
$this->storagePath = $storagePath;
return $this;
}
/**
* Get the log pattern.
*
* @return string
*/
public function getPattern(): string
{
return $this->prefixPattern.$this->datePattern.$this->extension;
}
/**
* Set the log pattern.
*
* @param string $date
* @param string $prefix
* @param string $extension
*
* @return $this
*/
public function setPattern(
$prefix = self::PATTERN_PREFIX,
$date = self::PATTERN_DATE,
$extension = self::PATTERN_EXTENSION
) {
$this->setPrefixPattern($prefix);
$this->setDatePattern($date);
$this->setExtension($extension);
return $this;
}
/**
* Set the log date pattern.
*
* @param string $datePattern
*
* @return $this
*/
public function setDatePattern($datePattern)
{
$this->datePattern = $datePattern;
return $this;
}
/**
* Set the log prefix pattern.
*
* @param string $prefixPattern
*
* @return $this
*/
public function setPrefixPattern($prefixPattern)
{
$this->prefixPattern = $prefixPattern;
return $this;
}
/**
* Set the log extension.
*
* @param string $extension
*
* @return $this
*/
public function setExtension($extension)
{
$this->extension = $extension;
return $this;
}
/* -----------------------------------------------------------------
| Main Methods
| -----------------------------------------------------------------
*/
/**
* Get all log files.
*
* @return array
*/
public function all()
{
return $this->getFiles('*'.$this->extension);
}
/**
* Get all valid log files.
*
* @return array
*/
public function logs()
{
return $this->getFiles($this->getPattern());
}
/**
* List the log files (Only dates).
*
* @param bool $withPaths
*
* @return array
*/
public function dates($withPaths = false)
{
$files = array_reverse($this->logs());
$dates = $this->extractDates($files);
if ($withPaths) {
$dates = array_combine($dates, $files); // [date => file]
}
return $dates;
}
/**
* Read the log.
*
* @param string $date
*
* @return string
*
* @throws \Arcanedev\LogViewer\Exceptions\FilesystemException
*/
public function read($date)
{
try {
$log = $this->filesystem->get(
$this->getLogPath($date)
);
}
catch (Exception $e) {
throw new FilesystemException($e->getMessage());
}
return $log;
}
/**
* Delete the log.
*
* @param string $date
*
* @return bool
*
* @throws \Arcanedev\LogViewer\Exceptions\FilesystemException
*/
public function delete(string $date)
{
$path = $this->getLogPath($date);
throw_unless($this->filesystem->delete($path), FilesystemException::cannotDeleteLog());
return true;
}
/**
* Clear the log files.
*
* @return bool
*/
public function clear()
{
return $this->filesystem->delete($this->logs());
}
/**
* Get the log file path.
*
* @param string $date
*
* @return string
*/
public function path($date)
{
return $this->getLogPath($date);
}
/* -----------------------------------------------------------------
| Other Methods
| -----------------------------------------------------------------
*/
/**
* Get all files.
*
* @param string $pattern
*
* @return array
*/
private function getFiles($pattern)
{
$files = $this->filesystem->glob(
$this->storagePath.DIRECTORY_SEPARATOR.$pattern, defined('GLOB_BRACE') ? GLOB_BRACE : 0
);
return array_filter(array_map('realpath', $files));
}
/**
* Get the log file path.
*
* @param string $date
*
* @return string
*
* @throws \Arcanedev\LogViewer\Exceptions\FilesystemException
*/
private function getLogPath(string $date)
{
$path = $this->storagePath.DIRECTORY_SEPARATOR.$this->prefixPattern.$date.$this->extension;
if ( ! $this->filesystem->exists($path)) {
throw FilesystemException::invalidPath($path);
}
return realpath($path);
}
/**
* Extract dates from files.
*
* @param array $files
*
* @return array
*/
private function extractDates(array $files)
{
return array_map(function ($file) {
return LogParser::extractDate(basename($file));
}, $files);
}
}
+312
View File
@@ -0,0 +1,312 @@
<?php
declare(strict_types=1);
namespace Arcanedev\LogViewer\Utilities;
use Arcanedev\LogViewer\Contracts\Utilities\Filesystem as FilesystemContract;
use Arcanedev\LogViewer\Contracts\Utilities\LogChecker as LogCheckerContract;
use Illuminate\Contracts\Config\Repository as ConfigContract;
/**
* Class LogChecker
*
* @author ARCANEDEV <[email protected]>
*/
class LogChecker implements LogCheckerContract
{
/* -----------------------------------------------------------------
| Properties
| -----------------------------------------------------------------
*/
/**
* The config repository instance.
*
* @var \Illuminate\Contracts\Config\Repository
*/
private $config;
/**
* The filesystem instance.
*
* @var \Arcanedev\LogViewer\Contracts\Utilities\Filesystem
*/
private $filesystem;
/**
* Log handler mode.
*
* @var string
*/
protected $handler = '';
/**
* The check status.
*
* @var bool
*/
private $status = true;
/**
* The check messages.
*
* @var array
*/
private $messages;
/**
* Log files statuses.
*
* @var array
*/
private $files = [];
/* -----------------------------------------------------------------
| Constructor
| -----------------------------------------------------------------
*/
/**
* LogChecker constructor.
*
* @param \Illuminate\Contracts\Config\Repository $config
* @param \Arcanedev\LogViewer\Contracts\Utilities\Filesystem $filesystem
*/
public function __construct(ConfigContract $config, FilesystemContract $filesystem)
{
$this->setConfig($config);
$this->setFilesystem($filesystem);
$this->refresh();
}
/* -----------------------------------------------------------------
| Getters & Setters
| -----------------------------------------------------------------
*/
/**
* Set the config instance.
*
* @param \Illuminate\Contracts\Config\Repository $config
*
* @return self
*/
public function setConfig(ConfigContract $config)
{
$this->config = $config;
return $this;
}
/**
* Set the Filesystem instance.
*
* @param \Arcanedev\LogViewer\Contracts\Utilities\Filesystem $filesystem
*
* @return self
*/
public function setFilesystem(FilesystemContract $filesystem)
{
$this->filesystem = $filesystem;
return $this;
}
/**
* Set the log handler mode.
*
* @param string $handler
*
* @return self
*/
protected function setHandler($handler)
{
$this->handler = strtolower($handler);
return $this;
}
/* -----------------------------------------------------------------
| Main Methods
| -----------------------------------------------------------------
*/
/**
* Get messages.
*
* @return array
*/
public function messages()
{
$this->refresh();
return $this->messages;
}
/**
* Check if the checker passes.
*
* @return bool
*/
public function passes()
{
$this->refresh();
return $this->status;
}
/**
* Check if the checker fails.
*
* @return bool
*/
public function fails()
{
return ! $this->passes();
}
/**
* Get the requirements.
*
* @return array
*/
public function requirements()
{
$this->refresh();
return $this->isDaily() ? [
'status' => 'success',
'header' => 'Application requirements fulfilled.',
'message' => 'Are you ready to rock ?',
] : [
'status' => 'failed',
'header' => 'Application requirements failed.',
'message' => $this->messages['handler']
];
}
/* -----------------------------------------------------------------
| Check Methods
| -----------------------------------------------------------------
*/
/**
* Is a daily handler mode ?
*
* @return bool
*/
protected function isDaily()
{
return $this->isSameHandler(self::HANDLER_DAILY);
}
/**
* Is the handler is the same as the application log handler.
*
* @param string $handler
*
* @return bool
*/
private function isSameHandler($handler)
{
return $this->handler === $handler;
}
/* -----------------------------------------------------------------
| Other Methods
| -----------------------------------------------------------------
*/
/**
* Refresh the checks.
*
* @return \Arcanedev\LogViewer\Utilities\LogChecker
*/
private function refresh()
{
$this->setHandler($this->config->get('logging.default', 'stack'));
$this->messages = [
'handler' => '',
'files' => [],
];
$this->files = [];
$this->checkHandler();
$this->checkLogFiles();
return $this;
}
/**
* Check the handler mode.
*/
private function checkHandler()
{
if ($this->isDaily()) return;
$this->messages['handler'] = 'You should set the log handler to `daily` mode. Please check the LogViewer wiki page (Requirements) for more details.';
}
/**
* Check all log files.
*/
private function checkLogFiles()
{
foreach ($this->filesystem->all() as $path) {
$this->checkLogFile($path);
}
}
/**
* Check a log file.
*
* @param string $path
*/
private function checkLogFile($path)
{
$status = true;
$filename = basename($path);
$message = "The log file [$filename] is valid.";
$pattern = $this->filesystem->getPattern();
if ($this->isSingleLogFile($filename)) {
$this->status = $status = false;
$this->messages['files'][$filename] = $message =
"You have a single log file in your application, you should split the [$filename] into separate log files.";
}
elseif ($this->isInvalidLogPattern($filename, $pattern)) {
$this->status = $status = false;
$this->messages['files'][$filename] = $message =
"The log file [$filename] has an invalid date, the format must be like {$pattern}.";
}
$this->files[$filename] = compact('filename', 'status', 'message', 'path');
}
/**
* Check if it's not a single log file.
*
* @param string $file
*
* @return bool
*/
private function isSingleLogFile($file)
{
return $file === 'laravel.log';
}
/**
* Check the date of the log file.
*
* @param string $file
* @param string $pattern
*
* @return bool
*/
private function isInvalidLogPattern($file, $pattern)
{
return ((bool) preg_match("/{$pattern}/", $file, $matches)) === false;
}
}
+183
View File
@@ -0,0 +1,183 @@
<?php
declare(strict_types=1);
namespace Arcanedev\LogViewer\Utilities;
use Arcanedev\LogViewer\Contracts\Utilities\LogLevels as LogLevelsContract;
use Illuminate\Support\Arr;
use Illuminate\Translation\Translator;
use Psr\Log\LogLevel;
use ReflectionClass;
/**
* Class LogLevels
*
* @author ARCANEDEV <[email protected]>
*/
class LogLevels implements LogLevelsContract
{
/* -----------------------------------------------------------------
| Properties
| -----------------------------------------------------------------
*/
/**
* The log levels.
*
* @var array
*/
protected static $levels = [];
/**
* The Translator instance.
*
* @var \Illuminate\Translation\Translator
*/
private $translator;
/**
* The selected locale.
*
* @var string
*/
private $locale;
/* -----------------------------------------------------------------
| Constructor
| -----------------------------------------------------------------
*/
/**
* LogLevels constructor.
*
* @param \Illuminate\Translation\Translator $translator
* @param string $locale
*/
public function __construct(Translator $translator, $locale)
{
$this->setTranslator($translator);
$this->setLocale($locale);
}
/* -----------------------------------------------------------------
| Getters & Setters
| -----------------------------------------------------------------
*/
/**
* Set the Translator instance.
*
* @param \Illuminate\Translation\Translator $translator
*
* @return $this
*/
public function setTranslator(Translator $translator)
{
$this->translator = $translator;
return $this;
}
/**
* Get the selected locale.
*
* @return string
*/
public function getLocale()
{
return $this->locale === 'auto'
? $this->translator->getLocale()
: $this->locale;
}
/**
* Set the selected locale.
*
* @param string $locale
*
* @return $this
*/
public function setLocale($locale)
{
$this->locale = is_null($locale) ? 'auto' : $locale;
return $this;
}
/* -----------------------------------------------------------------
| Main Methods
| -----------------------------------------------------------------
*/
/**
* Get the log levels.
*
* @param bool $flip
*
* @return array
*/
public function lists($flip = false)
{
return static::all($flip);
}
/**
* Get translated levels.
*
* @param string|null $locale
*
* @return array
*/
public function names($locale = null)
{
$levels = static::all(true);
array_walk($levels, function (&$name, $level) use ($locale) {
$name = $this->get($level, $locale);
});
return $levels;
}
/**
* Get PSR log levels.
*
* @param bool $flip
*
* @return array
*/
public static function all($flip = false)
{
if (empty(static::$levels)) {
static::$levels = (new ReflectionClass(LogLevel::class))->getConstants();
}
return $flip ? array_flip(static::$levels) : static::$levels;
}
/**
* Get the translated level.
*
* @param string $key
* @param string|null $locale
*
* @return string
*/
public function get($key, $locale = null)
{
$translations = [
'all' => 'All',
LogLevel::EMERGENCY => 'Emergency',
LogLevel::ALERT => 'Alert',
LogLevel::CRITICAL => 'Critical',
LogLevel::ERROR => 'Error',
LogLevel::WARNING => 'Warning',
LogLevel::NOTICE => 'Notice',
LogLevel::INFO => 'Info',
LogLevel::DEBUG => 'Debug',
];
return $this->translator->get(Arr::get($translations, $key, $key), [], $locale ?: $this->getLocale());
}
}
+148
View File
@@ -0,0 +1,148 @@
<?php
declare(strict_types=1);
namespace Arcanedev\LogViewer\Utilities;
use Arcanedev\LogViewer\Contracts\Utilities\LogMenu as LogMenuContract;
use Arcanedev\LogViewer\Contracts\Utilities\LogStyler as LogStylerContract;
use Arcanedev\LogViewer\Entities\Log;
use Illuminate\Contracts\Config\Repository as ConfigContract;
/**
* Class LogMenu
*
* @author ARCANEDEV <[email protected]>
*/
class LogMenu implements LogMenuContract
{
/* -----------------------------------------------------------------
| Properties
| -----------------------------------------------------------------
*/
/**
* The config repository instance.
*
* @var \Illuminate\Contracts\Config\Repository
*/
protected $config;
/**
* The log styler instance.
*
* @var \Arcanedev\LogViewer\Contracts\Utilities\LogStyler
*/
private $styler;
/* -----------------------------------------------------------------
| Constructor
| -----------------------------------------------------------------
*/
/**
* LogMenu constructor.
*
* @param \Illuminate\Contracts\Config\Repository $config
* @param \Arcanedev\LogViewer\Contracts\Utilities\LogStyler $styler
*/
public function __construct(ConfigContract $config, LogStylerContract $styler)
{
$this->setConfig($config);
$this->setLogStyler($styler);
}
/* -----------------------------------------------------------------
| Getters & Setters
| -----------------------------------------------------------------
*/
/**
* Set the config instance.
*
* @param \Illuminate\Contracts\Config\Repository $config
*
* @return self
*/
public function setConfig(ConfigContract $config)
{
$this->config = $config;
return $this;
}
/**
* Set the log styler instance.
*
* @param \Arcanedev\LogViewer\Contracts\Utilities\LogStyler $styler
*
* @return self
*/
public function setLogStyler(LogStylerContract $styler)
{
$this->styler = $styler;
return $this;
}
/* -----------------------------------------------------------------
| Main Methods
| -----------------------------------------------------------------
*/
/**
* Make log menu.
*
* @param \Arcanedev\LogViewer\Entities\Log $log
* @param bool $trans
*
* @return array
*/
public function make(Log $log, $trans = true)
{
$items = [];
$route = $this->config('menu.filter-route');
foreach($log->tree($trans) as $level => $item) {
$items[$level] = array_merge($item, [
'url' => route($route, [$log->date, $level]),
'icon' => $this->isIconsEnabled() ? $this->styler->icon($level)->toHtml() : '',
]);
}
return $items;
}
/* -----------------------------------------------------------------
| Check Methods
| -----------------------------------------------------------------
*/
/**
* Check if the icons are enabled.
*
* @return bool
*/
private function isIconsEnabled()
{
return (bool) $this->config('menu.icons-enabled', false);
}
/* -----------------------------------------------------------------
| Other Methods
| -----------------------------------------------------------------
*/
/**
* Get config.
*
* @param string $key
* @param mixed $default
*
* @return mixed
*/
private function config($key, $default = null)
{
return $this->config->get("log-viewer.$key", $default);
}
}
+107
View File
@@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
namespace Arcanedev\LogViewer\Utilities;
use Arcanedev\LogViewer\Contracts\Utilities\LogStyler as LogStylerContract;
use Illuminate\Contracts\Config\Repository as ConfigContract;
use Illuminate\Support\HtmlString;
/**
* Class LogStyler
*
* @author ARCANEDEV <[email protected]>
*/
class LogStyler implements LogStylerContract
{
/* -----------------------------------------------------------------
| Properties
| -----------------------------------------------------------------
*/
/**
* The config repository instance.
*
* @var \Illuminate\Contracts\Config\Repository
*/
protected $config;
/* -----------------------------------------------------------------
| Constructor
| -----------------------------------------------------------------
*/
/**
* Create a new instance.
*
* @param \Illuminate\Contracts\Config\Repository $config
*/
public function __construct(ConfigContract $config)
{
$this->config = $config;
}
/* -----------------------------------------------------------------
| Getters & Setters
| -----------------------------------------------------------------
*/
/**
* Get config.
*
* @param string $key
* @param mixed $default
*
* @return mixed
*/
private function get($key, $default = null)
{
return $this->config->get("log-viewer.$key", $default);
}
/* -----------------------------------------------------------------
| Main Methods
| -----------------------------------------------------------------
*/
/**
* Make level icon.
*
* @param string $level
* @param string|null $default
*
* @return \Illuminate\Support\HtmlString
*/
public function icon($level, $default = null)
{
return new HtmlString(
'<i class="'.$this->get("icons.$level", $default).'"></i>'
);
}
/**
* Get level color.
*
* @param string $level
* @param string|null $default
*
* @return string
*/
public function color($level, $default = null)
{
return $this->get("colors.levels.$level", $default);
}
/**
* Get strings to highlight.
*
* @param array $default
*
* @return array
*/
public function toHighlight(array $default = [])
{
return $this->get('highlight', $default);
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"Date": "تاريخ",
"The list of logs is empty!": "قائمة سجلات فارغة!",
"All": "الجميع",
"Emergency": "حالات الطوارئ",
"Alert": "إنذار",
"Critical": "حرج",
"Error": "خطأ",
"Warning": "تحذير",
"Notice": "ملاحظة",
"Info": "المعلومات",
"Debug": "التصحيح"
}
+13
View File
@@ -0,0 +1,13 @@
{
"Date": "Дата",
"The list of logs is empty!": "Не са намерени логове!",
"All": "Всички",
"Emergency": "Emergency",
"Alert": "Alert",
"Critical": "Critical",
"Error": "Error",
"Warning": "Warning",
"Notice": "Notice",
"Info": "Info",
"Debug": "Debug"
}
+13
View File
@@ -0,0 +1,13 @@
{
"Date": "তারিখ",
"The list of logs is empty!": "কোন লগ পাওয়া যায়নি!",
"All": "সবগুলি",
"Emergency": "জরুরী",
"Alert": "সতর্ক",
"Critical": "সংকটপূর্ণ",
"Error": "ত্রুটি",
"Warning": "সতর্কতা",
"Notice": "নোটিস",
"Info": "তথ্য",
"Debug": "রুটি অনুসন্ধান"
}
+13
View File
@@ -0,0 +1,13 @@
{
"Date": "Datum",
"The list of logs is empty!": "KeineLogDateiengefunden!",
"All": "Alle",
"Emergency": "Notfall",
"Alert": "Alarm",
"Critical": "Kritisch",
"Error": "Fehler",
"Warning": "Warnung",
"Notice": "Hinweis",
"Info": "Info",
"Debug": "Debug"
}
+13
View File
@@ -0,0 +1,13 @@
{
"Date": "Fecha",
"The list of logs is empty!": "La lista del log está vacía!",
"All": "Todos",
"Emergency": "Emergencia",
"Alert": "Alerta",
"Critical": "Criticos",
"Error": "Errores",
"Warning": "Advertencia",
"Notice": "Aviso",
"Info": "Info",
"Debug": "Debug"
}
+13
View File
@@ -0,0 +1,13 @@
{
"Date": "Kuupäev",
"The list of logs is empty!": "Logide nimekiri on tühi!",
"All": "Kõik",
"Emergency": "Erakorraline",
"Alert": "Häire",
"Critical": "Kriitiline",
"Error": "Viga",
"Warning": "Hoiatus",
"Notice": "Teade",
"Info": "Info",
"Debug": "Silumine"
}
+13
View File
@@ -0,0 +1,13 @@
{
"Date": "تاریخ",
"The list of logs is empty!": "لیست لاگ ها خالی است!",
"All": "همه",
"Emergency": "اورژانسی",
"Alert": "اخطار",
"Critical": "بحرانی",
"Error": "خطا",
"Warning": "هشدار",
"Notice": "اعلان",
"Info": "اطلاعات",
"Debug": "دیباگ"
}
+13
View File
@@ -0,0 +1,13 @@
{
"Date": "Date",
"The list of logs is empty!": "La liste des logs est vide !",
"All": "Tous",
"Emergency": "Urgence",
"Alert": "Alerte",
"Critical": "Critique",
"Error": "Erreur",
"Warning": "Avertissement",
"Notice": "Notice",
"Info": "Info",
"Debug": "Debug"
}
+13
View File
@@ -0,0 +1,13 @@
{
"Date": "תאריך",
"The list of logs is empty!": "רשימת הלוגים ריקה!",
"All": "הכל",
"Emergency": "חרום",
"Alert": "אזעקה",
"Critical": "קריטי",
"Error": "שגיאה",
"Warning": "אזהרה",
"Notice": "הודעה",
"Info": "מידע",
"Debug": "ניפוי"
}
+13
View File
@@ -0,0 +1,13 @@
{
"Date": "Dátum",
"The list of logs is empty!": "A naplók listája üres!",
"All": "Összes",
"Emergency": "Vészhelyzet",
"Alert": "Riasztás",
"Critical": "Kritikus",
"Error": "Hiba",
"Warning": "Figyelmeztetés",
"Notice": "Értesítés",
"Info": "Információ",
"Debug": "Hibakeresés"
}
+13
View File
@@ -0,0 +1,13 @@
{
"Date": "Ամսաթիվ",
"The list of logs is empty!": "Լոգերի ցուցակը դատարկ է։",
"All": "Բոլորը",
"Emergency": "Վթարային",
"Alert": "Նախազգուշացում",
"Critical": "Կրիտիկական",
"Error": "Սխալ",
"Warning": "Նախազգուշացում",
"Notice": "Ծանուցում",
"Info": "Տեղեկատվություն",
"Debug": "Կարգաբերում"
}
+13
View File
@@ -0,0 +1,13 @@
{
"Date": "Tanggal",
"The list of logs is empty!": "Daftar Log Kosong",
"All": "Semua",
"Emergency": "Darurat",
"Alert": "Waspada",
"Critical": "Kritis",
"Error": "Kesalahan",
"Warning": "Peringatan",
"Notice": "Pemberitahuan",
"Info": "Info",
"Debug": "Debug"
}
+13
View File
@@ -0,0 +1,13 @@
{
"Date": "Data",
"The list of logs is empty!": "L'elenco dei log è vuoto!",
"All": "Tutti",
"Emergency": "Emergenza",
"Alert": "Allarme",
"Critical": "Critico",
"Error": "Errore",
"Warning": "Avviso",
"Notice": "Notifica",
"Info": "Info",
"Debug": "Debug"
}
+13
View File
@@ -0,0 +1,13 @@
{
"Date": "日付",
"The list of logs is empty!": "ログリストが空です!",
"All": "すべて",
"Emergency": "緊急",
"Alert": "警戒",
"Critical": "致命的",
"Error": "エラー",
"Warning": "警告",
"Notice": "通知",
"Info": "情報",
"Debug": "デバッグ"
}
+13
View File
@@ -0,0 +1,13 @@
{
"Date": "날짜",
"The list of logs is empty!": "로그가 없습니다.",
"All": "전체",
"Emergency": "긴급",
"Alert": "경고",
"Critical": "심각",
"Error": "오류",
"Warning": "주의",
"Notice": "알림",
"Info": "정보",
"Debug": "디버그"
}
+13
View File
@@ -0,0 +1,13 @@
{
"Date": "Tarikh",
"The list of logs is empty!": "Senarai log kosong!",
"All": "Semua",
"Emergency": "Kecemasan",
"Alert": "Waspada",
"Critical": "Kritikal",
"Error": "Ralat",
"Warning": "Amaran",
"Notice": "Notis",
"Info": "Info",
"Debug": "Debug"
}
+13
View File
@@ -0,0 +1,13 @@
{
"Date": "Datum",
"The list of logs is empty!": "De lijst met logs is leeg!",
"All": "Alle",
"Emergency": "Noodgeval",
"Alert": "Alarm",
"Critical": "Cruciaal",
"Error": "Error",
"Warning": "Waarschuwing",
"Notice": "Opmerking",
"Info": "Informatie",
"Debug": "Debug"
}
+13
View File
@@ -0,0 +1,13 @@
{
"Date": "Data",
"The list of logs is empty!": "Lista logów jest pusta!",
"All": "Wszystkie",
"Emergency": "Awaryjne",
"Alert": "Alerty",
"Critical": "Krytyczne",
"Error": "Błędy",
"Warning": "Ostrzeżenia",
"Notice": "Warte uwagi",
"Info": "Informacje",
"Debug": "Debug"
}
+13
View File
@@ -0,0 +1,13 @@
{
"Date": "Data",
"The list of logs is empty!": "A lista de logs está vazia!",
"All": "Todos",
"Emergency": "Emergência",
"Alert": "Alerta",
"Critical": "Crítico",
"Error": "Erro",
"Warning": "Aviso",
"Notice": "Notícia",
"Info": "Informação",
"Debug": "Debug"
}
+13
View File
@@ -0,0 +1,13 @@
{
"Date": "Dată",
"The list of logs is empty!": "Nu există niciun log!",
"All": "Toate",
"Emergency": "Urgență",
"Alert": "Alertă",
"Critical": "Critic",
"Error": "Eroare",
"Warning": "Pericol",
"Notice": "Avertisment",
"Info": "Informare",
"Debug": "Depanare"
}
+13
View File
@@ -0,0 +1,13 @@
{
"Date": "Дата",
"The list of logs is empty!": "Список журналов пуст!",
"All": "Все",
"Emergency": "Аварийная",
"Alert": "Предупреждение",
"Critical": "Критический",
"Error": "Ошибка",
"Warning": "Предупреждение",
"Notice": "Уведомление",
"Info": "Информация",
"Debug": "Отладка"
}
+13
View File
@@ -0,0 +1,13 @@
{
"Date": "දිනය",
"The list of logs is empty!": "සටහන් ලැයිස්තුව හිස්ය",
"All": "සියල්ල",
"Emergency": "හදිසි",
"Alert": "පරීක්ෂාකාරී",
"Critical": "අවදානම්",
"Error": "දෝෂය",
"Warning": "අවවාදය",
"Notice": "නිවේදනය",
"Info": "තොරතුරු",
"Debug": "නිදොස්කරණය"
}
+13
View File
@@ -0,0 +1,13 @@
{
"Date": "Datum",
"The list of logs is empty!": "Det finns inga loggar att visa.",
"All": "Alla",
"Emergency": "Akut",
"Alert": "Alarmerande",
"Critical": "Kritisk",
"Error": "Error",
"Warning": "Varning",
"Notice": "Notis",
"Info": "Information",
"Debug": "Debug"
}
+13
View File
@@ -0,0 +1,13 @@
{
"Date": "วันที่",
"The list of logs is empty!": "ไม่มีรายการล็อก!",
"All": "ทั้งหมด",
"Emergency": "ฉุกเฉิน",
"Alert": "วิกฤติ",
"Critical": "ร้ายแรง",
"Error": "ข้อผิดพลาด",
"Warning": "คำเตือน",
"Notice": "ประกาศ",
"Info": "ข้อมูล",
"Debug": "ดีบัก"
}
+13
View File
@@ -0,0 +1,13 @@
{
"Date": "Tarih",
"The list of logs is empty!": "Günlük listesi boş!",
"All": "Tümü",
"Emergency": "Acil",
"Alert": "Alarm",
"Critical": "Kritik",
"Error": "Hata",
"Warning": "Uyarı",
"Notice": "Bildirim",
"Info": "Bilgi",
"Debug": "Hata ayıklama"
}
+13
View File
@@ -0,0 +1,13 @@
{
"Date": "Дата",
"The list of logs is empty!": "Список журналів порожній!",
"All": "Всі",
"Emergency": "Аварійна",
"Alert": "Попередження",
"Critical": "Критична",
"Error": "Помилка",
"Warning": "Попереждення",
"Notice": "Сповіщення",
"Info": "Інформація",
"Debug": "Відладка"
}
+13
View File
@@ -0,0 +1,13 @@
{
"Date": "Sana",
"The list of logs is empty!": "Loglar ro'yhati bo'sh!",
"All": "Hammasi",
"Emergency": "Favqulodda holat",
"Alert": "Ogohlantirish",
"Critical": "Xavfli(Muhim)",
"Error": "Xato",
"Warning": "Ogohlantirish",
"Notice": "Eslatma",
"Info": "Ma'lumot",
"Debug": "Debug"
}
+13
View File
@@ -0,0 +1,13 @@
{
"Date": "日期",
"The list of logs is empty!": "列表中沒有任何紀錄!",
"All": "全部",
"Emergency": "緊急",
"Alert": "警報",
"Critical": "嚴重",
"Error": "錯誤",
"Warning": "警告",
"Notice": "注意",
"Info": "訊息",
"Debug": "除錯"
}
+13
View File
@@ -0,0 +1,13 @@
{
"Date": "日期",
"The list of logs is empty!": "日志列表为空!",
"All": "全部",
"Emergency": "危急",
"Alert": "紧急",
"Critical": "严重",
"Error": "错误",
"Warning": "警告",
"Notice": "注意",
"Info": "信息",
"Debug": "调试"
}
@@ -0,0 +1,342 @@
<!DOCTYPE html>
<html lang="{{ app()->getLocale() }}">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>LogViewer - Created by ARCANEDEV</title>
<meta name="description" content="LogViewer">
<meta name="author" content="ARCANEDEV">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css">
<link href='https://fonts.googleapis.com/css?family=Montserrat:400,700|Source+Sans+Pro:400,600' rel='stylesheet' type='text/css'>
<style>
html {
position: relative;
min-height: 100%;
}
body {
padding-top: 50px;
/* Margin bottom by footer height */
margin-bottom: 50px;
font-family: 'Source Sans Pro', 'Helvetica Neue', Helvetica, sans-serif;
font-weight: 600;
}
h1, h2, h3 {
font-family: 'Montserrat', 'Helvetica Neue', Helvetica, sans-serif;
}
.sub-header {
padding-bottom: 10px;
border-bottom: 1px solid #EEE;
}
.navbar-inverse {
background-color: #1a237e;
border-color: #1a237e;
}
.navbar-inverse .navbar-nav>.active>a,
.navbar-inverse .navbar-nav>.active>a:focus,
.navbar-inverse .navbar-nav>.active>a:hover {
background-color: #3949ab;
}
.navbar-inverse .navbar-brand {
color: #c5cae9;
}
.navbar-inverse .navbar-nav>li>a {
color: #c5cae9;
}
.navbar-fixed-top {
border: 0;
}
.main {
padding: 20px;
}
.main .page-header {
margin-top: 0;
}
footer.main-footer {
position: absolute;
padding: 10px 0;
bottom: 0;
width: 100%;
background-color: #e8eaf6;
font-weight: 600;
}
footer.main-footer p {
margin: 0;
}
footer.main-footer i.fa.fa-heart {
color: #C62828;
}
.pagination {
margin: 0;
}
.pagination > li > a,
.pagination > li > span {
padding: 4px 10px;
}
.table-condensed > tbody > tr > td.stack,
.table-condensed > tfoot > tr > td.stack,
.table-condensed > thead > tr > td.stack {
padding: 0;
border-top: none;
background-color: #F6F6F6;
border-top: 1px solid #D1D1D1;
max-width: 0;
overflow-x: auto;
}
.table-condensed > tbody > tr > td > p {
margin: 0;
}
.stack-content {
padding: 8px;
color: #AE0E0E;
font-family: consolas, Menlo, Courier, monospace;
font-size: 12px;
font-weight: 400;
white-space: pre-line;
}
.info-box.level {
display: block;
padding: 0;
margin-bottom: 15px;
min-height: 70px;
background: #fff;
width: 100%;
box-shadow: 0 1px 1px rgba(0,0,0,0.1);
border-radius: 2px;
}
.info-box.level .info-box-text,
.info-box.level .info-box-number,
.info-box.level .info-box-icon > i {
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.3);
}
.info-box.level .info-box-text {
display: block;
font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.info-box.level .info-box-content {
padding: 5px 10px;
margin-left: 70px;
}
.info-box.level .info-box-number {
display: block;
font-weight: bold;
font-size: 18px;
}
.info-box.level .info-box-icon {
border-radius: 2px 0 0 2px;
display: block;
float: left;
height: 70px; width: 70px;
text-align: center;
font-size: 40px;
line-height: 70px;
background: rgba(0,0,0,0.2);
}
.info-box.level .progress {
background: rgba(0,0,0,0.2);
margin: 5px -10px 5px -10px;
height: 2px;
}
.info-box.level .progress .progress-bar {
background: #fff;
}
.info-box.level-empty {
opacity: .6;
-webkit-filter: grayscale(1);
-moz-filter: grayscale(1);
-ms-filter: grayscale(1);
filter: grayscale(1);
-webkit-transition: all 0.2s ease-in-out;
-moz-transition: all 0.2s ease-in-out;
-o-transition: all 0.2s ease-in-out;
transition: all 0.2s ease-in-out;
-webkit-transition-property: -webkit-filter, opacity;
-moz-transition-property: -moz-filter, opacity;
-o-transition-property: filter, opacity;
transition-property: -webkit-filter, -moz-filter, -o-filter, filter, opacity;
}
.info-box.level-empty:hover {
opacity: 1;
-webkit-filter: grayscale(0);
-moz-filter: grayscale(0);
-ms-filter: grayscale(0);
filter: grayscale(0);
}
.level {
padding: 2px 6px;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.3);
border-radius: 2px;
font-size: .9em;
font-weight: 600;
}
.badge.level-all,
.badge.level-emergency,
.badge.level-alert,
.badge.level-critical,
.badge.level-error,
.badge.level-warning,
.badge.level-notice,
.badge.level-info,
.badge.level-debug,
.level, .level i,
.info-box.level-all,
.info-box.level-emergency,
.info-box.level-alert,
.info-box.level-critical,
.info-box.level-error,
.info-box.level-warning,
.info-box.level-notice,
.info-box.level-info,
.info-box.level-debug {
color: #FFF;
}
.label-env {
font-size: .85em;
}
.badge.level-all, .level.level-all, .info-box.level-all {
background-color: {{ log_styler()->color('all') }};
}
.badge.level-emergency, .level.level-emergency, .info-box.level-emergency {
background-color: {{ log_styler()->color('emergency') }};
}
.badge.level-alert, .level.level-alert, .info-box.level-alert {
background-color: {{ log_styler()->color('alert') }};
}
.badge.level-critical, .level.level-critical, .info-box.level-critical {
background-color: {{ log_styler()->color('critical') }};
}
.badge.level-error, .level.level-error, .info-box.level-error {
background-color: {{ log_styler()->color('error') }};
}
.badge.level-warning, .level.level-warning, .info-box.level-warning {
background-color: {{ log_styler()->color('warning') }};
}
.badge.level-notice, .level.level-notice, .info-box.level-notice {
background-color: {{ log_styler()->color('notice') }};
}
.badge.level-info, .level.level-info, .info-box.level-info {
background-color: {{ log_styler()->color('info') }};
}
.badge.level-debug, .level.level-debug, .info-box.level-debug {
background-color: {{ log_styler()->color('debug') }};
}
.badge.level-empty, .level.level-empty {
background-color: {{ log_styler()->color('empty') }};
}
.badge.label-env, .label.label-env {
background-color: #6A1B9A;
}
</style>
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
</head>
<body>
{{-- Navbar --}}
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
<span class="sr-only">@lang('Toggle navigation')</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a href="{{ route('log-viewer::dashboard') }}" class="navbar-brand">
<i class="fa fa-fw fa-book"></i> LogViewer
</a>
</div>
<div class="collapse navbar-collapse" id="navbar">
<ul class="nav navbar-nav">
<li class="{{ Route::is('log-viewer::dashboard') ? 'active' : '' }}">
<a href="{{ route('log-viewer::dashboard') }}">
<i class="fa fa-dashboard"></i> @lang('Dashboard')
</a>
</li>
<li class="{{ Route::is('log-viewer::logs.list') ? 'active' : '' }}">
<a href="{{ route('log-viewer::logs.list') }}">
<i class="fa fa-archive"></i> @lang('Logs')
</a>
</li>
</ul>
</div>
</div>
</nav>
{{-- Main container --}}
<main class="container-fluid">
@yield('content')
</main>
{{-- Footer --}}
<footer class="main-footer">
<div class="container">
<p class="text-muted pull-left">
LogViewer - <span class="label label-info">version {{ log_viewer()->version() }}</span>
</p>
<p class="text-muted pull-right">
Created with <i class="fa fa-heart"></i> by ARCANEDEV <sup>&copy;</sup>
</p>
</div>
</footer>
{{-- Scripts --}}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.3.0/Chart.min.js"></script>
<script>
Chart.defaults.global.responsive = true;
Chart.defaults.global.scaleFontFamily = "'Source Sans Pro'";
Chart.defaults.global.animationEasing = "easeOutQuart";
</script>
@yield('modals')
@yield('scripts')
</body>
</html>
@@ -0,0 +1,52 @@
@extends('log-viewer::bootstrap-3._master')
@section('content')
<h1 class="page-header">@lang('Dashboard')</h1>
<div class="row">
<div class="col-md-3">
<canvas id="stats-doughnut-chart" height="300"></canvas>
</div>
<div class="col-md-9">
<section class="box-body">
<div class="row">
@foreach($percents as $level => $item)
<div class="col-md-4">
<div class="info-box level level-{{ $level }} {{ $item['count'] === 0 ? 'level-empty' : '' }}">
<span class="info-box-icon">
{{ log_styler()->icon($level) }}
</span>
<div class="info-box-content">
<span class="info-box-text">{{ $item['name'] }}</span>
<span class="info-box-number">
{{ $item['count'] }} @lang('entries') - {!! $item['percent'] !!} %
</span>
<div class="progress">
<div class="progress-bar" style="width: {{ $item['percent'] }}%"></div>
</div>
</div>
</div>
</div>
@endforeach
</div>
</section>
</div>
</div>
@endsection
@section('scripts')
<script>
$(function() {
new Chart($('canvas#stats-doughnut-chart'), {
type: 'doughnut',
data: {!! $chartData !!},
options: {
legend: {
position: 'bottom'
}
}
});
});
</script>
@endsection
@@ -0,0 +1,152 @@
@extends('log-viewer::bootstrap-3._master')
<?php /** @var Illuminate\Pagination\LengthAwarePaginator $rows */ ?>
@section('content')
<h1 class="page-header">@lang('Logs')</h1>
{{ $rows->render() }}
<div class="table-responsive">
<table class="table table-condensed table-hover table-stats">
<thead>
<tr>
@foreach($headers as $key => $header)
<th class="{{ $key == 'date' ? 'text-left' : 'text-center' }}">
@if ($key == 'date')
<span class="label label-info">{{ $header }}</span>
@else
<span class="level level-{{ $key }}">
{{ log_styler()->icon($key) }} {{ $header }}
</span>
@endif
</th>
@endforeach
<th class="text-right">@lang('Actions')</th>
</tr>
</thead>
<tbody>
@forelse($rows as $date => $row)
<tr>
@foreach($row as $key => $value)
<td class="{{ $key == 'date' ? 'text-left' : 'text-center' }}">
@if ($key == 'date')
<span class="label label-primary">{{ $value }}</span>
@elseif ($value == 0)
<span class="level level-empty">{{ $value }}</span>
@else
<a href="{{ route('log-viewer::logs.filter', [$date, $key]) }}">
<span class="level level-{{ $key }}">{{ $value }}</span>
</a>
@endif
</td>
@endforeach
<td class="text-right">
<a href="{{ route('log-viewer::logs.show', [$date]) }}" class="btn btn-xs btn-info">
<i class="fa fa-search"></i>
</a>
<a href="{{ route('log-viewer::logs.download', [$date]) }}" class="btn btn-xs btn-success">
<i class="fa fa-download"></i>
</a>
<a href="#delete-log-modal" class="btn btn-xs btn-danger" data-log-date="{{ $date }}">
<i class="fa fa-trash-o"></i>
</a>
</td>
</tr>
@empty
<tr>
<td colspan="11" class="text-center">
<span class="label label-default">{{ trans('log-viewer::general.empty-logs') }}</span>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
{{ $rows->render() }}
@endsection
@section('modals')
{{-- DELETE MODAL --}}
<div id="delete-log-modal" class="modal fade">
<div class="modal-dialog">
<form id="delete-log-form" action="{{ route('log-viewer::logs.delete') }}" method="POST">
<input type="hidden" name="_method" value="DELETE">
<input type="hidden" name="_token" value="{{ csrf_token() }}">
<input type="hidden" name="date" value="">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">@lang('Delete log file')</h4>
</div>
<div class="modal-body">
<p></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-sm btn-default pull-left" data-dismiss="modal">@lang('Cancel')</button>
<button type="submit" class="btn btn-sm btn-danger" data-loading-text="@lang('Loading')&hellip;">@lang('Delete')</button>
</div>
</div>
</form>
</div>
</div>
@endsection
@section('scripts')
<script>
$(function () {
var deleteLogModal = $('div#delete-log-modal'),
deleteLogForm = $('form#delete-log-form'),
submitBtn = deleteLogForm.find('button[type=submit]');
$("a[href=#delete-log-modal]").on('click', function(event) {
event.preventDefault();
var date = $(this).data('log-date'),
message = "@lang('Are you sure you want to DELETE this log file: :date ?')";
deleteLogForm.find('input[name=date]').val(date);
deleteLogModal.find('.modal-body p').html(message.replace(':date', date));
deleteLogModal.modal('show');
});
deleteLogForm.on('submit', function(event) {
event.preventDefault();
submitBtn.button('loading');
$.ajax({
url: $(this).attr('action'),
type: $(this).attr('method'),
dataType: 'json',
data: $(this).serialize(),
success: function(data) {
submitBtn.button('reset');
if (data.result === 'success') {
deleteLogModal.modal('hide');
location.reload();
}
else {
alert('AJAX ERROR ! Check the console !');
console.error(data);
}
},
error: function(xhr, textStatus, errorThrown) {
alert('AJAX ERROR ! Check the console !');
console.error(errorThrown);
submitBtn.button('reset');
}
});
return false;
});
deleteLogModal.on('hidden.bs.modal', function() {
deleteLogForm.find('input[name=date]').val('');
deleteLogModal.find('.modal-body p').html('');
});
});
</script>
@endsection
@@ -0,0 +1,286 @@
<?php
/**
* @var Arcanedev\LogViewer\Entities\Log $log
* @var Illuminate\Pagination\LengthAwarePaginator|array<string|int, Arcanedev\LogViewer\Entities\LogEntry> $entries
* @var string|null $query
*/
?>
@extends('log-viewer::bootstrap-3._master')
@section('content')
<h1 class="page-header">@lang('Log') [{{ $log->date }}]</h1>
<div class="row">
<div class="col-md-2">
<div class="panel panel-default">
<div class="panel-heading"><i class="fa fa-fw fa-flag"></i> @lang('Levels')</div>
<ul class="list-group">
@foreach($log->menu() as $levelKey => $item)
@if ($item['count'] === 0)
<a href="#" class="list-group-item disabled">
<span class="badge">
{{ $item['count'] }}
</span>
{!! $item['icon'] !!} {{ $item['name'] }}
</a>
@else
<a href="{{ $item['url'] }}" class="list-group-item {{ $levelKey }}">
<span class="badge level-{{ $levelKey }}">
{{ $item['count'] }}
</span>
<span class="level level-{{ $levelKey }}">
{!! $item['icon'] !!} {{ $item['name'] }}
</span>
</a>
@endif
@endforeach
</ul>
</div>
</div>
<div class="col-md-10">
{{-- Log Details --}}
<div class="panel panel-default">
<div class="panel-heading">
@lang('Log info') :
<div class="group-btns pull-right">
<a href="{{ route('log-viewer::logs.download', [$log->date]) }}" class="btn btn-xs btn-success">
<i class="fa fa-download"></i> @lang('Download')
</a>
<a href="#delete-log-modal" class="btn btn-xs btn-danger" data-toggle="modal">
<i class="fa fa-trash-o"></i> @lang('Delete')
</a>
</div>
</div>
<div class="table-responsive">
<table class="table table-condensed">
<thead>
<tr>
<td>@lang('File path') :</td>
<td colspan="5">{{ $log->getPath() }}</td>
</tr>
</thead>
<tbody>
<tr>
<td>@lang('Log entries') :</td>
<td>
<span class="label label-primary">{{ $entries->total() }}</span>
</td>
<td>@lang('Size') :</td>
<td>
<span class="label label-primary">{{ $log->size() }}</span>
</td>
<td>@lang('Created at') :</td>
<td>
<span class="label label-primary">{{ $log->createdAt() }}</span>
</td>
<td>@lang('Updated at') :</td>
<td>
<span class="label label-primary">{{ $log->updatedAt() }}</span>
</td>
</tr>
</tbody>
</table>
</div>
<div class="panel-footer">
{{-- Search --}}
<form action="{{ route('log-viewer::logs.search', [$log->date, $level]) }}" method="GET">
<div class=form-group">
<div class="input-group">
<input id="query" name="query" class="form-control" value="{{ $query }}" placeholder="@lang('Type here to search')">
<span class="input-group-btn">
@unless (is_null($query))
<a href="{{ route('log-viewer::logs.show', [$log->date]) }}" class="btn btn-default">
(@lang(':count results', ['count' => $entries->count()])) <span class="glyphicon glyphicon-remove"></span>
</a>
@endunless
<button id="search-btn" class="btn btn-primary">
<span class="glyphicon glyphicon-search"></span>
</button>
</span>
</div>
</div>
</form>
</div>
</div>
{{-- Log Entries --}}
<div class="panel panel-default">
@if ($entries->hasPages())
<div class="panel-heading">
{{ $entries->appends(compact('query'))->render() }}
<span class="label label-info pull-right">
{{ __('Page :current of :last', ['current' => $entries->currentPage(), 'last' => $entries->lastPage()]) }}
</span>
</div>
@endif
<div class="table-responsive">
<table id="entries" class="table table-condensed">
<thead>
<tr>
<th>@lang('ENV')</th>
<th style="width: 120px;">@lang('Level')</th>
<th style="width: 65px;">@lang('Time')</th>
<th>@lang('Header')</th>
<th class="text-right">@lang('Actions')</th>
</tr>
</thead>
<tbody>
@forelse($entries as $key => $entry)
<tr>
<td>
<span class="label label-env">{{ $entry->env }}</span>
</td>
<td>
<span class="level level-{{ $entry->level }}">{!! $entry->level() !!}</span>
</td>
<td>
<span class="label label-default">
{{ $entry->datetime->format('H:i:s') }}
</span>
</td>
<td>
<p>{{ $entry->header }}</p>
</td>
<td class="text-right">
@if ($entry->hasStack())
<a class="btn btn-xs btn-default" role="button" data-toggle="collapse"
href="#log-stack-{{ $key }}" aria-expanded="false" aria-controls="log-stack-{{ $key }}">
<i class="fa fa-toggle-on"></i> @lang('Stack')
</a>
@endif
@if ($entry->hasContext())
<a class="btn btn-xs btn-default" role="button" data-toggle="collapse"
href="#log-context-{{ $key }}" aria-expanded="false" aria-controls="log-context-{{ $key }}">
<i class="fa fa-toggle-on"></i> @lang('Context')
</a>
@endif
</td>
</tr>
@if ($entry->hasStack() || $entry->hasContext())
<tr>
<td colspan="5" class="stack">
@if ($entry->hasStack())
<div class="stack-content collapse" id="log-stack-{{ $key }}">
{!! $entry->stack() !!}
</div>
@endif
@if ($entry->hasContext())
<div class="stack-content collapse" id="log-context-{{ $key }}">
<pre>{{ $entry->context() }}</pre>
</div>
@endif
</td>
</tr>
@endif
@empty
<tr>
<td colspan="5" class="text-center">
<span class="label label-default">@lang('The list of logs is empty!')</span>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@if ($entries->hasPages())
<div class="panel-footer">
{!! $entries->appends(compact('query'))->render() !!}
<span class="label label-info pull-right">
@lang('Page :current of :last', ['current' => $entries->currentPage(), 'last' => $entries->lastPage()])
</span>
</div>
@endif
</div>
</div>
</div>
@endsection
@section('modals')
{{-- DELETE MODAL --}}
<div id="delete-log-modal" class="modal fade">
<div class="modal-dialog">
<form id="delete-log-form" action="{{ route('log-viewer::logs.delete') }}" method="POST">
<input type="hidden" name="_method" value="DELETE">
<input type="hidden" name="_token" value="{{ csrf_token() }}">
<input type="hidden" name="date" value="{{ $log->date }}">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">@lang('Delete log file')</h4>
</div>
<div class="modal-body">
<p>@lang('Are you sure you want to delete this log file: :date ?', ['date' => $log->date])</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-sm btn-default pull-left" data-dismiss="modal">@lang('Cancel')</button>
<button type="submit" class="btn btn-sm btn-danger" data-loading-text="@lang('Loading')&hellip;">@lang('Delete')</button>
</div>
</div>
</form>
</div>
</div>
@endsection
@section('scripts')
<script>
$(function () {
var deleteLogModal = $('div#delete-log-modal'),
deleteLogForm = $('form#delete-log-form'),
submitBtn = deleteLogForm.find('button[type=submit]');
deleteLogForm.on('submit', function(event) {
event.preventDefault();
submitBtn.button('loading');
$.ajax({
url: $(this).attr('action'),
type: $(this).attr('method'),
dataType: 'json',
data: $(this).serialize(),
success: function(data) {
submitBtn.button('reset');
if (data.result === 'success') {
deleteLogModal.modal('hide');
location.replace("{{ route('log-viewer::logs.list') }}");
}
else {
alert('OOPS ! This is a lack of coffee exception !')
}
},
error: function(xhr, textStatus, errorThrown) {
alert('AJAX ERROR ! Check the console !');
console.error(errorThrown);
submitBtn.button('reset');
}
});
return false;
});
@unless (empty(log_styler()->toHighlight()))
@php
$htmlHighlight = version_compare(PHP_VERSION, '7.4.0') >= 0
? join('|', log_styler()->toHighlight())
: join(log_styler()->toHighlight(), '|');
@endphp
$('.stack-content').each(function() {
var $this = $(this);
var html = $this.html().trim()
.replace(/({!! $htmlHighlight !!})/gm, '<strong>$1</strong>');
$this.html(html);
});
@endunless
});
</script>
@endsection
@@ -0,0 +1,270 @@
<!doctype html>
<html lang="{{ app()->getLocale() }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="LogViewer">
<meta name="author" content="ARCANEDEV">
<title>LogViewer - Created by ARCANEDEV</title>
{{-- Styles --}}
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css">
<link href='https://fonts.googleapis.com/css?family=Montserrat:400,700|Source+Sans+Pro:400,600' rel='stylesheet' type='text/css'>
<style>
html {
position: relative;
min-height: 100%;
}
body {
font-size: .875rem;
margin-bottom: 60px;
}
.main-footer {
position: absolute;
bottom: 0;
width: 100%;
height: 60px;
line-height: 60px;
background-color: #E8EAF6;
}
.main-footer p {
margin-bottom: 0;
}
.main-footer .fa.fa-heart {
color: #C62828;
}
.page-header {
border-bottom: 1px solid #8a8a8a;
}
/*
* Navbar
*/
.navbar-brand {
padding: .75rem 1rem;
font-size: 1rem;
}
.navbar-nav .nav-link {
padding-right: .5rem;
padding-left: .5rem;
}
/*
* Boxes
*/
.box {
display: block;
padding: 0;
min-height: 70px;
background: #fff;
width: 100%;
box-shadow: 0 1px 1px rgba(0,0,0,0.1);
border-radius: .25rem;
}
.box > .box-icon > i,
.box .box-content .box-text,
.box .box-content .box-number {
color: #FFF;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.3);
}
.box > .box-icon {
border-radius: 2px 0 0 2px;
display: block;
float: left;
height: 70px; width: 70px;
text-align: center;
font-size: 40px;
line-height: 70px;
background: rgba(0,0,0,0.2);
}
.box .box-content {
padding: 5px 10px;
margin-left: 70px;
}
.box .box-content .box-text {
display: block;
font-size: 1rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-weight: 600;
}
.box .box-content .box-number {
display: block;
}
.box .box-content .progress {
background: rgba(0,0,0,0.2);
margin: 5px -10px 5px -10px;
}
.box .box-content .progress .progress-bar {
background-color: #FFF;
}
/*
* Log Menu
*/
.log-menu .list-group-item.disabled {
cursor: not-allowed;
}
.log-menu .list-group-item.disabled .level-name {
color: #D1D1D1;
}
/*
* Log Entry
*/
.stack-content {
color: #AE0E0E;
font-family: consolas, Menlo, Courier, monospace;
white-space: pre-line;
font-size: .8rem;
}
/*
* Colors: Badge & Infobox
*/
.badge.badge-env,
.badge.badge-level-all,
.badge.badge-level-emergency,
.badge.badge-level-alert,
.badge.badge-level-critical,
.badge.badge-level-error,
.badge.badge-level-warning,
.badge.badge-level-notice,
.badge.badge-level-info,
.badge.badge-level-debug,
.badge.empty {
color: #FFF;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.3);
}
.badge.badge-level-all,
.box.level-all {
background-color: {{ log_styler()->color('all') }};
}
.badge.badge-level-emergency,
.box.level-emergency {
background-color: {{ log_styler()->color('emergency') }};
}
.badge.badge-level-alert,
.box.level-alert {
background-color: {{ log_styler()->color('alert') }};
}
.badge.badge-level-critical,
.box.level-critical {
background-color: {{ log_styler()->color('critical') }};
}
.badge.badge-level-error,
.box.level-error {
background-color: {{ log_styler()->color('error') }};
}
.badge.badge-level-warning,
.box.level-warning {
background-color: {{ log_styler()->color('warning') }};
}
.badge.badge-level-notice,
.box.level-notice {
background-color: {{ log_styler()->color('notice') }};
}
.badge.badge-level-info,
.box.level-info {
background-color: {{ log_styler()->color('info') }};
}
.badge.badge-level-debug,
.box.level-debug {
background-color: {{ log_styler()->color('debug') }};
}
.badge.empty,
.box.empty {
background-color: {{ log_styler()->color('empty') }};
}
.badge.badge-env {
background-color: #6A1B9A;
}
#entries {
overflow-wrap: anywhere;
}
</style>
</head>
<body>
<nav class="navbar navbar-expand-md navbar-dark sticky-top bg-dark p-0">
<a href="{{ route('log-viewer::dashboard') }}" class="navbar-brand mr-0">
<i class="fa fa-fw fa-book"></i> LogViewer
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="{{ __('Toggle navigation') }}">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav mr-auto">
<li class="nav-item {{ Route::is('log-viewer::dashboard') ? 'active' : '' }}">
<a href="{{ route('log-viewer::dashboard') }}" class="nav-link">
<i class="fa fa-dashboard"></i> @lang('Dashboard')
</a>
</li>
<li class="nav-item {{ Route::is('log-viewer::logs.list') ? 'active' : '' }}">
<a href="{{ route('log-viewer::logs.list') }}" class="nav-link">
<i class="fa fa-archive"></i> @lang('Logs')
</a>
</li>
</ul>
</div>
</nav>
<div class="container-fluid">
<main role="main" class="pt-3">
@yield('content')
</main>
</div>
{{-- Footer --}}
<footer class="main-footer">
<div class="container-fluid">
<p class="text-muted pull-left">
LogViewer - <span class="badge badge-info">version {{ log_viewer()->version() }}</span>
</p>
<p class="text-muted pull-right">
Created with <i class="fa fa-heart"></i> by ARCANEDEV <sup>&copy;</sup>
</p>
</div>
</footer>
{{-- Scripts --}}
<script src="https://code.jquery.com/jquery-3.2.1.min.js" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.1/Chart.min.js"></script>
@yield('modals')
@yield('scripts')
</body>
</html>
@@ -0,0 +1,53 @@
@extends('log-viewer::bootstrap-4._master')
@section('content')
<div class="page-header mb-4">
<h1>@lang('Dashboard')</h1>
</div>
<div class="row">
<div class="col-md-6 col-lg-3">
<canvas id="stats-doughnut-chart" height="300" class="mb-3"></canvas>
</div>
<div class="col-md-6 col-lg-9">
<div class="row">
@foreach($percents as $level => $item)
<div class="col-sm-6 col-md-12 col-lg-4 mb-3">
<div class="box level-{{ $level }} {{ $item['count'] === 0 ? 'empty' : '' }}">
<div class="box-icon">
{!! log_styler()->icon($level) !!}
</div>
<div class="box-content">
<span class="box-text">{{ $item['name'] }}</span>
<span class="box-number">
{{ $item['count'] }} @lang('entries') - {!! $item['percent'] !!} %
</span>
<div class="progress" style="height: 3px;">
<div class="progress-bar" style="width: {{ $item['percent'] }}%"></div>
</div>
</div>
</div>
</div>
@endforeach
</div>
</div>
</div>
@endsection
@section('scripts')
<script>
$(function() {
new Chart(document.getElementById("stats-doughnut-chart"), {
type: 'doughnut',
data: {!! $chartData !!},
options: {
legend: {
position: 'bottom'
}
}
});
});
</script>
@endsection
@@ -0,0 +1,152 @@
@extends('log-viewer::bootstrap-4._master')
<?php /** @var Illuminate\Pagination\LengthAwarePaginator $rows */ ?>
@section('content')
<div class="page-header mb-4">
<h1>@lang('Logs')</h1>
</div>
<div class="table-responsive">
<table class="table table-sm table-hover">
<thead>
<tr>
@foreach($headers as $key => $header)
<th scope="col" class="{{ $key == 'date' ? 'text-left' : 'text-center' }}">
@if ($key == 'date')
<span class="badge badge-info">{{ $header }}</span>
@else
<span class="badge badge-level-{{ $key }}">
{{ log_styler()->icon($key) }} {{ $header }}
</span>
@endif
</th>
@endforeach
<th scope="col" class="text-right">@lang('Actions')</th>
</tr>
</thead>
<tbody>
@forelse($rows as $date => $row)
<tr>
@foreach($row as $key => $value)
<td class="{{ $key == 'date' ? 'text-left' : 'text-center' }}">
@if ($key == 'date')
<span class="badge badge-primary">{{ $value }}</span>
@elseif ($value == 0)
<span class="badge empty">{{ $value }}</span>
@else
<a href="{{ route('log-viewer::logs.filter', [$date, $key]) }}">
<span class="badge badge-level-{{ $key }}">{{ $value }}</span>
</a>
@endif
</td>
@endforeach
<td class="text-right">
<a href="{{ route('log-viewer::logs.show', [$date]) }}" class="btn btn-sm btn-info">
<i class="fa fa-search"></i>
</a>
<a href="{{ route('log-viewer::logs.download', [$date]) }}" class="btn btn-sm btn-success">
<i class="fa fa-download"></i>
</a>
<a href="#delete-log-modal" class="btn btn-sm btn-danger" data-log-date="{{ $date }}">
<i class="fa fa-trash-o"></i>
</a>
</td>
</tr>
@empty
<tr>
<td colspan="11" class="text-center">
<span class="badge badge-secondary">@lang('The list of logs is empty!')</span>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
{{ $rows->render() }}
@endsection
@section('modals')
{{-- DELETE MODAL --}}
<div id="delete-log-modal" class="modal fade" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<form id="delete-log-form" action="{{ route('log-viewer::logs.delete') }}" method="POST">
<input type="hidden" name="_method" value="DELETE">
<input type="hidden" name="_token" value="{{ csrf_token() }}">
<input type="hidden" name="date" value="">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">@lang('Delete log file')</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-sm btn-secondary mr-auto" data-dismiss="modal">@lang('Cancel')</button>
<button type="submit" class="btn btn-sm btn-danger" data-loading-text="@lang('Loading')&hellip;">@lang('Delete')</button>
</div>
</div>
</form>
</div>
</div>
@endsection
@section('scripts')
<script>
$(function () {
var deleteLogModal = $('div#delete-log-modal'),
deleteLogForm = $('form#delete-log-form'),
submitBtn = deleteLogForm.find('button[type=submit]');
$("a[href='#delete-log-modal']").on('click', function(event) {
event.preventDefault();
var date = $(this).data('log-date'),
message = "{{ __('Are you sure you want to delete this log file: :date ?') }}";
deleteLogForm.find('input[name=date]').val(date);
deleteLogModal.find('.modal-body p').html(message.replace(':date', date));
deleteLogModal.modal('show');
});
deleteLogForm.on('submit', function(event) {
event.preventDefault();
submitBtn.button('loading');
$.ajax({
url: $(this).attr('action'),
type: $(this).attr('method'),
dataType: 'json',
data: $(this).serialize(),
success: function(data) {
submitBtn.button('reset');
if (data.result === 'success') {
deleteLogModal.modal('hide');
location.reload();
}
else {
alert('AJAX ERROR ! Check the console !');
console.error(data);
}
},
error: function(xhr, textStatus, errorThrown) {
alert('AJAX ERROR ! Check the console !');
console.error(errorThrown);
submitBtn.button('reset');
}
});
return false;
});
deleteLogModal.on('hidden.bs.modal', function() {
deleteLogForm.find('input[name=date]').val('');
deleteLogModal.find('.modal-body p').html('');
});
});
</script>
@endsection
@@ -0,0 +1,273 @@
<?php
/**
* @var Arcanedev\LogViewer\Entities\Log $log
* @var Illuminate\Pagination\LengthAwarePaginator|array<string|int, Arcanedev\LogViewer\Entities\LogEntry> $entries
* @var string|null $query
*/
?>
@extends('log-viewer::bootstrap-4._master')
@section('content')
<div class="page-header mb-4">
<h1>@lang('Log') [{{ $log->date }}]</h1>
</div>
<div class="row">
<div class="col-lg-2">
{{-- Log Menu --}}
<div class="card mb-4">
<div class="card-header"><i class="fa fa-fw fa-flag"></i> @lang('Levels')</div>
<div class="list-group list-group-flush log-menu">
@foreach($log->menu() as $levelKey => $item)
@if ($item['count'] === 0)
<a class="list-group-item list-group-item-action d-flex justify-content-between align-items-center disabled">
<span class="level-name">{!! $item['icon'] !!} {{ $item['name'] }}</span>
<span class="badge empty">{{ $item['count'] }}</span>
</a>
@else
<a href="{{ $item['url'] }}" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center level-{{ $levelKey }}{{ $level === $levelKey ? ' active' : ''}}">
<span class="level-name">{!! $item['icon'] !!} {{ $item['name'] }}</span>
<span class="badge badge-level-{{ $levelKey }}">{{ $item['count'] }}</span>
</a>
@endif
@endforeach
</div>
</div>
</div>
<div class="col-lg-10">
{{-- Log Details --}}
<div class="card mb-4">
<div class="card-header">
@lang('Log info') :
<div class="group-btns pull-right">
<a href="{{ route('log-viewer::logs.download', [$log->date]) }}" class="btn btn-sm btn-success">
<i class="fa fa-download"></i> @lang('Download')
</a>
<a href="#delete-log-modal" class="btn btn-sm btn-danger" data-toggle="modal">
<i class="fa fa-trash-o"></i> @lang('Delete')
</a>
</div>
</div>
<div class="table-responsive">
<table class="table table-condensed mb-0">
<tbody>
<tr>
<td>@lang('File path') :</td>
<td colspan="7">{{ $log->getPath() }}</td>
</tr>
<tr>
<td>@lang('Log entries') :</td>
<td>
<span class="badge badge-primary">{{ $entries->total() }}</span>
</td>
<td>@lang('Size') :</td>
<td>
<span class="badge badge-primary">{{ $log->size() }}</span>
</td>
<td>@lang('Created at') :</td>
<td>
<span class="badge badge-primary">{{ $log->createdAt() }}</span>
</td>
<td>@lang('Updated at') :</td>
<td>
<span class="badge badge-primary">{{ $log->updatedAt() }}</span>
</td>
</tr>
</tbody>
</table>
</div>
<div class="card-footer">
{{-- Search --}}
<form action="{{ route('log-viewer::logs.search', [$log->date, $level]) }}" method="GET">
<div class="form-group">
<div class="input-group">
<input id="query" name="query" class="form-control" value="{{ $query }}" placeholder="@lang('Type here to search')">
<div class="input-group-append">
@unless (is_null($query))
<a href="{{ route('log-viewer::logs.show', [$log->date]) }}" class="btn btn-secondary">
(@lang(':count results', ['count' => $entries->count()])) <i class="fa fa-fw fa-times"></i>
</a>
@endunless
<button id="search-btn" class="btn btn-primary">
<span class="fa fa-fw fa-search"></span>
</button>
</div>
</div>
</div>
</form>
</div>
</div>
{{-- Log Entries --}}
<div class="card mb-4">
@if ($entries->hasPages())
<div class="card-header">
<span class="badge badge-info float-right">
{{ __('Page :current of :last', ['current' => $entries->currentPage(), 'last' => $entries->lastPage()]) }}
</span>
</div>
@endif
<div class="table-responsive">
<table id="entries" class="table mb-0">
<thead>
<tr>
<th>@lang('ENV')</th>
<th style="width: 120px;">@lang('Level')</th>
<th style="width: 65px;">@lang('Time')</th>
<th>@lang('Header')</th>
<th class="text-right">@lang('Actions')</th>
</tr>
</thead>
<tbody>
@forelse($entries as $key => $entry)
<tr>
<td>
<span class="badge badge-env">{{ $entry->env }}</span>
</td>
<td>
<span class="badge badge-level-{{ $entry->level }}">
{!! $entry->level() !!}
</span>
</td>
<td>
<span class="badge badge-secondary">
{{ $entry->datetime->format('H:i:s') }}
</span>
</td>
<td>
{{ $entry->header }}
</td>
<td class="text-right">
@if ($entry->hasStack())
<a class="btn btn-sm btn-light" role="button" data-toggle="collapse"
href="#log-stack-{{ $key }}" aria-expanded="false" aria-controls="log-stack-{{ $key }}">
<i class="fa fa-toggle-on"></i> @lang('Stack')
</a>
@endif
@if ($entry->hasContext())
<a class="btn btn-sm btn-light" role="button" data-toggle="collapse"
href="#log-context-{{ $key }}" aria-expanded="false" aria-controls="log-context-{{ $key }}">
<i class="fa fa-toggle-on"></i> @lang('Context')
</a>
@endif
</td>
</tr>
@if ($entry->hasStack() || $entry->hasContext())
<tr>
<td colspan="5" class="stack py-0">
@if ($entry->hasStack())
<div class="stack-content collapse" id="log-stack-{{ $key }}">
{!! $entry->stack() !!}
</div>
@endif
@if ($entry->hasContext())
<div class="stack-content collapse" id="log-context-{{ $key }}">
<pre>{{ $entry->context() }}</pre>
</div>
@endif
</td>
</tr>
@endif
@empty
<tr>
<td colspan="5" class="text-center">
<span class="badge badge-secondary">@lang('The list of logs is empty!')</span>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
{!! $entries->appends(compact('query'))->render() !!}
</div>
</div>
@endsection
@section('modals')
{{-- DELETE MODAL --}}
<div id="delete-log-modal" class="modal fade" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<form id="delete-log-form" action="{{ route('log-viewer::logs.delete') }}" method="POST">
<input type="hidden" name="_method" value="DELETE">
<input type="hidden" name="_token" value="{{ csrf_token() }}">
<input type="hidden" name="date" value="{{ $log->date }}">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">@lang('Delete log file')</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>@lang('Are you sure you want to delete this log file: :date ?', ['date' => $log->date])</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-sm btn-secondary mr-auto" data-dismiss="modal">@lang('Cancel')</button>
<button type="submit" class="btn btn-sm btn-danger" data-loading-text="@lang('Loading')&hellip;">@lang('Delete')</button>
</div>
</div>
</form>
</div>
</div>
@endsection
@section('scripts')
<script>
$(function () {
var deleteLogModal = $('div#delete-log-modal'),
deleteLogForm = $('form#delete-log-form'),
submitBtn = deleteLogForm.find('button[type=submit]');
deleteLogForm.on('submit', function(event) {
event.preventDefault();
submitBtn.button('loading');
$.ajax({
url: $(this).attr('action'),
type: $(this).attr('method'),
dataType: 'json',
data: $(this).serialize(),
success: function(data) {
submitBtn.button('reset');
if (data.result === 'success') {
deleteLogModal.modal('hide');
location.replace("{{ route('log-viewer::logs.list') }}");
}
else {
alert('OOPS ! This is a lack of coffee exception !')
}
},
error: function(xhr, textStatus, errorThrown) {
alert('AJAX ERROR ! Check the console !');
console.error(errorThrown);
submitBtn.button('reset');
}
});
return false;
});
@unless (empty(log_styler()->toHighlight()))
@php
$htmlHighlight = version_compare(PHP_VERSION, '7.4.0') >= 0
? join('|', log_styler()->toHighlight())
: join(log_styler()->toHighlight(), '|');
@endphp
$('.stack-content').each(function() {
var $this = $(this);
var html = $this.html().trim()
.replace(/({!! $htmlHighlight !!})/gm, '<strong>$1</strong>');
$this.html(html);
});
@endunless
});
</script>
@endsection
@@ -0,0 +1,277 @@
<!doctype html>
<html lang="{{ app()->getLocale() }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="LogViewer">
<meta name="author" content="ARCANEDEV">
<title>LogViewer - Created by ARCANEDEV</title>
{{-- Styles --}}
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css">
<link href='https://fonts.googleapis.com/css?family=Montserrat:400,700|Source+Sans+Pro:400,600' rel='stylesheet' type='text/css'>
<style>
html {
position: relative;
min-height: 100%;
}
body {
font-size: .875rem;
margin-bottom: 60px;
}
.main-footer {
position: absolute;
bottom: 0;
width: 100%;
height: 60px;
line-height: 60px;
background-color: #E8EAF6;
}
.main-footer p {
margin-bottom: 0;
}
.main-footer .fa.fa-heart {
color: #C62828;
}
.page-header {
border-bottom: 1px solid #8a8a8a;
}
/*
* Navbar
*/
.navbar-brand {
padding: .75rem 1rem;
font-size: 1rem;
}
.navbar-nav .nav-link {
padding-right: .5rem;
padding-left: .5rem;
}
/*
* Boxes
*/
.box {
display: block;
padding: 0;
min-height: 70px;
background: #fff;
width: 100%;
box-shadow: 0 1px 1px rgba(0,0,0,0.1);
border-radius: .25rem;
}
.box > .box-icon > i,
.box .box-content .box-text,
.box .box-content .box-number {
color: #FFF;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.3);
}
.box > .box-icon {
border-radius: 2px 0 0 2px;
display: block;
float: left;
height: 70px; width: 70px;
text-align: center;
font-size: 40px;
line-height: 70px;
background: rgba(0,0,0,0.2);
}
.box .box-content {
padding: 5px 10px;
margin-left: 70px;
}
.box .box-content .box-text {
display: block;
font-size: 1rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-weight: 600;
}
.box .box-content .box-number {
display: block;
}
.box .box-content .progress {
background: rgba(0,0,0,0.2);
margin: 5px -10px 5px -10px;
}
.box .box-content .progress .progress-bar {
background-color: #FFF;
}
/*
* Log Menu
*/
.log-menu .list-group-item.disabled {
cursor: not-allowed;
}
.log-menu .list-group-item.disabled .level-name {
color: #D1D1D1;
}
/*
* Log Entry
*/
.stack-content {
color: #AE0E0E;
font-family: consolas, Menlo, Courier, monospace;
white-space: pre-line;
font-size: .8rem;
}
/*
* Colors: Badge & Infobox
*/
.badge.badge-env,
.badge.badge-level-all,
.badge.badge-level-emergency,
.badge.badge-level-alert,
.badge.badge-level-critical,
.badge.badge-level-error,
.badge.badge-level-warning,
.badge.badge-level-notice,
.badge.badge-level-info,
.badge.badge-level-debug,
.badge.empty {
color: #FFF;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.3);
}
.badge.badge-level-all,
.box.level-all {
background-color: {{ log_styler()->color('all') }};
}
.badge.badge-level-emergency,
.box.level-emergency {
background-color: {{ log_styler()->color('emergency') }};
}
.badge.badge-level-alert,
.box.level-alert {
background-color: {{ log_styler()->color('alert') }};
}
.badge.badge-level-critical,
.box.level-critical {
background-color: {{ log_styler()->color('critical') }};
}
.badge.badge-level-error,
.box.level-error {
background-color: {{ log_styler()->color('error') }};
}
.badge.badge-level-warning,
.box.level-warning {
background-color: {{ log_styler()->color('warning') }};
}
.badge.badge-level-notice,
.box.level-notice {
background-color: {{ log_styler()->color('notice') }};
}
.badge.badge-level-info,
.box.level-info {
background-color: {{ log_styler()->color('info') }};
}
.badge.badge-level-debug,
.box.level-debug {
background-color: {{ log_styler()->color('debug') }};
}
.badge.empty,
.box.empty {
background-color: {{ log_styler()->color('empty') }};
}
.badge.badge-env {
background-color: #6A1B9A;
}
#entries {
overflow-wrap: anywhere;
}
</style>
</head>
<body>
<nav class="navbar navbar-expand-md navbar-dark sticky-top bg-dark p-0">
<a href="{{ route('log-viewer::dashboard') }}" class="navbar-brand mr-0">
<i class="fa fa-fw fa-book"></i> LogViewer
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="{{ __('Toggle navigation') }}">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav mr-auto">
<li class="nav-item {{ Route::is('log-viewer::dashboard') ? 'active' : '' }}">
<a href="{{ route('log-viewer::dashboard') }}" class="nav-link">
<i class="fa fa-dashboard"></i> @lang('Dashboard')
</a>
</li>
<li class="nav-item {{ Route::is('log-viewer::logs.list') ? 'active' : '' }}">
<a href="{{ route('log-viewer::logs.list') }}" class="nav-link">
<i class="fa fa-archive"></i> @lang('Logs')
</a>
</li>
</ul>
</div>
</nav>
<div class="container-fluid">
<main role="main" class="pt-3">
@yield('content')
</main>
</div>
{{-- Footer --}}
<footer class="main-footer">
<div class="container-fluid">
<p class="text-muted pull-left">
LogViewer - <span class="badge text-bg-info">version {{ log_viewer()->version() }}</span>
</p>
<p class="text-muted pull-right">
Created with <i class="fa fa-heart"></i> by ARCANEDEV <sup>&copy;</sup>
</p>
</div>
</footer>
{{-- Scripts --}}
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.min.js" integrity="sha384-BBtl+eGJRgqQAUMxJ7pMwbEyER4l1g+O15P+16Ep7Q9Q+zqX6gSbd85u4mG4QzX+" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.0/chart.umd.min.js" integrity="sha512-SIMGYRUjwY8+gKg7nn9EItdD8LCADSDfJNutF9TPrvEo86sQmFMh6MyralfIyhADlajSxqc7G0gs7+MwWF/ogQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script>
function ready(fn) {
if (document.readyState !== 'loading'){
fn();
} else {
document.addEventListener('DOMContentLoaded', fn);
}
}
</script>
@yield('modals')
@yield('scripts')
</body>
</html>
@@ -0,0 +1,53 @@
@extends('log-viewer::bootstrap-5._master')
@section('content')
<div class="page-header mb-4">
<h1>@lang('Dashboard')</h1>
</div>
<div class="row">
<div class="col-md-6 col-lg-3">
<canvas id="stats-doughnut-chart" height="300" class="mb-3"></canvas>
</div>
<div class="col-md-6 col-lg-9">
<div class="row">
@foreach($percents as $level => $item)
<div class="col-sm-6 col-md-12 col-lg-4 mb-3">
<div class="box level-{{ $level }} {{ $item['count'] === 0 ? 'empty' : '' }}">
<div class="box-icon">
{!! log_styler()->icon($level) !!}
</div>
<div class="box-content">
<span class="box-text">{{ $item['name'] }}</span>
<span class="box-number">
{{ $item['count'] }} @lang('entries') - {!! $item['percent'] !!} %
</span>
<div class="progress" style="height: 3px;">
<div class="progress-bar" style="width: {{ $item['percent'] }}%"></div>
</div>
</div>
</div>
</div>
@endforeach
</div>
</div>
</div>
@endsection
@section('scripts')
<script>
ready(function () {
new Chart(document.getElementById("stats-doughnut-chart"), {
type: 'doughnut',
data: {!! $chartData !!},
options: {
legend: {
position: 'bottom'
}
}
});
});
</script>
@endsection
@@ -0,0 +1,157 @@
@extends('log-viewer::bootstrap-5._master')
<?php /** @var Illuminate\Pagination\LengthAwarePaginator $rows */ ?>
@section('content')
<div class="page-header mb-4">
<h1>@lang('Logs')</h1>
</div>
<div class="table-responsive">
<table class="table table-sm table-hover">
<thead>
<tr>
@foreach($headers as $key => $header)
<th scope="col" class="{{ $key == 'date' ? 'text-left' : 'text-center' }}">
@if ($key == 'date')
<span class="badge text-bg-info">{{ $header }}</span>
@else
<span class="badge badge-level-{{ $key }}">
{{ log_styler()->icon($key) }} {{ $header }}
</span>
@endif
</th>
@endforeach
<th scope="col" class="text-end">@lang('Actions')</th>
</tr>
</thead>
<tbody>
@forelse($rows as $date => $row)
<tr>
@foreach($row as $key => $value)
<td class="{{ $key == 'date' ? 'text-left' : 'text-center' }}">
@if ($key == 'date')
<span class="badge text-bg-primary">{{ $value }}</span>
@elseif ($value == 0)
<span class="badge empty">{{ $value }}</span>
@else
<a href="{{ route('log-viewer::logs.filter', [$date, $key]) }}">
<span class="badge badge-level-{{ $key }}">{{ $value }}</span>
</a>
@endif
</td>
@endforeach
<td class="text-end">
<a href="{{ route('log-viewer::logs.show', [$date]) }}" class="btn btn-sm btn-info">
<i class="fa fa-fw fa-search"></i>
</a>
<a href="{{ route('log-viewer::logs.download', [$date]) }}" class="btn btn-sm btn-success">
<i class="fa fa-fw fa-download"></i>
</a>
<a href="#delete-log-modal" class="btn btn-sm btn-danger" data-log-date="{{ $date }}">
<i class="fa fa-fw fa-trash-o"></i>
</a>
</td>
</tr>
@empty
<tr>
<td colspan="11" class="text-center">
<span class="badge text-bg-secondary">@lang('The list of logs is empty!')</span>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
{{ $rows->render() }}
@endsection
@section('modals')
{{-- DELETE MODAL --}}
<div class="modal fade" id="delete-log-modal" tabindex="-1" aria-labelledby="delete-log-modal-label" aria-hidden="true">
<div class="modal-dialog">
<form id="delete-log-form" action="{{ route('log-viewer::logs.delete') }}" method="POST">
{{ csrf_field() }}
{{ method_field('DELETE') }}
<input type="hidden" name="date" value="">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="delete-log-modal-label">@lang('Delete log file')</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p></p>
</div>
<div class="modal-footer justify-content-between">
<button type="button" class="btn btn-sm btn-light" data-bs-dismiss="modal">@lang('Cancel')</button>
<button type="submit" class="btn btn-sm btn-danger" data-loading-text="@lang('Loading')&hellip;">@lang('Delete')</button>
</div>
</div>
</form>
</div>
</div>
@endsection
@section('scripts')
<script>
ready(() => {
let deleteLogModal = new bootstrap.Modal('div#delete-log-modal')
let deleteLogModalElt = deleteLogModal._element
let deleteLogForm = document.querySelector('form#delete-log-form')
let submitBtn = new bootstrap.Button(deleteLogForm.querySelector('button[type=submit]'))
document.querySelectorAll("a[href='#delete-log-modal']").forEach((elt) => {
elt.addEventListener('click', (event) => {
event.preventDefault()
let date = event.currentTarget.getAttribute('data-log-date')
let message = "{{ __('Are you sure you want to delete this log file: :date ?') }}"
deleteLogForm.querySelector('input[name=date]').value = date
deleteLogModalElt.querySelector('.modal-body p').innerHTML = message.replace(':date', date)
deleteLogModal.show()
})
})
deleteLogForm.addEventListener('submit', (event) => {
event.preventDefault()
submitBtn.toggle('loading')
fetch(event.currentTarget.getAttribute('action'), {
method: 'DELETE',
headers: {
"X-Requested-With": "XMLHttpRequest",
'Content-type': 'application/json'
},
body: JSON.stringify({
date: event.currentTarget.querySelector("input[name='date']").value,
})
})
.then((resp) => resp.json())
.then((resp) => {
if (resp.result === 'success') {
deleteLogModal.hide()
location.reload()
}
else {
alert('AJAX ERROR ! Check the console !')
console.error(resp)
}
})
.catch((err) => {
alert('AJAX ERROR ! Check the console !')
console.error(err)
})
return false
})
deleteLogModalElt.addEventListener('hidden.bs.modal', () => {
deleteLogForm.querySelector('input[name=date]').value = ''
deleteLogModalElt.querySelector('.modal-body p').innerHTML = ''
})
})
</script>
@endsection
@@ -0,0 +1,271 @@
<?php
/**
* @var Arcanedev\LogViewer\Entities\Log $log
* @var Illuminate\Pagination\LengthAwarePaginator $entries
* @var string|null $query
*/
?>
@extends('log-viewer::bootstrap-5._master')
@section('content')
<div class="page-header mb-4">
<h1>@lang('Log') [{{ $log->date }}]</h1>
</div>
<div class="row">
<div class="col-lg-2">
{{-- Log Menu --}}
<div class="card mb-4">
<div class="card-header"><i class="fa fa-fw fa-flag"></i> @lang('Levels')</div>
<div class="list-group list-group-flush log-menu">
@foreach($log->menu() as $levelKey => $item)
@if ($item['count'] === 0)
<a class="list-group-item list-group-item-action d-flex justify-content-between align-items-center disabled">
<span class="level-name">{!! $item['icon'] !!} {{ $item['name'] }}</span>
<span class="badge empty">{{ $item['count'] }}</span>
</a>
@else
<a href="{{ $item['url'] }}" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center level-{{ $levelKey }}{{ $level === $levelKey ? ' active' : ''}}">
<span class="level-name">{!! $item['icon'] !!} {{ $item['name'] }}</span>
<span class="badge badge-level-{{ $levelKey }}">{{ $item['count'] }}</span>
</a>
@endif
@endforeach
</div>
</div>
</div>
<div class="col-lg-10">
{{-- Log Details --}}
<div class="card mb-4">
<div class="card-header">
@lang('Log info') :
<div class="group-btns pull-right">
<a href="{{ route('log-viewer::logs.download', [$log->date]) }}" class="btn btn-sm btn-success">
<i class="fa fa-download"></i> @lang('Download')
</a>
<button type="button" class="btn btn-sm btn-danger" data-bs-toggle="modal" data-bs-target="#delete-log-modal">
<i class="fa fa-trash-o"></i> @lang('Delete')
</button>
</div>
</div>
<div class="table-responsive">
<table class="table table-condensed mb-0">
<tbody>
<tr>
<td>@lang('File path') :</td>
<td colspan="7">{{ $log->getPath() }}</td>
</tr>
<tr>
<td>@lang('Log entries') :</td>
<td>
<span class="badge text-bg-primary">{{ $entries->total() }}</span>
</td>
<td>@lang('Size') :</td>
<td>
<span class="badge text-bg-primary">{{ $log->size() }}</span>
</td>
<td>@lang('Created at') :</td>
<td>
<span class="badge text-bg-primary">{{ $log->createdAt() }}</span>
</td>
<td>@lang('Updated at') :</td>
<td>
<span class="badge text-bg-primary">{{ $log->updatedAt() }}</span>
</td>
</tr>
</tbody>
</table>
</div>
<div class="card-footer">
{{-- Search --}}
<form action="{{ route('log-viewer::logs.search', [$log->date, $level]) }}" method="GET">
<div class="form-group">
<div class="input-group">
<input type="search" id="query" name="query" class="form-control" value="{{ $query }}" placeholder="@lang('Type here to search')">
@unless (is_null($query))
<a href="{{ route('log-viewer::logs.show', [$log->date]) }}" class="btn btn-light">
(@lang(':count results', ['count' => $entries->count()])) <i class="fa fa-fw fa-times"></i>
</a>
@endunless
<button id="search-btn" class="btn btn-primary">
<span class="fa fa-fw fa-search"></span>
</button>
</div>
</div>
</form>
</div>
</div>
{{-- Log Entries --}}
<div class="card mb-4">
@if ($entries->hasPages())
<div class="card-header">
<span class="badge text-bg-info float-right">
{{ __('Page :current of :last', ['current' => $entries->currentPage(), 'last' => $entries->lastPage()]) }}
</span>
</div>
@endif
<div class="table-responsive">
<table id="entries" class="table mb-0">
<thead>
<tr>
<th>@lang('ENV')</th>
<th style="width: 120px;">@lang('Level')</th>
<th style="width: 65px;">@lang('Time')</th>
<th>@lang('Header')</th>
<th class="text-end">@lang('Actions')</th>
</tr>
</thead>
<tbody>
@forelse($entries as $key => $entry)
<?php /** @var Arcanedev\LogViewer\Entities\LogEntry $entry */ ?>
<tr>
<td>
<span class="badge badge-env">{{ $entry->env }}</span>
</td>
<td>
<span class="badge badge-level-{{ $entry->level }}">
{!! $entry->level() !!}
</span>
</td>
<td>
<span class="badge text-bg-secondary">
{{ $entry->datetime->format('H:i:s') }}
</span>
</td>
<td>
{{ $entry->header }}
</td>
<td class="text-end">
@if ($entry->hasStack())
<a class="btn btn-sm btn-light" role="button" data-bs-toggle="collapse"
href="#log-stack-{{ $key }}" aria-expanded="false" aria-controls="log-stack-{{ $key }}">
<i class="fa fa-toggle-on"></i> @lang('Stack')
</a>
@endif
@if ($entry->hasContext())
<a class="btn btn-sm btn-light" role="button" data-bs-toggle="collapse"
href="#log-context-{{ $key }}" aria-expanded="false" aria-controls="log-context-{{ $key }}">
<i class="fa fa-toggle-on"></i> @lang('Context')
</a>
@endif
</td>
</tr>
@if ($entry->hasStack() || $entry->hasContext())
<tr>
<td colspan="5" class="stack py-0">
@if ($entry->hasStack())
<div class="stack-content collapse" id="log-stack-{{ $key }}">
{!! $entry->stack() !!}
</div>
@endif
@if ($entry->hasContext())
<div class="stack-content collapse" id="log-context-{{ $key }}">
<pre>{{ $entry->context() }}</pre>
</div>
@endif
</td>
</tr>
@endif
@empty
<tr>
<td colspan="5" class="text-center">
<span class="badge text-bg-secondary">@lang('The list of logs is empty!')</span>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
{!! $entries->appends(compact('query'))->render() !!}
</div>
</div>
@endsection
@section('modals')
{{-- DELETE MODAL --}}
<div class="modal fade" id="delete-log-modal" tabindex="-1" aria-labelledby="delete-log-modal-label" aria-hidden="true">
<div class="modal-dialog">
<form id="delete-log-form" action="{{ route('log-viewer::logs.delete') }}" method="POST">
{{ csrf_field() }}
{{ method_field('DELETE') }}
<input type="hidden" name="date" value="{{ $log->date }}">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title fs-5" id="delete-log-modal-label">@lang('Delete log file')</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>@lang('Are you sure you want to delete this log file: :date ?', ['date' => $log->date])</p>
</div>
<div class="modal-footer justify-content-between">
<button type="button" class="btn btn-sm btn-light" data-bs-dismiss="modal">@lang('Cancel')</button>
<button type="submit" class="btn btn-sm btn-danger" data-loading-text="@lang('Loading')&hellip;">@lang('Delete')</button>
</div>
</div>
</form>
</div>
</div>
@endsection
@section('scripts')
<script>
ready(() => {
let deleteLogModal = new bootstrap.Modal('div#delete-log-modal')
let deleteLogForm = document.querySelector('form#delete-log-form')
let submitBtn = new bootstrap.Button(deleteLogForm.querySelector('button[type=submit]'))
deleteLogForm.addEventListener('submit', (event) => {
event.preventDefault()
submitBtn.toggle('loading')
fetch(event.currentTarget.getAttribute('action'), {
method: 'DELETE',
headers: {
"X-Requested-With": "XMLHttpRequest",
'Content-type': 'application/json'
},
body: JSON.stringify({
date: event.currentTarget.querySelector("input[name='date']").value,
})
})
.then((resp) => resp.json())
.then((resp) => {
if (resp.result === 'success') {
deleteLogModal.hide();
location.replace("{{ route('log-viewer::logs.list') }}");
}
else {
alert('AJAX ERROR ! Check the console !')
console.error(resp)
}
})
.catch((err) => {
alert('AJAX ERROR ! Check the console !')
console.error(err)
})
return false
})
@unless (empty(log_styler()->toHighlight()))
@php
$htmlHighlight = version_compare(PHP_VERSION, '7.4.0') >= 0
? join('|', log_styler()->toHighlight())
: join(log_styler()->toHighlight(), '|');
@endphp
document.querySelectorAll('.stack-content').forEach((elt) => {
elt.innerHTML = elt.innerHTML.trim()
.replace(/({!! $htmlHighlight !!})/gm, '<strong>$1</strong>')
})
@endunless
});
</script>
@endsection