mirror of
https://gitee.com/ulthon/ulthon_admin.git
synced 2026-08-30 12:45:32 +08:00
feat(admin): XHProf 函数监控规则与命中记录
This commit is contained in:
84
app/admin/controller/xhprof/RunWatch.php
Normal file
84
app/admin/controller/xhprof/RunWatch.php
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\admin\controller\xhprof;
|
||||||
|
|
||||||
|
use app\common\controller\AdminController;
|
||||||
|
use app\admin\service\annotation\ControllerAnnotation;
|
||||||
|
use app\admin\service\annotation\NodeAnotation;
|
||||||
|
use think\App;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* XHProf 监控规则命中记录(只读)
|
||||||
|
* 数据由导入定时任务物化写入,后台仅查看,不支持增删改。
|
||||||
|
* 页面零计算:耗时换算与阈值比对均在前端展示层完成。
|
||||||
|
*
|
||||||
|
* @ControllerAnnotation(title="XHProf 命中记录")
|
||||||
|
*/
|
||||||
|
class RunWatch extends AdminController
|
||||||
|
{
|
||||||
|
use \app\admin\traits\Curd;
|
||||||
|
|
||||||
|
protected $relationSearch = true;
|
||||||
|
|
||||||
|
public function __construct(App $app)
|
||||||
|
{
|
||||||
|
parent::__construct($app);
|
||||||
|
|
||||||
|
$this->model = new \app\admin\model\XhprofRunWatch();
|
||||||
|
|
||||||
|
// 命中记录的规则下拉(含禁用规则,便于回溯历史命中)
|
||||||
|
$this->assign('select_list_watch', \app\admin\model\XhprofWatch::column('name', 'id'), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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
|
||||||
|
->withJoin(['run', 'watch'], 'LEFT')
|
||||||
|
->where($where)
|
||||||
|
->group($group)
|
||||||
|
->count();
|
||||||
|
$list = $this->model
|
||||||
|
->withJoin(['run', 'watch'], 'LEFT')
|
||||||
|
->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->withJoin(['run', 'watch'], 'LEFT')->find($id);
|
||||||
|
empty($row) && $this->error('数据不存在');
|
||||||
|
|
||||||
|
// 获取模型的标题(表注释)
|
||||||
|
$title = $row->title;
|
||||||
|
|
||||||
|
$this->assign('row', $row);
|
||||||
|
$this->assign('title', $title);
|
||||||
|
|
||||||
|
return $this->fetch();
|
||||||
|
}
|
||||||
|
}
|
||||||
72
app/admin/controller/xhprof/Watch.php
Normal file
72
app/admin/controller/xhprof/Watch.php
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\admin\controller\xhprof;
|
||||||
|
|
||||||
|
use app\common\controller\AdminController;
|
||||||
|
use app\admin\service\annotation\ControllerAnnotation;
|
||||||
|
use app\admin\service\annotation\NodeAnotation;
|
||||||
|
use think\App;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* XHProf 函数监控规则管理
|
||||||
|
* regex 为 PCRE(对被调用函数名匹配),保存前校验编译合法性,
|
||||||
|
* 防止非法正则打爆导入任务的规则编译。
|
||||||
|
*
|
||||||
|
* @ControllerAnnotation(title="XHProf 函数监控规则")
|
||||||
|
*/
|
||||||
|
class Watch extends AdminController
|
||||||
|
{
|
||||||
|
use \app\admin\traits\Curd {
|
||||||
|
\app\admin\traits\Curd::add as curdAdd;
|
||||||
|
\app\admin\traits\Curd::edit as curdEdit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __construct(App $app)
|
||||||
|
{
|
||||||
|
parent::__construct($app);
|
||||||
|
|
||||||
|
$this->model = new \app\admin\model\XhprofWatch();
|
||||||
|
|
||||||
|
$this->assign('select_list_status', $this->model::SELECT_LIST_STATUS, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @NodeAnotation(title="添加")
|
||||||
|
*/
|
||||||
|
public function add()
|
||||||
|
{
|
||||||
|
if ($this->request->isPost()) {
|
||||||
|
$this->validateWatchRegex((string) $this->request->post('regex', ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->curdAdd();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @NodeAnotation(title="编辑")
|
||||||
|
*/
|
||||||
|
public function edit($id)
|
||||||
|
{
|
||||||
|
if ($this->request->isPost()) {
|
||||||
|
$this->validateWatchRegex((string) $this->request->post('regex', ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->curdEdit($id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验 PCRE 正则合法性(与导入任务 compileWatchRegexes 同一判据).
|
||||||
|
*/
|
||||||
|
protected function validateWatchRegex(string $regex): void
|
||||||
|
{
|
||||||
|
if ($regex === '') {
|
||||||
|
$this->error('正则不能为空');
|
||||||
|
}
|
||||||
|
error_clear_last();
|
||||||
|
if (@preg_match($regex, '') === false) {
|
||||||
|
$error = error_get_last();
|
||||||
|
$message = $error !== null ? $error['message'] : preg_last_error_msg();
|
||||||
|
$this->error('正则非法(必须带定界符,如 ~think\\\\db\\\\.*~):' . $message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,8 @@ namespace app\admin\model;
|
|||||||
use app\common\model\TimeModel;
|
use app\common\model\TimeModel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* XHProf 采样与监控规则命中聚合(导入时物化)
|
||||||
|
*
|
||||||
* @property int $id 主键ID
|
* @property int $id 主键ID
|
||||||
* @property int $run_id 采样记录ID(ul_xhprof_run.id)
|
* @property int $run_id 采样记录ID(ul_xhprof_run.id)
|
||||||
* @property int $watch_id 监控规则ID(ul_xhprof_watch.id)
|
* @property int $watch_id 监控规则ID(ul_xhprof_watch.id)
|
||||||
@@ -20,4 +22,13 @@ class XhprofRunWatch extends TimeModel
|
|||||||
|
|
||||||
protected $deleteTime = false;
|
protected $deleteTime = false;
|
||||||
|
|
||||||
|
public function run()
|
||||||
|
{
|
||||||
|
return $this->belongsTo('app\admin\model\XhprofRun', 'run_id', 'id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function watch()
|
||||||
|
{
|
||||||
|
return $this->belongsTo('app\admin\model\XhprofWatch', 'watch_id', 'id');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ namespace app\admin\model;
|
|||||||
use app\common\model\TimeModel;
|
use app\common\model\TimeModel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* XHProf 函数监控规则
|
||||||
|
*
|
||||||
* @property int $id 主键ID
|
* @property int $id 主键ID
|
||||||
* @property string $name 规则名称(人读)
|
* @property string $name 规则名称(人读)
|
||||||
* @property string $regex PCRE 正则(对被调用函数名匹配)
|
* @property string $regex PCRE 正则(对被调用函数名匹配,带定界符)
|
||||||
* @property string $note 备注
|
* @property string $note 备注
|
||||||
* @property int $threshold_ms 比对阈值(毫秒),0=不比对
|
* @property int $threshold_ms 比对阈值(毫秒),0=不比对
|
||||||
* @property int $status 状态:1=启用,0=禁用
|
* @property int $status 状态:1=启用,0=禁用
|
||||||
@@ -21,4 +23,5 @@ class XhprofWatch extends TimeModel
|
|||||||
|
|
||||||
protected $deleteTime = false;
|
protected $deleteTime = false;
|
||||||
|
|
||||||
|
public const SELECT_LIST_STATUS = ['0' => '禁用', '1' => '启用'];
|
||||||
}
|
}
|
||||||
|
|||||||
11
app/admin/view/xhprof/run_watch/_common.js
Normal file
11
app/admin/view/xhprof/run_watch/_common.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
var init = {
|
||||||
|
tableElem: '#currentTable',
|
||||||
|
tableRenderId: 'currentTableRenderId',
|
||||||
|
indexUrl: 'xhprof.run_watch/index',
|
||||||
|
addUrl: '',
|
||||||
|
editUrl: '',
|
||||||
|
readUrl: 'xhprof.run_watch/read',
|
||||||
|
deleteUrl: '',
|
||||||
|
exportUrl: '',
|
||||||
|
modifyUrl: '',
|
||||||
|
};
|
||||||
44
app/admin/view/xhprof/run_watch/add.html
Normal file
44
app/admin/view/xhprof/run_watch/add.html
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<div class="layuimini-container">
|
||||||
|
<form id="app-form" class="layui-form layuimini-form">
|
||||||
|
|
||||||
|
<div class="layui-form-item">
|
||||||
|
<label class="layui-form-label">采样记录ID(ul_xhprof_run.id)</label>
|
||||||
|
<div class="layui-input-block">
|
||||||
|
<input type="text" name="run_id" class="layui-input" placeholder="请输入采样记录ID(ul_xhprof_run.id)" value="{$Request.param.run_id|default='0'}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="layui-form-item">
|
||||||
|
<label class="layui-form-label">监控规则ID(ul_xhprof_watch.id)</label>
|
||||||
|
<div class="layui-input-block">
|
||||||
|
<input type="text" name="watch_id" class="layui-input" placeholder="请输入监控规则ID(ul_xhprof_watch.id)" value="{$Request.param.watch_id|default='0'}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="layui-form-item">
|
||||||
|
<label class="layui-form-label">命中边次数和</label>
|
||||||
|
<div class="layui-input-block">
|
||||||
|
<input type="text" name="ct" class="layui-input" placeholder="请输入命中边次数和" value="{$Request.param.ct|default='0'}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="layui-form-item">
|
||||||
|
<label class="layui-form-label">命中耗时合计(微秒)</label>
|
||||||
|
<div class="layui-input-block">
|
||||||
|
<input type="text" name="wt_sum" class="layui-input" placeholder="请输入命中耗时合计(微秒)" value="{$Request.param.wt_sum|default='0'}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="layui-form-item">
|
||||||
|
<label class="layui-form-label">命中单次最大耗时(微秒)</label>
|
||||||
|
<div class="layui-input-block">
|
||||||
|
<input type="text" name="wt_max" class="layui-input" placeholder="请输入命中单次最大耗时(微秒)" value="{$Request.param.wt_max|default='0'}">
|
||||||
|
</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>
|
||||||
3
app/admin/view/xhprof/run_watch/add.js
Normal file
3
app/admin/view/xhprof/run_watch/add.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
$(function(){
|
||||||
|
ua.listen();
|
||||||
|
})
|
||||||
44
app/admin/view/xhprof/run_watch/edit.html
Normal file
44
app/admin/view/xhprof/run_watch/edit.html
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<div class="layuimini-container">
|
||||||
|
<form id="app-form" class="layui-form layuimini-form">
|
||||||
|
|
||||||
|
<div class="layui-form-item">
|
||||||
|
<label class="layui-form-label">采样记录ID(ul_xhprof_run.id)</label>
|
||||||
|
<div class="layui-input-block">
|
||||||
|
<input type="text" name="run_id" class="layui-input" placeholder="请输入采样记录ID(ul_xhprof_run.id)" value="{$row.run_id|default=''}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="layui-form-item">
|
||||||
|
<label class="layui-form-label">监控规则ID(ul_xhprof_watch.id)</label>
|
||||||
|
<div class="layui-input-block">
|
||||||
|
<input type="text" name="watch_id" class="layui-input" placeholder="请输入监控规则ID(ul_xhprof_watch.id)" value="{$row.watch_id|default=''}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="layui-form-item">
|
||||||
|
<label class="layui-form-label">命中边次数和</label>
|
||||||
|
<div class="layui-input-block">
|
||||||
|
<input type="text" name="ct" class="layui-input" placeholder="请输入命中边次数和" value="{$row.ct|default=''}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="layui-form-item">
|
||||||
|
<label class="layui-form-label">命中耗时合计(微秒)</label>
|
||||||
|
<div class="layui-input-block">
|
||||||
|
<input type="text" name="wt_sum" class="layui-input" placeholder="请输入命中耗时合计(微秒)" value="{$row.wt_sum|default=''}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="layui-form-item">
|
||||||
|
<label class="layui-form-label">命中单次最大耗时(微秒)</label>
|
||||||
|
<div class="layui-input-block">
|
||||||
|
<input type="text" name="wt_max" class="layui-input" placeholder="请输入命中单次最大耗时(微秒)" value="{$row.wt_max|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>
|
||||||
3
app/admin/view/xhprof/run_watch/edit.js
Normal file
3
app/admin/view/xhprof/run_watch/edit.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
$(function(){
|
||||||
|
ua.listen();
|
||||||
|
})
|
||||||
9
app/admin/view/xhprof/run_watch/index.html
Normal file
9
app/admin/view/xhprof/run_watch/index.html
Normal 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('xhprof.run_watch/index')}"
|
||||||
|
data-auth-read="{:auth('xhprof.run_watch/read')}"
|
||||||
|
lay-filter="currentTable">
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
62
app/admin/view/xhprof/run_watch/index.js
Normal file
62
app/admin/view/xhprof/run_watch/index.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
$(function(){
|
||||||
|
ua.table.render({
|
||||||
|
init: init,
|
||||||
|
toolbar: [],
|
||||||
|
defaultToolbar: ['filter'],
|
||||||
|
cols: [[
|
||||||
|
{field: 'id', title: 'ID', width: 80, sort: true},
|
||||||
|
{field: 'watch_id', title: '监控规则', width: 160, search: 'select', searchOp: '=', selectList: ua.getDataBrage('select_list_watch'), templet: function(d) {
|
||||||
|
return d.watch && d.watch.name ? d.watch.name : ('#' + d.watch_id);
|
||||||
|
}},
|
||||||
|
{field: 'run.url', title: '请求 URL', minWidth: 220, search: false, templet: function(d) {
|
||||||
|
if (!d.run || !d.run.url) {
|
||||||
|
return '<span class="layui-text-em">#' + d.run_id + '</span>';
|
||||||
|
}
|
||||||
|
var esc = String(d.run.url).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||||
|
return '<span title="' + esc + '">' + esc + '</span>';
|
||||||
|
}},
|
||||||
|
{field: 'run.request_time', title: '请求时间', width: 170, search: 'range', templet: function(d) {
|
||||||
|
if (!d.run || !d.run.request_time) {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
var ts = parseFloat(d.run.request_time);
|
||||||
|
if (isNaN(ts) || ts <= 0) {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
return window.UA_CORE.util.toDateString(ts * 1000, 'yyyy-MM-dd HH:mm:ss');
|
||||||
|
}},
|
||||||
|
{field: 'ct', title: '命中次数', width: 100, search: 'number_limit'},
|
||||||
|
{field: 'wt_sum', title: '耗时合计(ms)', width: 140, search: 'number_limit', templet: function(d) {
|
||||||
|
var ms = parseFloat(d.wt_sum) / 1000;
|
||||||
|
var threshold = d.watch ? parseInt(d.watch.threshold_ms, 10) : 0;
|
||||||
|
var text = (isNaN(ms) ? 0 : ms).toFixed(3) + ' ms';
|
||||||
|
if (threshold > 0 && ms > threshold) {
|
||||||
|
return '<span class="layui-badge layui-bg-red">' + text + '(超阈值 ' + threshold + 'ms)</span>';
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}},
|
||||||
|
{field: 'wt_max', title: '单次最大(ms)', width: 130, search: 'number_limit', templet: function(d) {
|
||||||
|
var ms = parseFloat(d.wt_max) / 1000;
|
||||||
|
return (isNaN(ms) ? 0 : ms).toFixed(3) + ' ms';
|
||||||
|
}},
|
||||||
|
{field: 'create_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();
|
||||||
|
})
|
||||||
97
app/admin/view/xhprof/run_watch/read.html
Normal file
97
app/admin/view/xhprof/run_watch/read.html
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
<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>
|
||||||
|
</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.watch.name"}
|
||||||
|
{$row.watch.name}(#{$row.watch_id})
|
||||||
|
{else/}
|
||||||
|
<span class="layui-text-em">#{$row.watch_id}</span>
|
||||||
|
{/notempty}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-field-item">
|
||||||
|
<div class="detail-field-label">命中次数(边数和)</div>
|
||||||
|
<div class="detail-field-value">
|
||||||
|
{$row.ct}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-field-item">
|
||||||
|
<div class="detail-field-label">耗时合计</div>
|
||||||
|
<div class="detail-field-value">
|
||||||
|
{$row.wt_sum} μs = {:number_format($row['wt_sum'] / 1000, 3)} ms
|
||||||
|
{if condition="$row['watch']['threshold_ms'] > 0 && ($row['wt_sum'] / 1000) > $row['watch']['threshold_ms']"}
|
||||||
|
<span class="layui-badge layui-bg-red">超阈值 {$row.watch.threshold_ms}ms</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-field-item">
|
||||||
|
<div class="detail-field-label">单次最大耗时</div>
|
||||||
|
<div class="detail-field-value">
|
||||||
|
{$row.wt_max} μs = {:number_format($row['wt_max'] / 1000, 3)} ms
|
||||||
|
</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.run_id}</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-field-item">
|
||||||
|
<div class="detail-field-label">请求 URL</div>
|
||||||
|
<div class="detail-field-value">
|
||||||
|
{notempty name="row.run.url"}
|
||||||
|
{$row.run.url}
|
||||||
|
{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.run.request_time"}
|
||||||
|
{:date('Y-m-d H:i:s', intval($row['run']['request_time']))}
|
||||||
|
{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>
|
||||||
22
app/admin/view/xhprof/run_watch/read.js
Normal file
22
app/admin/view/xhprof/run_watch/read.js
Normal 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();
|
||||||
|
})
|
||||||
11
app/admin/view/xhprof/watch/_common.js
Normal file
11
app/admin/view/xhprof/watch/_common.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
var init = {
|
||||||
|
tableElem: '#currentTable',
|
||||||
|
tableRenderId: 'currentTableRenderId',
|
||||||
|
indexUrl: 'xhprof.watch/index',
|
||||||
|
addUrl: 'xhprof.watch/add' + location.search,
|
||||||
|
editUrl: 'xhprof.watch/edit',
|
||||||
|
readUrl: 'xhprof.watch/read',
|
||||||
|
deleteUrl: 'xhprof.watch/delete',
|
||||||
|
exportUrl: '',
|
||||||
|
modifyUrl: 'xhprof.watch/modify',
|
||||||
|
};
|
||||||
48
app/admin/view/xhprof/watch/add.html
Normal file
48
app/admin/view/xhprof/watch/add.html
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<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="name" class="layui-input" lay-verify="required" placeholder="请输入规则名称(人读),如:SQL 执行层" value="{$Request.param.name|default=''}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="layui-form-item">
|
||||||
|
<label class="layui-form-label">正则表达式</label>
|
||||||
|
<div class="layui-input-block">
|
||||||
|
<input type="text" name="regex" class="layui-input" lay-verify="required|watchRegex" placeholder="必须带 PCRE 定界符,如 ~think\\db\\.*~(对被调用函数名匹配)" value="{$Request.param.regex|default=''}">
|
||||||
|
<div class="layui-form-mid layui-word-aux">对采样内被调用函数名匹配;必须带定界符(推荐 ~...~),无定界符或非法正则将被拒绝保存。</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="layui-form-item">
|
||||||
|
<label class="layui-form-label">备注</label>
|
||||||
|
<div class="layui-input-block">
|
||||||
|
<input type="text" name="note" class="layui-input" placeholder="备注(可选)" value="{$Request.param.note|default=''}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="layui-form-item">
|
||||||
|
<label class="layui-form-label">阈值(毫秒)</label>
|
||||||
|
<div class="layui-input-block">
|
||||||
|
<input type="number" name="threshold_ms" class="layui-input" lay-verify="number" placeholder="单位毫秒,0=不比对(命中记录页超阈值标红)" value="{$Request.param.threshold_ms|default='0'}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="layui-form-item">
|
||||||
|
<label class="layui-form-label">状态</label>
|
||||||
|
<div class="layui-input-block">
|
||||||
|
{foreach $select_list_status as $k=>$v}
|
||||||
|
<input type="radio" name="status" value="{$k}" title="{$v}" {in name="k" value="$Request.param.status"}checked=""{/in}>
|
||||||
|
{/foreach}
|
||||||
|
</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>
|
||||||
40
app/admin/view/xhprof/watch/add.js
Normal file
40
app/admin/view/xhprof/watch/add.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
$(function(){
|
||||||
|
// PCRE 正则前端预校验:必须带定界符(权威校验在后端 @preg_match)
|
||||||
|
var form = window.UA_CORE.form;
|
||||||
|
if (form) {
|
||||||
|
form.verify({
|
||||||
|
watchRegex: function (value) {
|
||||||
|
value = $.trim(value);
|
||||||
|
if (value === '') {
|
||||||
|
return '正则不能为空';
|
||||||
|
}
|
||||||
|
var delim = value.charAt(0);
|
||||||
|
if (/[a-zA-Z0-9\s\\]/.test(delim)) {
|
||||||
|
return '必须带 PCRE 定界符,如 ~think\\db\\.*~';
|
||||||
|
}
|
||||||
|
var body = value.substring(1);
|
||||||
|
var closeIdx = -1;
|
||||||
|
for (var i = 0; i < body.length; i++) {
|
||||||
|
var c = body.charAt(i);
|
||||||
|
if (c === '\\') {
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c === delim) {
|
||||||
|
closeIdx = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (closeIdx === -1) {
|
||||||
|
return '找不到闭合定界符 ' + delim;
|
||||||
|
}
|
||||||
|
var modifiers = body.substring(closeIdx + 1);
|
||||||
|
if (!/^[imsxADSUXJu]*$/.test(modifiers)) {
|
||||||
|
return '非法修饰符:' + modifiers;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ua.listen();
|
||||||
|
})
|
||||||
48
app/admin/view/xhprof/watch/edit.html
Normal file
48
app/admin/view/xhprof/watch/edit.html
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<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="name" class="layui-input" lay-verify="required" placeholder="请输入规则名称(人读)" value="{$row.name|default=''}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="layui-form-item">
|
||||||
|
<label class="layui-form-label">正则表达式</label>
|
||||||
|
<div class="layui-input-block">
|
||||||
|
<input type="text" name="regex" class="layui-input" lay-verify="required|watchRegex" placeholder="必须带 PCRE 定界符,如 ~think\\db\\.*~(对被调用函数名匹配)" value="{$row.regex|default=''}">
|
||||||
|
<div class="layui-form-mid layui-word-aux">对采样内被调用函数名匹配;必须带定界符(推荐 ~...~),无定界符或非法正则将被拒绝保存。</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="layui-form-item">
|
||||||
|
<label class="layui-form-label">备注</label>
|
||||||
|
<div class="layui-input-block">
|
||||||
|
<input type="text" name="note" class="layui-input" placeholder="备注(可选)" value="{$row.note|default=''}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="layui-form-item">
|
||||||
|
<label class="layui-form-label">阈值(毫秒)</label>
|
||||||
|
<div class="layui-input-block">
|
||||||
|
<input type="number" name="threshold_ms" class="layui-input" lay-verify="number" placeholder="单位毫秒,0=不比对(命中记录页超阈值标红)" value="{$row.threshold_ms|default='0'}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="layui-form-item">
|
||||||
|
<label class="layui-form-label">状态</label>
|
||||||
|
<div class="layui-input-block">
|
||||||
|
{foreach $select_list_status as $k=>$v}
|
||||||
|
<input type="radio" name="status" value="{$k}" title="{$v}" {in name="k" value="$row.status"}checked=""{/in}>
|
||||||
|
{/foreach}
|
||||||
|
</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>
|
||||||
40
app/admin/view/xhprof/watch/edit.js
Normal file
40
app/admin/view/xhprof/watch/edit.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
$(function(){
|
||||||
|
// PCRE 正则前端预校验:必须带定界符(权威校验在后端 @preg_match)
|
||||||
|
var form = window.UA_CORE.form;
|
||||||
|
if (form) {
|
||||||
|
form.verify({
|
||||||
|
watchRegex: function (value) {
|
||||||
|
value = $.trim(value);
|
||||||
|
if (value === '') {
|
||||||
|
return '正则不能为空';
|
||||||
|
}
|
||||||
|
var delim = value.charAt(0);
|
||||||
|
if (/[a-zA-Z0-9\s\\]/.test(delim)) {
|
||||||
|
return '必须带 PCRE 定界符,如 ~think\\db\\.*~';
|
||||||
|
}
|
||||||
|
var body = value.substring(1);
|
||||||
|
var closeIdx = -1;
|
||||||
|
for (var i = 0; i < body.length; i++) {
|
||||||
|
var c = body.charAt(i);
|
||||||
|
if (c === '\\') {
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c === delim) {
|
||||||
|
closeIdx = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (closeIdx === -1) {
|
||||||
|
return '找不到闭合定界符 ' + delim;
|
||||||
|
}
|
||||||
|
var modifiers = body.substring(closeIdx + 1);
|
||||||
|
if (!/^[imsxADSUXJu]*$/.test(modifiers)) {
|
||||||
|
return '非法修饰符:' + modifiers;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ua.listen();
|
||||||
|
})
|
||||||
12
app/admin/view/xhprof/watch/index.html
Normal file
12
app/admin/view/xhprof/watch/index.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<div class="layuimini-container">
|
||||||
|
<div class="layuimini-main">
|
||||||
|
<table id="currentTable" class="layui-table layui-hide"
|
||||||
|
data-auth-index="{:auth('xhprof.watch/index')}"
|
||||||
|
data-auth-add="{:auth('xhprof.watch/add')}"
|
||||||
|
data-auth-edit="{:auth('xhprof.watch/edit')}"
|
||||||
|
data-auth-delete="{:auth('xhprof.watch/delete')}"
|
||||||
|
data-auth-modify="{:auth('xhprof.watch/modify')}"
|
||||||
|
lay-filter="currentTable">
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
38
app/admin/view/xhprof/watch/index.js
Normal file
38
app/admin/view/xhprof/watch/index.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
$(function(){
|
||||||
|
ua.table.render({
|
||||||
|
init: init,
|
||||||
|
toolbar: ['add', 'delete'],
|
||||||
|
cols: [[
|
||||||
|
{type: 'checkbox'},
|
||||||
|
{field: 'id', title: 'ID', width: 80, sort: true},
|
||||||
|
{field: 'name', title: '规则名称', minWidth: 140},
|
||||||
|
{field: 'regex', title: '正则(匹配被调用函数名)', minWidth: 260, templet: function(d) {
|
||||||
|
var esc = String(d.regex == null ? '' : d.regex).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||||
|
return '<code style="font-size:12px;">' + esc + '</code>';
|
||||||
|
}},
|
||||||
|
{field: 'note', title: '备注', minWidth: 140},
|
||||||
|
{field: 'threshold_ms', title: '阈值(ms)', width: 100, search: 'number_limit'},
|
||||||
|
{field: 'status', title: '状态', width: 100, search: 'select', selectList: ua.getDataBrage('select_list_status'), templet: ua.table.switch},
|
||||||
|
{field: 'create_time', title: '创建时间', width: 170, search: false, templet: ua.table.date},
|
||||||
|
{
|
||||||
|
width: 200, 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: ''
|
||||||
|
}],
|
||||||
|
'edit',
|
||||||
|
'delete'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
]],
|
||||||
|
});
|
||||||
|
|
||||||
|
ua.listen();
|
||||||
|
})
|
||||||
95
app/admin/view/xhprof/watch/read.html
Normal file
95
app/admin/view/xhprof/watch/read.html
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
<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.name"}
|
||||||
|
{$row.name}
|
||||||
|
{else/}
|
||||||
|
<span class="layui-text-em">暂无数据</span>
|
||||||
|
{/notempty}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-field-item">
|
||||||
|
<div class="detail-field-label">PCRE 正则(对被调用函数名匹配)</div>
|
||||||
|
<div class="detail-field-value">
|
||||||
|
{notempty name="row.regex"}
|
||||||
|
{$row.regex}
|
||||||
|
{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.note"}
|
||||||
|
{$row.note}
|
||||||
|
{else/}
|
||||||
|
<span class="layui-text-em">暂无数据</span>
|
||||||
|
{/notempty}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-field-item">
|
||||||
|
<div class="detail-field-label">比对阈值(毫秒),0=不比对</div>
|
||||||
|
<div class="detail-field-value">
|
||||||
|
{notempty name="row.threshold_ms"}
|
||||||
|
{$row.threshold_ms}
|
||||||
|
{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">状态:1=启用,0=禁用</div>
|
||||||
|
<div class="detail-field-value">
|
||||||
|
<span class="layui-badge">{$select_list_status[$row.status]|default=''}</span>
|
||||||
|
</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>
|
||||||
22
app/admin/view/xhprof/watch/read.js
Normal file
22
app/admin/view/xhprof/watch/read.js
Normal 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();
|
||||||
|
})
|
||||||
81
database/migrations/20260814110001_xhprof_watch_seed.php
Normal file
81
database/migrations/20260814110001_xhprof_watch_seed.php
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use think\migration\Migrator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* XHProf 函数监控规则 - 预置三条规则
|
||||||
|
*
|
||||||
|
* regex 为 PCRE(带 ~...~ 定界符),导入任务物化时对被调用(callee)函数名匹配。
|
||||||
|
* 反斜杠形态说明:DB 存储值形如 ~think\\db\\PDOConnection::.*~(双反斜杠字符),
|
||||||
|
* PCRE 解析 \\ 为一个字面 \,与采样键 think\db\PDOConnection::xxx(命名空间单反斜杠)对齐。
|
||||||
|
* 已对运行环境真实采样验证:PDOConnection::(静态形态)、::save(如 think\Cookie::save)均有命中;
|
||||||
|
* app\admin\controller\.* 在后台页面流量下命中(控制器中心主义)。
|
||||||
|
*
|
||||||
|
* MySQL 字符串字面量会把 \\ 解析为单个 \,故写入前用 addcslashes 将每个字面 \ 加倍,
|
||||||
|
* 保证落库值为双反斜杠字符。
|
||||||
|
*
|
||||||
|
* 幂等:regex 有 UNIQUE 键,ON DUPLICATE KEY UPDATE id=id 使重复执行不产生新行、不覆盖用户改动。
|
||||||
|
*/
|
||||||
|
class XhprofWatchSeed extends Migrator
|
||||||
|
{
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
$now = time();
|
||||||
|
$rows = [
|
||||||
|
[
|
||||||
|
'name' => 'SQL 执行层',
|
||||||
|
// PHP 值:~think\\db\\PDOConnection::.*~(每个分隔位置两个反斜杠字符)
|
||||||
|
'regex' => '~think\\\\db\\\\PDOConnection::.*~',
|
||||||
|
'note' => 'PDO 连接层查询执行(真实形态实测)',
|
||||||
|
'threshold_ms' => 500,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => '业务层控制器',
|
||||||
|
'regex' => '~app\\\\admin\\\\controller\\\\.*~',
|
||||||
|
'note' => '控制器中心主义,业务逻辑所在',
|
||||||
|
'threshold_ms' => 1000,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => '数据保存操作',
|
||||||
|
'regex' => '~.*::save$~',
|
||||||
|
'note' => 'Model/log/session 的 save 调用',
|
||||||
|
'threshold_ms' => 0,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$sql = sprintf(
|
||||||
|
"INSERT INTO `%sxhprof_watch` (`name`, `regex`, `note`, `threshold_ms`, `status`, `create_time`, `update_time`) VALUES (%s, %s, %s, %d, 1, %d, %d) ON DUPLICATE KEY UPDATE `id` = `id`",
|
||||||
|
$this->getAdapter()->getOption('table_prefix'),
|
||||||
|
$this->quoteLiteral($row['name']),
|
||||||
|
$this->quoteLiteral($row['regex']),
|
||||||
|
$this->quoteLiteral($row['note']),
|
||||||
|
$row['threshold_ms'],
|
||||||
|
$now,
|
||||||
|
$now
|
||||||
|
);
|
||||||
|
$this->execute($sql);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
$prefix = $this->getAdapter()->getOption('table_prefix');
|
||||||
|
$regexes = implode(', ', array_map(function ($regex) {
|
||||||
|
return $this->quoteLiteral($regex);
|
||||||
|
}, [
|
||||||
|
'~think\\\\db\\\\PDOConnection::.*~',
|
||||||
|
'~app\\\\admin\\\\controller\\\\.*~',
|
||||||
|
'~.*::save$~',
|
||||||
|
]));
|
||||||
|
$this->execute("DELETE FROM `{$prefix}xhprof_watch` WHERE `regex` IN ({$regexes})");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MySQL 字面量包装:单引号包裹 + 反斜杠转义(保证落库值与 PHP 值逐字符一致).
|
||||||
|
*/
|
||||||
|
private function quoteLiteral(string $value): string
|
||||||
|
{
|
||||||
|
return "'" . addcslashes($value, "\\") . "'";
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user