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