init
This commit is contained in:
commit
931dd662ab
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2020 Jiang Qinghua
|
||||
|
||||
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.
|
75
README.md
Normal file
75
README.md
Normal file
|
@ -0,0 +1,75 @@
|
|||
<div align="center">
|
||||
|
||||
# Dcat Laravel Log Viewer
|
||||
|
||||
<p>
|
||||
<a href="https://github.com/jqhph/laravel-log-viewer/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-MIT-7389D8.svg?style=flat" ></a>
|
||||
<a href="https://styleci.io/repos/215738797">
|
||||
<img src="https://github.styleci.io/repos/215738797/shield" alt="StyleCI">
|
||||
</a>
|
||||
<a href="https://github.com/jqhph/laravel-log-viewer/releases" ><img src="https://img.shields.io/github/release/jqhph/laravel-log-viewer.svg?color=4099DE" /></a>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
`Dcat Log Viewer`是一个`Laravel`日志查看工具,支持查看、搜索大文件。
|
||||
|
||||
## 功能
|
||||
|
||||
- [x] 支持多层级目录
|
||||
- [x] 支持查看大文件日志
|
||||
- [x] 支持日志关键词检索
|
||||
- [x] 支持多层级目录文件名称搜索
|
||||
- [x] 支持下载功能
|
||||
- [x] 支持分页
|
||||
- [x] 支持手机页面
|
||||
|
||||
|
||||
|
||||
## 环境
|
||||
|
||||
- PHP >= 7
|
||||
- laravel >= 5.5
|
||||
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
composer require dcat/laravel-log-viewer
|
||||
```
|
||||
|
||||
发布配置文件,此步骤可省略
|
||||
|
||||
```bash
|
||||
php artisan vendor:publish --tag=dcat-log-viewer
|
||||
```
|
||||
|
||||
然后访问 `http://hostname/dcat-logs` 即可
|
||||
|
||||
配置文件
|
||||
|
||||
```php
|
||||
|
||||
return [
|
||||
'route' => [
|
||||
// 路由前缀
|
||||
'prefix' => 'dcat-logs',
|
||||
// 命名空间
|
||||
'namespace' => 'Dcat\LogViewer',
|
||||
// 中间件
|
||||
'middleware' => [],
|
||||
],
|
||||
|
||||
// 日志目录
|
||||
'directory' => storage_path('logs'),
|
||||
|
||||
// 搜索页显示条目数(搜索后不分页,所以这个参数可以设置大一些)
|
||||
'search_page_items' => 500,
|
||||
|
||||
// 默认每页条目数
|
||||
'page_items' => 30,
|
||||
];
|
||||
```
|
||||
|
||||
## License
|
||||
[The MIT License (MIT)](LICENSE).
|
29
composer.json
Normal file
29
composer.json
Normal file
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"name": "dcat/laravel-log-viewer",
|
||||
"description": "Laravel Log Viewer",
|
||||
"type": "library",
|
||||
"keywords": ["laravel", "log viewer"],
|
||||
"homepage": "https://github.com/jqhph/laravel-log-viewer",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "jqh",
|
||||
"email": "841324345@qq.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=7.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Dcat\\LogViewer\\": "src/"
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Dcat\\LogViewer\\DcatLogViewerServiceProvider"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
15
config/dcat-log-viewer.php
Normal file
15
config/dcat-log-viewer.php
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'route' => [
|
||||
'prefix' => 'dcat-logs',
|
||||
'namespace' => 'Dcat\LogViewer',
|
||||
'middleware' => [],
|
||||
],
|
||||
|
||||
'directory' => storage_path('logs'),
|
||||
|
||||
'search_page_items' => 500,
|
||||
|
||||
'page_items' => 30,
|
||||
];
|
392
resources/view/log.blade.php
Normal file
392
resources/view/log.blade.php
Normal file
|
@ -0,0 +1,392 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<title>Laravel log viewer</title>
|
||||
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
|
||||
<link href="https://cdn.bootcdn.net/ajax/libs/font-awesome/4.7.0/css/font-awesome.css" rel="stylesheet">
|
||||
<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
|
||||
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
|
||||
<!--[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]-->
|
||||
<style>
|
||||
body {
|
||||
padding: 25px;
|
||||
background: #eff3f8;
|
||||
color: #414750;
|
||||
font-size: 14px;
|
||||
}
|
||||
ol, ul {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-weight: 400;
|
||||
font-size: 20px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.logo {
|
||||
padding-left: 15px;
|
||||
font-weight: 400;
|
||||
font-size: 25px;
|
||||
}
|
||||
|
||||
.box {
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 3px #cdd8df;
|
||||
}
|
||||
|
||||
.box-header, .box-footer {
|
||||
padding: 15px;
|
||||
}
|
||||
.box-footer {
|
||||
border-top: 1px solid #efefef;
|
||||
}
|
||||
|
||||
.box-title a {
|
||||
color: #414750
|
||||
}
|
||||
|
||||
.table th {
|
||||
background-color: #f4f7fa;
|
||||
font-weight: 400;
|
||||
padding: .5rem 1.25rem;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.table td, .table th {
|
||||
border-color: #efefef;
|
||||
}
|
||||
|
||||
.table>thead>tr>th {
|
||||
border-bottom: 1px solid #e3e7eb!important;
|
||||
}
|
||||
.table-hover tbody tr:hover {
|
||||
background-color: #f4f8fb
|
||||
}
|
||||
|
||||
.label {
|
||||
max-width: 100%;
|
||||
margin-bottom: 5px;
|
||||
display: inline;
|
||||
padding: .2em .6em .3em;
|
||||
font-size: 75%;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
vertical-align: baseline;
|
||||
border-radius: .25em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #586cb1
|
||||
}
|
||||
a:hover, a:focus {
|
||||
color: #4b5ea0
|
||||
}
|
||||
|
||||
.bg-danger {
|
||||
color: #fff;
|
||||
background-color: #ef5753!important;
|
||||
}
|
||||
.bg-primary {
|
||||
color: #fff;
|
||||
background-color: #586cb1!important;
|
||||
}
|
||||
.bg-black {
|
||||
color: #fff;
|
||||
background-color: #444;
|
||||
}
|
||||
.bg-light {
|
||||
color: #555;
|
||||
background-color: #d2d6de!important;
|
||||
}
|
||||
.bg-orange {
|
||||
color: #fff;
|
||||
background-color: #dda451;
|
||||
}
|
||||
.bg-light-blue {
|
||||
color: #fff;
|
||||
background-color: #59a9f8;
|
||||
}
|
||||
.bg-maroon {
|
||||
color: #fff;
|
||||
background-color: #c00;
|
||||
}
|
||||
.bg-navy {
|
||||
color: #fff;
|
||||
background-color: #6f42c1;
|
||||
}
|
||||
|
||||
.btn-default {
|
||||
color: #414750;
|
||||
}
|
||||
|
||||
a.btn-default {
|
||||
background-color: #efefef;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #586cb1;
|
||||
border-color: #586cb1;
|
||||
}
|
||||
.btn-primary:hover, .btn-primary:focus, .btn-primary.active {
|
||||
background-color: #4b5ea0;
|
||||
border-color: #4b5ea0;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: #ef5753;
|
||||
border-color: #ef5753;
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 7px;
|
||||
white-space: pre-wrap;
|
||||
margin-bottom: 0;
|
||||
word-break: break-all;
|
||||
/*background-color: #f7f7f9;*/
|
||||
display: block;
|
||||
font-size: 90%;
|
||||
color: #2a2e30;
|
||||
}
|
||||
strong {
|
||||
color: #7c858e;
|
||||
}
|
||||
.trace-dump {
|
||||
white-space: pre-wrap;
|
||||
background: #222;
|
||||
color: #fff;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.nav {
|
||||
padding-left: 0;
|
||||
margin-bottom: 0;
|
||||
list-style: none;
|
||||
}
|
||||
.nav>li {
|
||||
position: relative;
|
||||
display: block;
|
||||
}
|
||||
.nav-pills>li {
|
||||
float: left;
|
||||
}
|
||||
.nav-stacked>li {
|
||||
float: none;
|
||||
width: 100%;
|
||||
}
|
||||
.nav>li>a {
|
||||
position: relative;
|
||||
display: block;
|
||||
padding: 6px 15px;
|
||||
}
|
||||
.nav-pills>li>a {
|
||||
border-radius: 4px;
|
||||
}
|
||||
.nav-pills>li>a {
|
||||
border-radius: 0;
|
||||
border-top: 3px solid transparent;
|
||||
color: #444;
|
||||
}
|
||||
.nav-stacked>li>a {
|
||||
border-radius: 0;
|
||||
border-top: 0;
|
||||
border-left: 3px solid transparent;
|
||||
color: #444;
|
||||
}
|
||||
.nav-pills>li>a>.fa, .nav-pills>li>a>.glyphicon, .nav-pills>li>a>.ion {
|
||||
margin-right: 5px;
|
||||
}
|
||||
.nav-stacked>li.active>a, .nav-stacked>li.active>a:hover {
|
||||
background: transparent;
|
||||
color: #4b5ea0;
|
||||
border-top: 0;
|
||||
font-weight: 600;
|
||||
/*border-left-color: #586cb1;*/
|
||||
}
|
||||
|
||||
.nav>li>a.dir {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="wrapper pl-2 pr-2">
|
||||
|
||||
<div class="row">
|
||||
|
||||
<div class="col-md-2">
|
||||
<div class="logo">
|
||||
Dcat Log Viewer
|
||||
</div>
|
||||
|
||||
<div class="">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title"><i class="fa fa-folder-open-o"></i>
|
||||
<a href="{{ route('dcat-log-viewer') }}">logs</a>
|
||||
@if($dir)
|
||||
@php($tmp = '')
|
||||
@foreach(explode('/', $dir) as $v)
|
||||
@php($tmp .= '/'.$v)
|
||||
/
|
||||
<a href="{{ route('dcat-log-viewer', ['dir' => trim($tmp, '/')])}}">{{ $v }}</a>
|
||||
@endforeach
|
||||
@endif
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<form action="{{ route('dcat-log-viewer') }}" style="display: inline-block;width: 220px;padding-left: 15px">
|
||||
<div class="input-group-sm" style="display: inline-block;width: 100%">
|
||||
<input name="filename" class="form-control" value="{{ request('filename') }}" type="text" placeholder="Search..." />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="box-body no-padding">
|
||||
<ul class="nav nav-pills nav-stacked">
|
||||
@if(! request('filename'))
|
||||
@foreach($logDirs as $d)
|
||||
<li @if($d === $fileName) class="active" @endif>
|
||||
<a class="dir" href="{{ route('dcat-log-viewer', ['dir' => $d]) }}">
|
||||
<i class="fa fa-folder-o"></i>{{ $d }}
|
||||
</a>
|
||||
</li>
|
||||
@endforeach
|
||||
@endif
|
||||
|
||||
@foreach($logFiles as $log)
|
||||
<li @if($log['active'])class="active"@endif>
|
||||
<a href="{{ $log['url'] }}">
|
||||
<i class="fa fa-file-text{{ ($log['active']) ? '' : '-o' }}"></i>{{ $log['file'] }}
|
||||
</a>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
<!-- /.box-body -->
|
||||
</div>
|
||||
|
||||
<!-- /.box -->
|
||||
</div>
|
||||
<!-- /.col -->
|
||||
|
||||
<!-- /.col -->
|
||||
<div class="col-md-10">
|
||||
<div class="box box-primary">
|
||||
<div class="box-header with-border">
|
||||
<a href="{{ route('dcat-log-viewer.download', ['dir' => $dir, 'file' => $fileName]) }}" class="btn btn-primary btn-sm download" style="color: #fff"><i class="fa-download fa"></i> {{ __('Download') }}</a>
|
||||
|
||||
{{-- <button class="btn btn-default btn-sm download"><i class="fa-trash-o fa"></i> {{ __('Delete') }}</button>--}}
|
||||
|
||||
<form style="display: inline-block;width: 180px">
|
||||
<div class="input-group-sm" style="display: inline-block;width: 100%">
|
||||
<input name="keyword" class="form-control" value="{{ request('keyword') }}" type="text" placeholder="Search..." />
|
||||
</div>
|
||||
</form>
|
||||
<div class="float-right">
|
||||
<a class=""><strong>Size:</strong> {{ $size }} <strong>Updated at:</strong>
|
||||
{{ \Carbon\Carbon::create(date('Y-m-d H:i:s', filectime($filePath)))->diffForHumans() }}</a>
|
||||
|
||||
<div class="btn-group">
|
||||
@if ($prevUrl)
|
||||
<a href="{{ $prevUrl }}" class="btn btn-default btn-sm"><i class="fa fa-chevron-left"></i> Previous</a>
|
||||
@endif
|
||||
@if ($nextUrl)
|
||||
<a href="{{ $nextUrl }}" class="btn btn-default btn-sm">Next <i class="fa fa-chevron-right"></i></a>
|
||||
@endif
|
||||
</div>
|
||||
<!-- /.btn-group -->
|
||||
</div>
|
||||
<!-- /.box-tools -->
|
||||
</div>
|
||||
<!-- /.box-header -->
|
||||
<div class="box-body no-padding">
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Level</th>
|
||||
<th>Env</th>
|
||||
<th>Time</th>
|
||||
<th>Message</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
|
||||
@foreach($logs as $index => $log)
|
||||
<tr>
|
||||
<td>{{ $index + 1 }}</td>
|
||||
<td><span class="label bg-{{\Dcat\LogViewer\LogViewer::$levelColors[$log['level']]}}">{{ $log['level'] }}</span></td>
|
||||
<td><strong>{{ $log['env'] }}</strong></td>
|
||||
<td style="width:150px;">{{ $log['time'] }}</td>
|
||||
<td><pre>{{ $log['info'] }}</pre></td>
|
||||
<td>
|
||||
@if(!empty($log['trace']))
|
||||
<button class="btn btn-primary btn-sm" data-toggle="collapse" data-target=".trace-{{$index}}"><i class="fa fa-info"></i> Exception</button>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@if (!empty($log['trace']))
|
||||
<tr class="collapse trace-{{$index}}">
|
||||
<td colspan="6"><div class="trace-dump">{{ $log['trace'] }}</div></td>
|
||||
</tr>
|
||||
@endif
|
||||
|
||||
@endforeach
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- /.table -->
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<div class="box-footer">
|
||||
<div class="float-left">
|
||||
<a class=""><strong>Size:</strong> {{ $size }} <strong>Updated at:</strong>
|
||||
{{ \Carbon\Carbon::create(date('Y-m-d H:i:s', filectime($filePath)))->diffForHumans() }}</a>
|
||||
</div>
|
||||
<div class="float-right">
|
||||
<div class="btn-group">
|
||||
@if ($prevUrl)
|
||||
<a href="{{ $prevUrl }}" class="btn btn-default btn-sm"><i class="fa fa-chevron-left"></i> Previous</a>
|
||||
@endif
|
||||
@if ($nextUrl)
|
||||
<a href="{{ $nextUrl }}" class="btn btn-default btn-sm">Next <i class="fa fa-chevron-right"></i></a>
|
||||
@endif
|
||||
</div>
|
||||
<!-- /.btn-group -->
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /. box -->
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- jQuery for Bootstrap -->
|
||||
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
|
||||
{{--<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>--}}
|
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
34
src/DcatLogViewerServiceProvider.php
Normal file
34
src/DcatLogViewerServiceProvider.php
Normal file
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
namespace Dcat\LogViewer;
|
||||
|
||||
use Illuminate\Routing\Router;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class DcatLogViewerServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function boot()
|
||||
{
|
||||
$this->loadViewsFrom(__DIR__.'/../resources/view', 'dcat-log-viewer');
|
||||
|
||||
if ($this->app->runningInConsole()) {
|
||||
$this->publishes([__DIR__.'/../config' => config_path()], 'dcat-log-viewer');
|
||||
}
|
||||
|
||||
$this->registerRoutes();
|
||||
}
|
||||
|
||||
protected function registerRoutes()
|
||||
{
|
||||
Route::group([
|
||||
'prefix' => config('dcat-log-viewer.route.prefix', 'dcat-logs'),
|
||||
'namespace' => config('dcat-log-viewer.route.namespace', 'Dcat\LogViewer'),
|
||||
'middleware' => config('dcat-log-viewer.route.middleware'),
|
||||
], function (Router $router) {
|
||||
$router->get('/', 'LogController@index')->name('dcat-log-viewer');
|
||||
$router->get('{file}', 'LogController@index')->name('dcat-log-viewer.file');
|
||||
$router->get('download/{file}', 'LogController@download')->name('dcat-log-viewer.download');
|
||||
});
|
||||
}
|
||||
}
|
81
src/LogController.php
Normal file
81
src/LogController.php
Normal file
|
@ -0,0 +1,81 @@
|
|||
<?php
|
||||
|
||||
namespace Dcat\LogViewer;
|
||||
|
||||
use Illuminate\Routing\Controller;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class LogController extends Controller
|
||||
{
|
||||
public function index($file = null)
|
||||
{
|
||||
$dir = trim(request('dir'));
|
||||
$filename = trim(request('filename'));
|
||||
$offset = request('offset');
|
||||
$keyword = trim(request('keyword'));
|
||||
$lines = $keyword ? (config('dcat-log-viewer.search_page_items') ?: 500) : (config('dcat-log-viewer.page_items') ?: 30);
|
||||
|
||||
$viewer = new LogViewer($this->getDirectory(), $dir, $file);
|
||||
|
||||
$viewer->setKeyword($keyword);
|
||||
$viewer->setFilename($filename);
|
||||
|
||||
return view('dcat-log-viewer::log', [
|
||||
'dir' => $dir,
|
||||
'logs' => $viewer->fetch($offset, $lines),
|
||||
'logFiles' => $this->formatLogFiles($viewer, $dir),
|
||||
'logDirs' => $viewer->getLogDirectories(),
|
||||
'fileName' => $viewer->file,
|
||||
'end' => $viewer->getFilesize(),
|
||||
'prevUrl' => $viewer->getPrevPageUrl(),
|
||||
'nextUrl' => $viewer->getNextPageUrl(),
|
||||
'filePath' => $viewer->getFilePath(),
|
||||
'size' => static::bytesToHuman($viewer->getFilesize()),
|
||||
]);
|
||||
}
|
||||
|
||||
public function download($file = null)
|
||||
{
|
||||
$viewer = new LogViewer($this->getDirectory(), request('dir'), $file);
|
||||
|
||||
return response()->download($viewer->getFilePath());
|
||||
}
|
||||
|
||||
protected function getDirectory()
|
||||
{
|
||||
return config('dcat-log-viewer.directory') ?: storage_path('logs');
|
||||
}
|
||||
|
||||
protected function formatLogFiles(LogViewer $logViewer, $currentDir)
|
||||
{
|
||||
return array_map(function ($value) use ($logViewer, $currentDir) {
|
||||
$file = $value;
|
||||
$dir = $currentDir;
|
||||
|
||||
if (Str::contains($value, '/')) {
|
||||
$array = explode('/', $value);
|
||||
$file = end($array);
|
||||
|
||||
array_pop($array);
|
||||
$dir = implode('/', $array);
|
||||
}
|
||||
|
||||
return [
|
||||
'file' => $value,
|
||||
'url' => route('dcat-log-viewer.file', ['file' => $file, 'dir' => $dir]),
|
||||
'active' => $logViewer->isCurrentFile($value),
|
||||
];
|
||||
}, $logViewer->getLogFiles());
|
||||
}
|
||||
|
||||
protected static function bytesToHuman($bytes)
|
||||
{
|
||||
$units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
||||
|
||||
for ($i = 0; $bytes > 1024; $i++) {
|
||||
$bytes /= 1024;
|
||||
}
|
||||
|
||||
return round($bytes, 2).' '.$units[$i];
|
||||
}
|
||||
}
|
491
src/LogViewer.php
Normal file
491
src/LogViewer.php
Normal file
|
@ -0,0 +1,491 @@
|
|||
<?php
|
||||
|
||||
namespace Dcat\LogViewer;
|
||||
|
||||
use Illuminate\Filesystem\Filesystem;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* Class LogViewer.
|
||||
*
|
||||
* @see https://github.com/laravel-admin-extensions/log-viewer/blob/master/src/LogViewer.php
|
||||
*/
|
||||
class LogViewer
|
||||
{
|
||||
/**
|
||||
* The log file name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $file;
|
||||
|
||||
/**
|
||||
* @var \Illuminate\Filesystem\Filesystem
|
||||
*/
|
||||
public $files;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $basePath;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $currentDirectory;
|
||||
|
||||
/**
|
||||
* The path of log file.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $filePath;
|
||||
|
||||
/**
|
||||
* Start and end offset in current page.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $pageOffset = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public static $levelColors = [
|
||||
'EMERGENCY' => 'black',
|
||||
'ALERT' => 'navy',
|
||||
'CRITICAL' => 'maroon',
|
||||
'ERROR' => 'danger',
|
||||
'WARNING' => 'orange',
|
||||
'NOTICE' => 'light-blue',
|
||||
'INFO' => 'primary',
|
||||
'DEBUG' => 'light',
|
||||
];
|
||||
|
||||
protected $keyword;
|
||||
|
||||
protected $filename;
|
||||
|
||||
/**
|
||||
* LogViewer constructor.
|
||||
*
|
||||
* @param null $file
|
||||
*/
|
||||
public function __construct($basePath, $dir, $file = null)
|
||||
{
|
||||
$this->basePath = trim($basePath, '/');
|
||||
$this->currentDirectory = trim($dir, '/');
|
||||
$this->file = $file;
|
||||
$this->files = new Filesystem();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file path by giving log file name.
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
*/
|
||||
public function getFilePath()
|
||||
{
|
||||
if (!$this->filePath) {
|
||||
$path = $this->mergeDirectory().'/'.$this->getFile();
|
||||
|
||||
$this->filePath = is_file($path) ? $path : false;
|
||||
}
|
||||
|
||||
return $this->filePath;
|
||||
}
|
||||
|
||||
public function setKeyword($value)
|
||||
{
|
||||
$this->keyword = $value;
|
||||
}
|
||||
|
||||
public function setFilename($value)
|
||||
{
|
||||
$this->filename = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get size of log file.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getFilesize()
|
||||
{
|
||||
if (!$this->getFilePath()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return filesize($this->getFilePath());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get log file list in storage.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getLogFiles()
|
||||
{
|
||||
if ($this->filename) {
|
||||
return collect($this->files->allFiles($this->mergeDirectory()))->map(function (\SplFileInfo $fileInfo) {
|
||||
return $this->replaceBasePath($fileInfo->getRealPath());
|
||||
})->filter(function ($v) {
|
||||
return Str::contains($v, $this->filename);
|
||||
})->toArray();
|
||||
}
|
||||
|
||||
$files = glob($this->mergeDirectory().'/*.*');
|
||||
$files = array_combine($files, array_map('filemtime', $files));
|
||||
arsort($files);
|
||||
|
||||
return array_map('basename', array_keys($files));
|
||||
}
|
||||
|
||||
public function getLogDirectories()
|
||||
{
|
||||
return array_map([$this, 'replaceBasePath'], $this->files->directories($this->mergeDirectory()));
|
||||
}
|
||||
|
||||
protected function replaceBasePath($v)
|
||||
{
|
||||
$basePath = str_replace('\\', '/', $this->getLogBasePath());
|
||||
|
||||
return str_replace($basePath.'/', '', str_replace('\\', '/', $v));
|
||||
}
|
||||
|
||||
public function mergeDirectory()
|
||||
{
|
||||
if (!$this->currentDirectory) {
|
||||
return $this->getLogBasePath();
|
||||
}
|
||||
|
||||
return $this->getLogBasePath() . '/' . $this->currentDirectory;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getLogBasePath()
|
||||
{
|
||||
return $this->basePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last modified log file.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLastModifiedLog()
|
||||
{
|
||||
return current($this->getLogFiles());
|
||||
}
|
||||
|
||||
public function getFile()
|
||||
{
|
||||
if (! $this->file) {
|
||||
$this->file = $this->getLastModifiedLog();
|
||||
}
|
||||
|
||||
return $this->file;
|
||||
}
|
||||
|
||||
public function isCurrentFile($file)
|
||||
{
|
||||
return $this->replaceBasePath($this->getFilePath()) === trim($this->currentDirectory.'/'.$file, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get previous page url.
|
||||
*
|
||||
* @return bool|string
|
||||
*/
|
||||
public function getPrevPageUrl()
|
||||
{
|
||||
if (
|
||||
!$this->getFilePath()
|
||||
|| $this->pageOffset['end'] >= $this->getFilesize() - 1
|
||||
|| $this->keyword
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return route('dcat-log-viewer.file', [
|
||||
'file' => $this->getFile(),
|
||||
'offset' => $this->pageOffset['end'],
|
||||
'keyword' => $this->keyword,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Next page url.
|
||||
*
|
||||
* @return bool|string
|
||||
*/
|
||||
public function getNextPageUrl()
|
||||
{
|
||||
if (
|
||||
!$this->getFilePath()
|
||||
|| $this->pageOffset['start'] == 0
|
||||
|| $this->keyword
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return route('dcat-log-viewer.file', [
|
||||
'file' => $this->getFile(),
|
||||
'offset' => -$this->pageOffset['start'],
|
||||
'keyword' => $this->keyword,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch logs by giving offset.
|
||||
*
|
||||
* @param int $seek
|
||||
* @param int $lines
|
||||
* @param int $buffer
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @see http://www.geekality.net/2011/05/28/php-tail-tackling-large-files/
|
||||
*/
|
||||
public function fetch($seek = 0, $lines = 20, $buffer = 4096)
|
||||
{
|
||||
$logs = $this->read($seek, $lines, $buffer);
|
||||
|
||||
if (!$this->keyword || !$logs) {
|
||||
return $logs;
|
||||
}
|
||||
|
||||
$result = [];
|
||||
|
||||
foreach ($logs as $log) {
|
||||
if (Str::contains(implode(' ', $log), $this->keyword)) {
|
||||
$result[] = $log;
|
||||
}
|
||||
}
|
||||
|
||||
if (count($result) >= $lines || !$this->getNextOffset()) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
return array_merge($result, $this->fetch($this->getNextOffset(), $lines - count($result), $buffer));
|
||||
}
|
||||
|
||||
public function getNextOffset()
|
||||
{
|
||||
if ($this->pageOffset['start'] == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return -$this->pageOffset['start'];
|
||||
}
|
||||
|
||||
protected function read($seek = 0, $lines = 20, $buffer = 4096)
|
||||
{
|
||||
if (! $this->getFilePath()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$f = fopen($this->getFilePath(), 'rb');
|
||||
|
||||
if ($seek) {
|
||||
fseek($f, abs($seek));
|
||||
} else {
|
||||
fseek($f, 0, SEEK_END);
|
||||
}
|
||||
|
||||
if (fread($f, 1) != "\n") {
|
||||
$lines -= 1;
|
||||
}
|
||||
fseek($f, -1, SEEK_CUR);
|
||||
|
||||
// 从前往后读,上一页
|
||||
// Start reading
|
||||
if ($seek > 0) {
|
||||
$output = $this->readPrevPage($f, $lines, $buffer);
|
||||
// 从后往前读,下一页
|
||||
} else {
|
||||
$output = $this->readNextPage($f, $lines, $buffer);
|
||||
}
|
||||
|
||||
fclose($f);
|
||||
|
||||
return $this->parseLog($output);
|
||||
}
|
||||
|
||||
protected function readPrevPage($f, &$lines, $buffer)
|
||||
{
|
||||
$output = '';
|
||||
|
||||
$this->pageOffset['start'] = ftell($f);
|
||||
|
||||
while (!feof($f) && $lines >= 0) {
|
||||
$output = $output . ($chunk = fread($f, $buffer));
|
||||
$lines -= substr_count($chunk, "\n[20");
|
||||
}
|
||||
|
||||
$this->pageOffset['end'] = ftell($f);
|
||||
|
||||
while ($lines++ < 0) {
|
||||
$strpos = strrpos($output, "\n[20") + 1;
|
||||
$_ = mb_strlen($output, '8bit') - $strpos;
|
||||
$output = substr($output, 0, $strpos);
|
||||
$this->pageOffset['end'] -= $_;
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
protected function readNextPage($f, &$lines, $buffer)
|
||||
{
|
||||
$output = '';
|
||||
|
||||
$this->pageOffset['end'] = ftell($f);
|
||||
|
||||
while (ftell($f) > 0 && $lines >= 0) {
|
||||
$offset = min(ftell($f), $buffer);
|
||||
fseek($f, -$offset, SEEK_CUR);
|
||||
$output = ($chunk = fread($f, $offset)) . $output;
|
||||
fseek($f, -mb_strlen($chunk, '8bit'), SEEK_CUR);
|
||||
$lines -= substr_count($chunk, "\n[20");
|
||||
}
|
||||
|
||||
$this->pageOffset['start'] = ftell($f);
|
||||
|
||||
while ($lines++ < 0) {
|
||||
$strpos = strpos($output, "\n[20") + 1;
|
||||
$output = substr($output, $strpos);
|
||||
$this->pageOffset['start'] += $strpos;
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tail logs in log file.
|
||||
*
|
||||
* @param int $seek
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function tail($seek)
|
||||
{
|
||||
// Open the file
|
||||
$f = fopen($this->getFilePath(), 'rb');
|
||||
|
||||
if (!$seek) {
|
||||
// Jump to last character
|
||||
fseek($f, -1, SEEK_END);
|
||||
} else {
|
||||
fseek($f, abs($seek));
|
||||
}
|
||||
|
||||
$output = '';
|
||||
|
||||
while (!feof($f)) {
|
||||
$output .= fread($f, 4096);
|
||||
}
|
||||
|
||||
$pos = ftell($f);
|
||||
|
||||
fclose($f);
|
||||
|
||||
$logs = [];
|
||||
|
||||
foreach ($this->parseLog(trim($output)) as $log) {
|
||||
$logs[] = $this->renderTableRow($log);
|
||||
}
|
||||
|
||||
return [$pos, $logs];
|
||||
}
|
||||
|
||||
/**
|
||||
* Render table row.
|
||||
*
|
||||
* @param $log
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function renderTableRow($log)
|
||||
{
|
||||
$color = self::$levelColors[$log['level']] ?? 'black';
|
||||
|
||||
$index = uniqid();
|
||||
|
||||
$button = '';
|
||||
|
||||
if (!empty($log['trace'])) {
|
||||
$button = "<a class=\"btn btn-primary btn-xs\" data-toggle=\"collapse\" data-target=\".trace-{$index}\"><i class=\"fa fa-info\"></i> Exception</a>";
|
||||
}
|
||||
|
||||
$trace = '';
|
||||
|
||||
if (!empty($log['trace'])) {
|
||||
$trace = "<tr class=\"collapse trace-{$index}\">
|
||||
<td colspan=\"5\"><div style=\"white-space: pre-wrap;background: #333;color: #fff; padding: 10px;\">{$log['trace']}</div></td>
|
||||
</tr>";
|
||||
}
|
||||
|
||||
return <<<TPL
|
||||
<tr style="background-color: rgb(255, 255, 213);">
|
||||
<td><span class="label bg-{$color}">{$log['level']}</span></td>
|
||||
<td><strong>{$log['env']}</strong></td>
|
||||
<td style="width:150px;">{$log['time']}</td>
|
||||
<td><code>{$log['info']}</code></td>
|
||||
<td>$button</td>
|
||||
</tr>
|
||||
$trace
|
||||
TPL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse raw log text to array.
|
||||
*
|
||||
* @param $raw
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function parseLog($raw)
|
||||
{
|
||||
$logs = preg_split('/\[(\d{4}(?:-\d{2}){2} \d{2}(?::\d{2}){2})\] (\w+)\.(\w+):((?:(?!{"exception").)*)?/', trim($raw), -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
|
||||
|
||||
foreach ($logs as $index => $log) {
|
||||
if (preg_match('/^\d{4}/', $log)) {
|
||||
break;
|
||||
} else {
|
||||
unset($logs[$index]);
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($logs)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$parsed = [];
|
||||
|
||||
foreach (array_chunk($logs, 5) as $log) {
|
||||
$parsed[] = [
|
||||
'time' => $log[0] ?? '',
|
||||
'env' => $log[1] ?? '',
|
||||
'level' => $log[2] ?? '',
|
||||
'info' => $log[3] ?? '',
|
||||
'trace' => $this->replaceRootPath(trim($log[4] ?? '')),
|
||||
];
|
||||
}
|
||||
|
||||
unset($logs);
|
||||
|
||||
rsort($parsed);
|
||||
|
||||
return $parsed;
|
||||
}
|
||||
|
||||
protected function replaceRootPath($content)
|
||||
{
|
||||
$basePath = str_replace('\\', '/', base_path() . '/');
|
||||
|
||||
return str_replace($basePath, '', str_replace(['\\\\', '\\'], '/', $content));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user