feat(nginx-log): curd 生成 raw/position 后台并改只读

- NginxAccessLog + NginxLogPosition Scheme 加 #[Component] 注解
- Scheme Table name 补 ul_ 前缀(curd 严格校验要求)
- curd 生成 nginx 模块完整后台(controller/model/view/js)
- Base/App 双层只读控制器(参照 TimerLogBase 范式)
  - AccessLogBase: add/edit/delete/modify 全部 error
  - LogPositionBase: 同上
- 视图改造:
  - access_log 裁剪到 8 列核心字段(time_local/method/uri/status/remote_addr/request_time/body_bytes_sent/http_user_agent)
  - status/request_time 自定义 badge 渲染
  - toolbar 清空 + operat 仅保留详情(B 类只读页面)
  - log_position 全字段 + 只读化
- HTTP 验证:index 200 OK + add/edit/delete/modify 全部返回错误
This commit is contained in:
augushong
2026-07-28 06:21:13 +08:00
parent 7987001cd2
commit d5329cdd45
26 changed files with 1203 additions and 34 deletions

View File

@@ -0,0 +1,13 @@
<?php
namespace app\admin\controller\nginx;
use base\admin\controller\nginx\AccessLogBase;
/**
* nginx 访问日志明细(解析入库)
* 日志表为只读,由系统定时任务自动写入,后台不支持手动增删改。
*/
class AccessLog extends AccessLogBase
{
}

View File

@@ -0,0 +1,13 @@
<?php
namespace app\admin\controller\nginx;
use base\admin\controller\nginx\LogPositionBase;
/**
* nginx 日志文件读取位置(增量读取状态)
* 日志表为只读,由系统定时任务自动维护,后台不支持手动增删改。
*/
class LogPosition extends LogPositionBase
{
}

View File

@@ -5,30 +5,36 @@ namespace app\admin\model;
use app\common\model\TimeModel;
/**
* @property int $id
* @property int $id 主键ID
* @property string $file_path 来源日志文件路径
* @property string $remote_addr 客户端 IP
* @property string $remote_addr 客户端 IP(含 IPv6
* @property string $remote_user HTTP 认证用户名
* @property int $time_local 请求时间戳
* @property int $time_local 请求时间戳(业务时区)
* @property string $method HTTP 方法
* @property string $uri 请求 URI
* @property string $uri 请求 URI(超 255 截断)
* @property string $query_string 查询字符串
* @property string $http_version HTTP 协议版本
* @property int $status HTTP 状态码
* @property int $body_bytes_sent 响应体字节数
* @property string $http_referer Referer
* @property string $http_user_agent User-Agent
* @property float $request_time 请求总耗时(秒)
* @property float $upstream_response_time upstream 响应时间(秒)
* @property int $bytes_sent 总发送字节数
* @property string $country 国家
* @property string $province 省份
* @property string $city 城市
* @property string $http_user_agent User-Agent(超 500 截断)
* @property float|double $request_time nginx 请求总耗时
* @property float|double $upstream_response_time upstream 响应时间
* @property int $bytes_sent 总发送字节数(含 header
* @property string $country 国家GeoIP 预留)
* @property string $province 省份GeoIP 预留)
* @property string $city 城市GeoIP 预留)
* @property int $create_time 入库时间戳
*/
class NginxAccessLog extends TimeModel
{
protected $name = 'nginx_access_log';
protected $name = "nginx_access_log";
protected $deleteTime = false;
}

View File

@@ -7,18 +7,24 @@ use app\common\model\TimeModel;
/**
* @property int $id
* @property string $file_path 日志文件绝对路径
* @property int $inode 文件 inode
* @property int $offset 字节偏移
* @property int $inode 文件 inode,用于检测日志轮转
* @property int $offset 上次读取到的字节偏移
* @property int $last_read_time 上次读取时间戳
* @property string $last_line_hash 上次最后一行 md5
* @property string $last_line_hash 上次最后一行 md5 哈希(容错校验)
* @property int $parse_fail_count 解析失败累计次数
* @property string $parse_fail_samples 解析失败样本
* @property string $parse_fail_samples 解析失败样本行(最多保留若干条)
* @property int $create_time 创建时间戳
* @property int $update_time 更新时间戳
*/
class NginxLogPosition extends TimeModel
{
protected $name = 'nginx_log_position';
protected $name = "nginx_log_position";
protected $deleteTime = false;
}

View File

@@ -5,9 +5,10 @@ namespace app\admin\scheme;
use app\common\scheme\BaseScheme;
use app\common\scheme\attribute\Table;
use app\common\scheme\attribute\Field;
use app\common\scheme\attribute\Component;
use app\common\scheme\attribute\Index;
#[Table(name: 'nginx_access_log', comment: 'nginx 访问日志明细(解析入库)')]
#[Table(name: 'ul_nginx_access_log', comment: 'nginx 访问日志明细(解析入库)')]
#[Index(columns: ['time_local'], name: 'idx_time_local', type: 'NORMAL')]
#[Index(columns: ['remote_addr'], name: 'idx_remote_addr', type: 'NORMAL')]
#[Index(columns: ['status'], name: 'idx_status', type: 'NORMAL')]
@@ -19,59 +20,78 @@ class NginxAccessLog extends BaseScheme
public $id;
#[Field(type: 'varchar', length: 500, nullable: false, comment: '来源日志文件路径')]
#[Component(type: 'text', options: [])]
public $file_path;
#[Field(type: 'varchar', length: 45, nullable: false, comment: '客户端 IP含 IPv6')]
#[Component(type: 'text', options: [])]
public $remote_addr;
#[Field(type: 'varchar', length: 100, nullable: true, comment: 'HTTP 认证用户名')]
#[Component(type: 'text', options: [])]
public $remote_user;
#[Field(type: 'int', length: 11, nullable: false, unsigned: true, comment: '请求时间戳(业务时区)')]
#[Component(type: 'date', options: [])]
public $time_local;
#[Field(type: 'varchar', length: 10, nullable: true, comment: 'HTTP 方法')]
#[Component(type: 'text', options: [])]
public $method;
#[Field(type: 'varchar', length: 255, nullable: false, comment: '请求 URI超 255 截断)')]
#[Component(type: 'text', options: [])]
public $uri;
#[Field(type: 'varchar', length: 500, nullable: false, default: '', comment: '查询字符串')]
#[Component(type: 'text', options: [])]
public $query_string;
#[Field(type: 'varchar', length: 10, nullable: true, comment: 'HTTP 协议版本')]
#[Component(type: 'text', options: [])]
public $http_version;
#[Field(type: 'int', length: 11, default: '0', comment: 'HTTP 状态码')]
#[Component(type: 'text', options: [])]
public $status;
#[Field(type: 'bigint', default: '0', comment: '响应体字节数')]
#[Component(type: 'text', options: [])]
public $body_bytes_sent;
#[Field(type: 'varchar', length: 500, nullable: false, default: '', comment: 'Referer')]
#[Component(type: 'text', options: [])]
public $http_referer;
#[Field(type: 'varchar', length: 500, nullable: false, default: '', comment: 'User-Agent超 500 截断)')]
#[Component(type: 'textarea', options: [])]
public $http_user_agent;
#[Field(type: 'decimal', precision: 10, scale: 3, default: '0.000', comment: 'nginx 请求总耗时(秒)')]
#[Component(type: 'text', options: [])]
public $request_time;
#[Field(type: 'decimal', precision: 10, scale: 3, default: '0.000', comment: 'upstream 响应时间(秒)')]
#[Component(type: 'text', options: [])]
public $upstream_response_time;
#[Field(type: 'bigint', default: '0', comment: '总发送字节数(含 header')]
#[Component(type: 'text', options: [])]
public $bytes_sent;
#[Field(type: 'varchar', length: 50, nullable: true, comment: '国家GeoIP 预留)')]
#[Component(type: 'text', options: [])]
public $country;
#[Field(type: 'varchar', length: 50, nullable: true, comment: '省份GeoIP 预留)')]
#[Component(type: 'text', options: [])]
public $province;
#[Field(type: 'varchar', length: 50, nullable: true, comment: '城市GeoIP 预留)')]
#[Component(type: 'text', options: [])]
public $city;
#[Field(type: 'int', length: 11, unsigned: true, comment: '入库时间戳')]
#[Component(type: 'date', options: [])]
public $create_time;
}

View File

@@ -5,9 +5,10 @@ namespace app\admin\scheme;
use app\common\scheme\BaseScheme;
use app\common\scheme\attribute\Table;
use app\common\scheme\attribute\Field;
use app\common\scheme\attribute\Component;
use app\common\scheme\attribute\Index;
#[Table(name: 'nginx_log_position', comment: 'nginx 日志文件读取位置(增量读取状态)')]
#[Table(name: 'ul_nginx_log_position', comment: 'nginx 日志文件读取位置(增量读取状态)')]
#[Index(columns: ['file_path'], name: 'uniq_file_path', type: 'UNIQUE')]
class NginxLogPosition extends BaseScheme
{
@@ -15,29 +16,38 @@ class NginxLogPosition extends BaseScheme
public $id;
#[Field(type: 'varchar', length: 500, nullable: false, comment: '日志文件绝对路径')]
#[Component(type: 'text', options: [])]
public $file_path;
#[Field(type: 'bigint', default: '0', unsigned: true, comment: '文件 inode用于检测日志轮转')]
#[Component(type: 'text', options: [])]
public $inode;
#[Field(type: 'bigint', default: '0', unsigned: true, comment: '上次读取到的字节偏移')]
#[Component(type: 'text', options: [])]
public $offset;
#[Field(type: 'int', length: 11, default: '0', unsigned: true, comment: '上次读取时间戳')]
#[Component(type: 'date', options: [])]
public $last_read_time;
#[Field(type: 'varchar', length: 32, default: '', comment: '上次最后一行的 md5 哈希(容错校验)')]
#[Component(type: 'text', options: [])]
public $last_line_hash;
#[Field(type: 'int', length: 11, default: '0', unsigned: true, comment: '解析失败累计次数')]
#[Component(type: 'text', options: [])]
public $parse_fail_count;
#[Field(type: 'text', nullable: true, comment: '解析失败样本行(最多保留若干条)')]
#[Component(type: 'textarea', options: [])]
public $parse_fail_samples;
#[Field(type: 'int', length: 11, unsigned: true, comment: '创建时间戳')]
#[Component(type: 'date', options: [])]
public $create_time;
#[Field(type: 'int', length: 11, unsigned: true, comment: '更新时间戳')]
#[Component(type: 'date', options: [])]
public $update_time;
}

View File

@@ -0,0 +1,11 @@
var init = {
tableElem: '#currentTable',
tableRenderId: 'currentTableRenderId',
indexUrl: 'nginx.access_log/index',
addUrl: '',
editUrl: '',
readUrl: 'nginx.access_log/read',
deleteUrl: '',
exportUrl: '',
modifyUrl: '',
};

View File

@@ -0,0 +1,122 @@
<div class="layuimini-container">
<form id="app-form" class="layui-form layuimini-form">
<div class="layui-form-item">
<label class="layui-form-label">来源日志文件路径</label>
<div class="layui-input-block">
<input type="text" name="file_path" class="layui-input" lay-verify="required" placeholder="请输入来源日志文件路径" value="{$Request.param.file_path|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">客户端 IP IPv6</label>
<div class="layui-input-block">
<input type="text" name="remote_addr" class="layui-input" lay-verify="required" placeholder="请输入客户端 IP含 IPv6" value="{$Request.param.remote_addr|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">HTTP 认证用户名</label>
<div class="layui-input-block">
<input type="text" name="remote_user" class="layui-input" placeholder="请输入HTTP 认证用户名" value="{$Request.param.remote_user|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">请求时间戳(业务时区)</label>
<div class="layui-input-block">
<input type="text" name="time_local" data-date="" data-date-type="datetime" class="layui-input" lay-verify="required" placeholder="请输入请求时间戳(业务时区)" value="{$Request.param.time_local|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">HTTP 方法</label>
<div class="layui-input-block">
<input type="text" name="method" class="layui-input" placeholder="请输入HTTP 方法" value="{$Request.param.method|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">请求 URI 255 截断)</label>
<div class="layui-input-block">
<input type="text" name="uri" class="layui-input" lay-verify="required" placeholder="请输入请求 URI超 255 截断)" value="{$Request.param.uri|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">查询字符串</label>
<div class="layui-input-block">
<input type="text" name="query_string" class="layui-input" lay-verify="required" placeholder="请输入查询字符串" value="{$Request.param.query_string|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">HTTP 协议版本</label>
<div class="layui-input-block">
<input type="text" name="http_version" class="layui-input" placeholder="请输入HTTP 协议版本" value="{$Request.param.http_version|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">HTTP 状态码</label>
<div class="layui-input-block">
<input type="text" name="status" class="layui-input" placeholder="请输入HTTP 状态码" value="{$Request.param.status|default='0'}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">响应体字节数</label>
<div class="layui-input-block">
<input type="text" name="body_bytes_sent" class="layui-input" placeholder="请输入响应体字节数" value="{$Request.param.body_bytes_sent|default='0'}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">Referer</label>
<div class="layui-input-block">
<input type="text" name="http_referer" class="layui-input" lay-verify="required" placeholder="请输入Referer" value="{$Request.param.http_referer|default=''}">
</div>
</div>
<div class="layui-form-item layui-form-text">
<label class="layui-form-label">User-Agent 500 截断)</label>
<div class="layui-input-block">
<textarea name="http_user_agent" class="layui-textarea" lay-verify="required" placeholder="请输入User-Agent超 500 截断)">{$Request.param.http_user_agent|default=''}</textarea>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">nginx 请求总耗时</label>
<div class="layui-input-block">
<input type="text" name="request_time" class="layui-input" placeholder="请输入nginx 请求总耗时" value="{$Request.param.request_time|default='0.000'}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">upstream 响应时间</label>
<div class="layui-input-block">
<input type="text" name="upstream_response_time" class="layui-input" placeholder="请输入upstream 响应时间" value="{$Request.param.upstream_response_time|default='0.000'}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">总发送字节数(含 header</label>
<div class="layui-input-block">
<input type="text" name="bytes_sent" class="layui-input" placeholder="请输入总发送字节数(含 header" value="{$Request.param.bytes_sent|default='0'}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">国家GeoIP 预留)</label>
<div class="layui-input-block">
<input type="text" name="country" class="layui-input" placeholder="请输入国家GeoIP 预留)" value="{$Request.param.country|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">省份GeoIP 预留)</label>
<div class="layui-input-block">
<input type="text" name="province" class="layui-input" placeholder="请输入省份GeoIP 预留)" value="{$Request.param.province|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">城市GeoIP 预留)</label>
<div class="layui-input-block">
<input type="text" name="city" class="layui-input" placeholder="请输入城市GeoIP 预留)" value="{$Request.param.city|default=''}">
</div>
</div>
<div class="hr-line"></div>
<div class="layui-form-item text-center">
{notempty name='$Request.param.backTagId'}
<div class="layui-btn layui-btn-sm page-back-button" layuimini-content-href="{$Request.param.backTagId}" data-back="1">返回</div>
{/notempty}
<button type="submit" class="layui-btn layui-btn-normal layui-btn-sm" lay-submit>确认</button>
<button type="reset" class="layui-btn layui-btn-primary layui-btn-sm">重置</button>
</div>
</form>
</div>

View File

@@ -0,0 +1,3 @@
$(function(){
ua.listen();
})

View File

@@ -0,0 +1,122 @@
<div class="layuimini-container">
<form id="app-form" class="layui-form layuimini-form">
<div class="layui-form-item">
<label class="layui-form-label">来源日志文件路径</label>
<div class="layui-input-block">
<input type="text" name="file_path" class="layui-input" lay-verify="required" placeholder="请输入来源日志文件路径" value="{$row.file_path|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">客户端 IP IPv6</label>
<div class="layui-input-block">
<input type="text" name="remote_addr" class="layui-input" lay-verify="required" placeholder="请输入客户端 IP含 IPv6" value="{$row.remote_addr|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">HTTP 认证用户名</label>
<div class="layui-input-block">
<input type="text" name="remote_user" class="layui-input" placeholder="请输入HTTP 认证用户名" value="{$row.remote_user|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">请求时间戳(业务时区)</label>
<div class="layui-input-block">
<input type="text" name="time_local" data-date="" data-date-type="datetime" class="layui-input" lay-verify="required" placeholder="请输入请求时间戳(业务时区)" value="{$row.time_local|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">HTTP 方法</label>
<div class="layui-input-block">
<input type="text" name="method" class="layui-input" placeholder="请输入HTTP 方法" value="{$row.method|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">请求 URI 255 截断)</label>
<div class="layui-input-block">
<input type="text" name="uri" class="layui-input" lay-verify="required" placeholder="请输入请求 URI超 255 截断)" value="{$row.uri|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">查询字符串</label>
<div class="layui-input-block">
<input type="text" name="query_string" class="layui-input" lay-verify="required" placeholder="请输入查询字符串" value="{$row.query_string|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">HTTP 协议版本</label>
<div class="layui-input-block">
<input type="text" name="http_version" class="layui-input" placeholder="请输入HTTP 协议版本" value="{$row.http_version|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">HTTP 状态码</label>
<div class="layui-input-block">
<input type="text" name="status" class="layui-input" placeholder="请输入HTTP 状态码" value="{$row.status|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">响应体字节数</label>
<div class="layui-input-block">
<input type="text" name="body_bytes_sent" class="layui-input" placeholder="请输入响应体字节数" value="{$row.body_bytes_sent|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">Referer</label>
<div class="layui-input-block">
<input type="text" name="http_referer" class="layui-input" lay-verify="required" placeholder="请输入Referer" value="{$row.http_referer|default=''}">
</div>
</div>
<div class="layui-form-item layui-form-text">
<label class="layui-form-label">User-Agent 500 截断)</label>
<div class="layui-input-block">
<textarea name="http_user_agent" class="layui-textarea" lay-verify="required" placeholder="请输入User-Agent超 500 截断)">{$row.http_user_agent|raw|default=''}</textarea>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">nginx 请求总耗时</label>
<div class="layui-input-block">
<input type="text" name="request_time" class="layui-input" placeholder="请输入nginx 请求总耗时" value="{$row.request_time|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">upstream 响应时间</label>
<div class="layui-input-block">
<input type="text" name="upstream_response_time" class="layui-input" placeholder="请输入upstream 响应时间" value="{$row.upstream_response_time|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">总发送字节数(含 header</label>
<div class="layui-input-block">
<input type="text" name="bytes_sent" class="layui-input" placeholder="请输入总发送字节数(含 header" value="{$row.bytes_sent|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">国家GeoIP 预留)</label>
<div class="layui-input-block">
<input type="text" name="country" class="layui-input" placeholder="请输入国家GeoIP 预留)" value="{$row.country|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">省份GeoIP 预留)</label>
<div class="layui-input-block">
<input type="text" name="province" class="layui-input" placeholder="请输入省份GeoIP 预留)" value="{$row.province|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">城市GeoIP 预留)</label>
<div class="layui-input-block">
<input type="text" name="city" class="layui-input" placeholder="请输入城市GeoIP 预留)" value="{$row.city|default=''}">
</div>
</div>
<div class="hr-line"></div>
<div class="layui-form-item text-center">
{notempty name='$Request.param.backTagId'}
<div class="layui-btn layui-btn-sm page-back-button" layuimini-content-href="{$Request.param.backTagId}" data-back="1">返回</div>
{/notempty}
<button type="submit" class="layui-btn layui-btn-normal layui-btn-sm" lay-submit>确认</button>
<button type="reset" class="layui-btn layui-btn-primary layui-btn-sm">重置</button>
</div>
</form>
</div>

View File

@@ -0,0 +1,3 @@
$(function(){
ua.listen();
})

View File

@@ -0,0 +1,9 @@
<div class="layuimini-container">
<div class="layuimini-main">
<table id="currentTable" class="layui-table layui-hide"
data-auth-index="{:auth('nginx.access_log/index')}"
data-auth-read="{:auth('nginx.access_log/read')}"
lay-filter="currentTable">
</table>
</div>
</div>

View File

@@ -0,0 +1,48 @@
$(function(){
ua.table.render({
init: init,
toolbar: [],
defaultToolbar: ['filter'],
cols: [[
{type: 'checkbox'},
{field: 'time_local', title: '时间', width: 170, search: 'range', templet: ua.table.date},
{field: 'method', title: '方法', width: 80, search: 'select', selectList: {GET: 'GET', POST: 'POST', PUT: 'PUT', DELETE: 'DELETE', HEAD: 'HEAD', OPTIONS: 'OPTIONS', PATCH: 'PATCH'}},
{field: 'uri', title: 'URI', minWidth: 200, searchOp: '%*%'},
{field: 'status', title: '状态码', width: 90, search: 'select', selectList: {200: '200', 301: '301', 302: '302', 304: '304', 400: '400', 401: '401', 403: '403', 404: '404', 499: '499', 500: '500', 502: '502', 503: '503', 504: '504'}, templet: function(d) {
var s = parseInt(d.status);
var cls = 'layui-badge';
if (s >= 200 && s < 300) cls = 'layui-badge layui-bg-green';
else if (s >= 300 && s < 400) cls = 'layui-badge layui-bg-blue';
else if (s >= 400 && s < 500) cls = 'layui-badge layui-bg-orange';
else if (s >= 500) cls = 'layui-badge layui-bg-red';
return '<span class="' + cls + '">' + d.status + '</span>';
}},
{field: 'remote_addr', title: '客户端IP', width: 140, searchOp: '%*%'},
{field: 'request_time', title: '耗时(秒)', width: 100, search: 'number_limit', templet: function(d) {
var t = parseFloat(d.request_time);
if (t >= 1) return '<span class="layui-badge layui-bg-red">' + t + 's</span>';
if (t >= 0.3) return '<span class="layui-badge layui-bg-orange">' + t + 's</span>';
return t + 's';
}},
{field: 'body_bytes_sent', title: '响应字节', width: 100, search: 'number_limit'},
{field: 'http_user_agent', title: 'User-Agent', minWidth: 200, search: false},
{
width: 100, title: '操作', templet: ua.table.tool, fixed: 'right', operat: [
[{
class: 'layui-btn layui-btn-primary layui-btn-xs',
method: 'tab',
field: 'id',
text: '详情',
title: '查看详情',
auth: 'read',
url: init.readUrl,
icon: ''
}]
]
},
]],
});
ua.listen();
})

View File

@@ -0,0 +1,229 @@
<div class="layuimini-container detail-container">
<div class="layuimini-main">
<div class="layui-card detail-card">
<div class="layui-card-header detail-header">
<div class="layui-row">
<div class="layui-col-md9">
<h2 class="detail-title">#{$row.id} {$title}</h2>
<div class="detail-id">ID: {$row.id}</div>
</div>
<div class="layui-col-md3 text-right detail-actions">
<button class="layui-btn layui-btn-primary" layuimini-content-href="{$Request.param.backTagId}" data-back="1">返回</button>
<button class="layui-btn" onclick="location.href='{:url("edit", ["id" => $row.id])}'">编辑</button>
</div>
</div>
</div>
<div class="layui-card-body detail-content">
<div class="layui-row layui-col-space12">
<!-- 左侧主体内容 -->
<div class="layui-col-md8 detail-main">
<div class="detail-field-group">
<div class="detail-field-item">
<div class="detail-field-label">来源日志文件路径</div>
<div class="detail-field-value">
{notempty name="row.file_path"}
{$row.file_path}
{else/}
<span class="layui-text-em">暂无数据</span>
{/notempty}
</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">客户端 IP IPv6</div>
<div class="detail-field-value">
{notempty name="row.remote_addr"}
{$row.remote_addr}
{else/}
<span class="layui-text-em">暂无数据</span>
{/notempty}
</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">HTTP 认证用户名</div>
<div class="detail-field-value">
{notempty name="row.remote_user"}
{$row.remote_user}
{else/}
<span class="layui-text-em">暂无数据</span>
{/notempty}
</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">请求时间戳(业务时区)</div>
<div class="detail-field-value">
{notempty name="row.time_local"}
{$row.time_local|date="Y-m-d H:i:s"}
{else/}
<span class="layui-text-em">暂无数据</span>
{/notempty}
</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">HTTP 方法</div>
<div class="detail-field-value">
{notempty name="row.method"}
{$row.method}
{else/}
<span class="layui-text-em">暂无数据</span>
{/notempty}
</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">请求 URI 255 截断)</div>
<div class="detail-field-value">
{notempty name="row.uri"}
{$row.uri}
{else/}
<span class="layui-text-em">暂无数据</span>
{/notempty}
</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">查询字符串</div>
<div class="detail-field-value">
{notempty name="row.query_string"}
{$row.query_string}
{else/}
<span class="layui-text-em">暂无数据</span>
{/notempty}
</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">HTTP 协议版本</div>
<div class="detail-field-value">
{notempty name="row.http_version"}
{$row.http_version}
{else/}
<span class="layui-text-em">暂无数据</span>
{/notempty}
</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">响应体字节数</div>
<div class="detail-field-value">
{notempty name="row.body_bytes_sent"}
{$row.body_bytes_sent}
{else/}
<span class="layui-text-em">暂无数据</span>
{/notempty}
</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">Referer</div>
<div class="detail-field-value">
{notempty name="row.http_referer"}
{$row.http_referer}
{else/}
<span class="layui-text-em">暂无数据</span>
{/notempty}
</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">User-Agent 500 截断)</div>
<div class="detail-field-value" style="white-space: pre-wrap;">
{notempty name="row.http_user_agent"}
{$row.http_user_agent|raw}
{else/}
<span class="layui-text-em">暂无内容</span>
{/notempty}
</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">nginx 请求总耗时</div>
<div class="detail-field-value">
{notempty name="row.request_time"}
{$row.request_time}
{else/}
<span class="layui-text-em">暂无数据</span>
{/notempty}
</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">upstream 响应时间</div>
<div class="detail-field-value">
{notempty name="row.upstream_response_time"}
{$row.upstream_response_time}
{else/}
<span class="layui-text-em">暂无数据</span>
{/notempty}
</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">总发送字节数(含 header</div>
<div class="detail-field-value">
{notempty name="row.bytes_sent"}
{$row.bytes_sent}
{else/}
<span class="layui-text-em">暂无数据</span>
{/notempty}
</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">国家GeoIP 预留)</div>
<div class="detail-field-value">
{notempty name="row.country"}
{$row.country}
{else/}
<span class="layui-text-em">暂无数据</span>
{/notempty}
</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">省份GeoIP 预留)</div>
<div class="detail-field-value">
{notempty name="row.province"}
{$row.province}
{else/}
<span class="layui-text-em">暂无数据</span>
{/notempty}
</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">城市GeoIP 预留)</div>
<div class="detail-field-value">
{notempty name="row.city"}
{$row.city}
{else/}
<span class="layui-text-em">暂无数据</span>
{/notempty}
</div>
</div>
</div>
</div>
<!-- 右侧基础信息 -->
<div class="layui-col-md4 detail-side">
<h3 class="detail-side-title">基础信息</h3>
<div class="detail-field-group">
<div class="detail-field-item">
<div class="detail-field-label">ID</div>
<div class="detail-field-value">{$row.id}</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">HTTP 状态码</div>
<div class="detail-field-value">
{notempty name="row.status"}
{$row.status}
{else/}
<span class="layui-text-em">暂无数据</span>
{/notempty}
</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">入库时间戳</div>
<div class="detail-field-value">
{notempty name="row.create_time"}
{$row.create_time|date="Y-m-d H:i:s"}
{else/}
<span class="layui-text-em">暂无数据</span>
{/notempty}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,22 @@
$(function(){
// 删除数据
window.deleteData = function(id) {
layer.confirm('确定要删除这条数据吗?', {
icon: 3,
title: '提示'
}, function(index) {
$.post('{{:url("delete")}}', {id: id}, function(res) {
if (res.code == 0) {
layer.msg('删除成功', {icon: 1}, function() {
location.href = '{{:url("index")}}';
});
} else {
layer.msg(res.msg, {icon: 2});
}
}, 'json');
layer.close(index);
});
};
ua.listen();
})

View File

@@ -0,0 +1,11 @@
var init = {
tableElem: '#currentTable',
tableRenderId: 'currentTableRenderId',
indexUrl: 'nginx.log_position/index',
addUrl: '',
editUrl: '',
readUrl: 'nginx.log_position/read',
deleteUrl: '',
exportUrl: '',
modifyUrl: '',
};

View File

@@ -0,0 +1,56 @@
<div class="layuimini-container">
<form id="app-form" class="layui-form layuimini-form">
<div class="layui-form-item">
<label class="layui-form-label">日志文件绝对路径</label>
<div class="layui-input-block">
<input type="text" name="file_path" class="layui-input" lay-verify="required" placeholder="请输入日志文件绝对路径" value="{$Request.param.file_path|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">文件 inode用于检测日志轮转</label>
<div class="layui-input-block">
<input type="text" name="inode" class="layui-input" placeholder="请输入文件 inode用于检测日志轮转" value="{$Request.param.inode|default='0'}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">上次读取到的字节偏移</label>
<div class="layui-input-block">
<input type="text" name="offset" class="layui-input" placeholder="请输入上次读取到的字节偏移" value="{$Request.param.offset|default='0'}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">上次读取时间戳</label>
<div class="layui-input-block">
<input type="text" name="last_read_time" data-date="" data-date-type="datetime" class="layui-input" placeholder="请输入上次读取时间戳" value="{$Request.param.last_read_time|default='0'}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">上次最后一行的 md5 哈希(容错校验)</label>
<div class="layui-input-block">
<input type="text" name="last_line_hash" class="layui-input" placeholder="请输入上次最后一行的 md5 哈希(容错校验)" value="{$Request.param.last_line_hash|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">解析失败累计次数</label>
<div class="layui-input-block">
<input type="text" name="parse_fail_count" class="layui-input" placeholder="请输入解析失败累计次数" value="{$Request.param.parse_fail_count|default='0'}">
</div>
</div>
<div class="layui-form-item layui-form-text">
<label class="layui-form-label">解析失败样本行(最多保留若干条)</label>
<div class="layui-input-block">
<textarea name="parse_fail_samples" class="layui-textarea" placeholder="请输入解析失败样本行(最多保留若干条)">{$Request.param.parse_fail_samples|default=''}</textarea>
</div>
</div>
<div class="hr-line"></div>
<div class="layui-form-item text-center">
{notempty name='$Request.param.backTagId'}
<div class="layui-btn layui-btn-sm page-back-button" layuimini-content-href="{$Request.param.backTagId}" data-back="1">返回</div>
{/notempty}
<button type="submit" class="layui-btn layui-btn-normal layui-btn-sm" lay-submit>确认</button>
<button type="reset" class="layui-btn layui-btn-primary layui-btn-sm">重置</button>
</div>
</form>
</div>

View File

@@ -0,0 +1,3 @@
$(function(){
ua.listen();
})

View File

@@ -0,0 +1,56 @@
<div class="layuimini-container">
<form id="app-form" class="layui-form layuimini-form">
<div class="layui-form-item">
<label class="layui-form-label">日志文件绝对路径</label>
<div class="layui-input-block">
<input type="text" name="file_path" class="layui-input" lay-verify="required" placeholder="请输入日志文件绝对路径" value="{$row.file_path|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">文件 inode用于检测日志轮转</label>
<div class="layui-input-block">
<input type="text" name="inode" class="layui-input" placeholder="请输入文件 inode用于检测日志轮转" value="{$row.inode|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">上次读取到的字节偏移</label>
<div class="layui-input-block">
<input type="text" name="offset" class="layui-input" placeholder="请输入上次读取到的字节偏移" value="{$row.offset|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">上次读取时间戳</label>
<div class="layui-input-block">
<input type="text" name="last_read_time" data-date="" data-date-type="datetime" class="layui-input" placeholder="请输入上次读取时间戳" value="{$row.last_read_time|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">上次最后一行的 md5 哈希(容错校验)</label>
<div class="layui-input-block">
<input type="text" name="last_line_hash" class="layui-input" placeholder="请输入上次最后一行的 md5 哈希(容错校验)" value="{$row.last_line_hash|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">解析失败累计次数</label>
<div class="layui-input-block">
<input type="text" name="parse_fail_count" class="layui-input" placeholder="请输入解析失败累计次数" value="{$row.parse_fail_count|default=''}">
</div>
</div>
<div class="layui-form-item layui-form-text">
<label class="layui-form-label">解析失败样本行(最多保留若干条)</label>
<div class="layui-input-block">
<textarea name="parse_fail_samples" class="layui-textarea" placeholder="请输入解析失败样本行(最多保留若干条)">{$row.parse_fail_samples|raw|default=''}</textarea>
</div>
</div>
<div class="hr-line"></div>
<div class="layui-form-item text-center">
{notempty name='$Request.param.backTagId'}
<div class="layui-btn layui-btn-sm page-back-button" layuimini-content-href="{$Request.param.backTagId}" data-back="1">返回</div>
{/notempty}
<button type="submit" class="layui-btn layui-btn-normal layui-btn-sm" lay-submit>确认</button>
<button type="reset" class="layui-btn layui-btn-primary layui-btn-sm">重置</button>
</div>
</form>
</div>

View File

@@ -0,0 +1,3 @@
$(function(){
ua.listen();
})

View File

@@ -0,0 +1,9 @@
<div class="layuimini-container">
<div class="layuimini-main">
<table id="currentTable" class="layui-table layui-hide"
data-auth-index="{:auth('nginx.log_position/index')}"
data-auth-read="{:auth('nginx.log_position/read')}"
lay-filter="currentTable">
</table>
</div>
</div>

View File

@@ -0,0 +1,41 @@
$(function(){
ua.table.render({
init: init,
toolbar: [],
defaultToolbar: ['filter'],
cols: [[
{type: 'checkbox'},
{field: 'id', title: 'ID', width: 80},
{field: 'file_path', title: '文件路径', minWidth: 250},
{field: 'inode', title: 'inode', width: 120},
{field: 'offset', title: '偏移量', width: 120},
{field: 'last_read_time', title: '最后读取时间', width: 170, search: 'range', templet: ua.table.date},
{field: 'last_line_hash', title: '最后行哈希', width: 160, search: false},
{field: 'parse_fail_count', title: '失败次数', width: 100, search: 'number_limit', templet: function(d) {
var c = parseInt(d.parse_fail_count);
if (c > 0) return '<span class="layui-badge layui-bg-red">' + c + '</span>';
return '<span class="layui-badge layui-bg-green">0</span>';
}},
{field: 'parse_fail_samples', title: '失败样本', minWidth: 200, search: false},
{field: 'create_time', title: '创建时间', width: 170, search: 'range', templet: ua.table.date},
{field: 'update_time', title: '更新时间', width: 170, search: false, templet: ua.table.date},
{
width: 100, title: '操作', templet: ua.table.tool, fixed: 'right', operat: [
[{
class: 'layui-btn layui-btn-primary layui-btn-xs',
method: 'tab',
field: 'id',
text: '详情',
title: '查看详情',
auth: 'read',
url: init.readUrl,
icon: ''
}]
]
},
]],
});
ua.listen();
})

View File

@@ -0,0 +1,119 @@
<div class="layuimini-container detail-container">
<div class="layuimini-main">
<div class="layui-card detail-card">
<div class="layui-card-header detail-header">
<div class="layui-row">
<div class="layui-col-md9">
<h2 class="detail-title">#{$row.id} {$title}</h2>
<div class="detail-id">ID: {$row.id}</div>
</div>
<div class="layui-col-md3 text-right detail-actions">
<button class="layui-btn layui-btn-primary" layuimini-content-href="{$Request.param.backTagId}" data-back="1">返回</button>
<button class="layui-btn" onclick="location.href='{:url("edit", ["id" => $row.id])}'">编辑</button>
</div>
</div>
</div>
<div class="layui-card-body detail-content">
<div class="layui-row layui-col-space12">
<!-- 左侧主体内容 -->
<div class="layui-col-md8 detail-main">
<div class="detail-field-group">
<div class="detail-field-item">
<div class="detail-field-label">日志文件绝对路径</div>
<div class="detail-field-value">
{notempty name="row.file_path"}
{$row.file_path}
{else/}
<span class="layui-text-em">暂无数据</span>
{/notempty}
</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">文件 inode用于检测日志轮转</div>
<div class="detail-field-value">
{notempty name="row.inode"}
{$row.inode}
{else/}
<span class="layui-text-em">暂无数据</span>
{/notempty}
</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">上次读取到的字节偏移</div>
<div class="detail-field-value">
{notempty name="row.offset"}
{$row.offset}
{else/}
<span class="layui-text-em">暂无数据</span>
{/notempty}
</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">上次读取时间戳</div>
<div class="detail-field-value">
{notempty name="row.last_read_time"}
{$row.last_read_time|date="Y-m-d H:i:s"}
{else/}
<span class="layui-text-em">暂无数据</span>
{/notempty}
</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">上次最后一行的 md5 哈希(容错校验)</div>
<div class="detail-field-value">
{notempty name="row.last_line_hash"}
{$row.last_line_hash}
{else/}
<span class="layui-text-em">暂无数据</span>
{/notempty}
</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">解析失败累计次数</div>
<div class="detail-field-value">
{notempty name="row.parse_fail_count"}
{$row.parse_fail_count}
{else/}
<span class="layui-text-em">暂无数据</span>
{/notempty}
</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">解析失败样本行(最多保留若干条)</div>
<div class="detail-field-value" style="white-space: pre-wrap;">
{notempty name="row.parse_fail_samples"}
{$row.parse_fail_samples|raw}
{else/}
<span class="layui-text-em">暂无内容</span>
{/notempty}
</div>
</div>
</div>
</div>
<!-- 右侧基础信息 -->
<div class="layui-col-md4 detail-side">
<h3 class="detail-side-title">基础信息</h3>
<div class="detail-field-group">
<div class="detail-field-item">
<div class="detail-field-label">ID</div>
<div class="detail-field-value">{$row.id}</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">创建时间戳</div>
<div class="detail-field-value">
{notempty name="row.create_time"}
{$row.create_time|date="Y-m-d H:i:s"}
{else/}
<span class="layui-text-em">暂无数据</span>
{/notempty}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,22 @@
$(function(){
// 删除数据
window.deleteData = function(id) {
layer.confirm('确定要删除这条数据吗?', {
icon: 3,
title: '提示'
}, function(index) {
$.post('{{:url("delete")}}', {id: id}, function(res) {
if (res.code == 0) {
layer.msg('删除成功', {icon: 1}, function() {
location.href = '{{:url("index")}}';
});
} else {
layer.msg(res.msg, {icon: 2});
}
}, 'json');
layer.close(index);
});
};
ua.listen();
})

View File

@@ -0,0 +1,106 @@
<?php
namespace base\admin\controller\nginx;
use app\common\controller\AdminController;
use app\admin\service\annotation\ControllerAnnotation;
use app\admin\service\annotation\NodeAnotation;
use think\App;
/**
* @ControllerAnnotation(title="nginx 访问日志明细(解析入库)")
*/
class AccessLogBase extends AdminController
{
use \app\admin\traits\Curd;
protected $sort = [
'time_local' => 'desc',
];
public function __construct(App $app)
{
parent::__construct($app);
$this->model = new \app\admin\model\NginxAccessLog();
}
/**
* @NodeAnotation(title="列表")
*/
public function index()
{
if ($this->request->isAjax()) {
if (input('selectFields')) {
return $this->selectList();
}
list($page, $limit, $where, $excludes, $request_options, $group) = $this->buildTableParames();
$count = $this->model
->where($where)
->group($group)
->count();
$list = $this->model
->where($where)
->page($page, $limit)
->order($this->sort)
->group($group)
->select();
$data = [
'code' => 0,
'msg' => '',
'count' => $count,
'data' => $list,
];
return json($data);
}
return $this->fetch();
}
/**
* @NodeAnotation(title="详情")
*/
public function read($id)
{
$row = $this->model->find($id);
empty($row) && $this->error('数据不存在');
$this->assign('row', $row);
return $this->fetch();
}
/**
* @NodeAnotation(title="添加")
*/
public function add()
{
$this->error('日志表不允许手动添加');
}
/**
* @NodeAnotation(title="编辑")
*/
public function edit($id)
{
$this->error('日志表不允许编辑');
}
/**
* @NodeAnotation(title="删除")
*/
public function delete($id)
{
$this->error('日志表不允许删除');
}
/**
* @NodeAnotation(title="属性修改")
*/
public function modify()
{
$this->error('日志表不允许修改');
}
}

View File

@@ -0,0 +1,106 @@
<?php
namespace base\admin\controller\nginx;
use app\common\controller\AdminController;
use app\admin\service\annotation\ControllerAnnotation;
use app\admin\service\annotation\NodeAnotation;
use think\App;
/**
* @ControllerAnnotation(title="nginx 日志文件读取位置(增量读取状态)")
*/
class LogPositionBase extends AdminController
{
use \app\admin\traits\Curd;
protected $sort = [
'id' => 'desc',
];
public function __construct(App $app)
{
parent::__construct($app);
$this->model = new \app\admin\model\NginxLogPosition();
}
/**
* @NodeAnotation(title="列表")
*/
public function index()
{
if ($this->request->isAjax()) {
if (input('selectFields')) {
return $this->selectList();
}
list($page, $limit, $where, $excludes, $request_options, $group) = $this->buildTableParames();
$count = $this->model
->where($where)
->group($group)
->count();
$list = $this->model
->where($where)
->page($page, $limit)
->order($this->sort)
->group($group)
->select();
$data = [
'code' => 0,
'msg' => '',
'count' => $count,
'data' => $list,
];
return json($data);
}
return $this->fetch();
}
/**
* @NodeAnotation(title="详情")
*/
public function read($id)
{
$row = $this->model->find($id);
empty($row) && $this->error('数据不存在');
$this->assign('row', $row);
return $this->fetch();
}
/**
* @NodeAnotation(title="添加")
*/
public function add()
{
$this->error('日志表不允许手动添加');
}
/**
* @NodeAnotation(title="编辑")
*/
public function edit($id)
{
$this->error('日志表不允许编辑');
}
/**
* @NodeAnotation(title="删除")
*/
public function delete($id)
{
$this->error('日志表不允许删除');
}
/**
* @NodeAnotation(title="属性修改")
*/
public function modify()
{
$this->error('日志表不允许修改');
}
}