Egg上层框架CabloyJS是如何输出SQL语句日志的?
背景
在Egg开发实践中,经常会遇到一个问题:如何查看刚刚执行过的Egg组装的原生SQL语句呢?
1. 现有方案
可以直接在项目的config配置文件中添加MySQL配置debug: true。这会启用底层模块mysql的调试标志,然后输出有关SQL语句的详尽信息,效果如下:

2. 弊端
debug: true方案有如下弊端:
输出信息过于详细,在实际开发中反而会干扰我们快速查看其他日志信息
没有输出SQL语句的执行时间
3. 理想方案
对于一个理想的SQL语句输出方案,我们其实只关心两个问题:
- Egg组装的原生SQL语句到底是怎样的?便于我们快速排查问题
- SQL语句的执行时间是多少,便于我们尽早锁定性能问题,从而得到及时解决
4. CabloyJS的方案效果
CabloyJS是基于Egg的上层开发框架,针对前面提到的两个核心问题,实现了如下效果

这种SQL语句日志的输出效果:不仅一目了然可以看到刚刚执行了多少SQL语句,而且每一条SQL语句的执行时间也是历历在目。当然,顺便我们还能看到SQL语句是由哪个连接对象执行的(通过threadId)
实现方案
下面我们看一下CabloyJS是如何实现的。这种实现机制也适用于其他Egg系的上层框架
假设你已经创建了一个CabloyJS的项目,下面的源码均位于CabloyJS项目内
如何创建CabloyJS项目,请参见:快速开始
1. config定义
为了让方案更灵活,我们先扩展一下MySQL参数的定义
node_modules/egg-born-backend/config/config.default.js
// mysql
config.mysql = {
clients: {
__ebdb: {
// debug: true,
hook: {
meta: {
color: 'orange',
long_query_time: 0,
},
callback: {
onQuery,
},
},
},
},
};
| 名称 | 说明 |
|---|---|
| debug | 如果为true,就是启用内置的调试标志。在这里没有启用 |
| hook.meta | 包含hook的配置参数 |
| hook.meta.color | 日志输出的颜色 |
| hook.meta.long_query_time | 如果SQL语句的执行时间超过了long_query_time(ms),就会被输出到控制台。特别的,如果long_query_time设为0,则输出所有SQL语句 |
| hook.callback.onQuery | 为了提升灵活性,我们可以通过onQuery提供一个自定义的回调函数,当SQL语句的执行信息准备就绪时会被自动调用 |
2. 改写模块ali-rds
Egg执行MySQL语句的技术栈如下:模块egg -> 模块egg-mysql -> 模块ali-rds -> 模块mysql
在这里,我们只需要改写模块ali-rds即可
node_modules/@zhennann/ali-rds/lib/client.js
function RDSClient(options) {
if (!(this instanceof RDSClient)) {
return new RDSClient(options);
}
Operator.call(this);
this.pool = mysql.createPool(options);
const _hook = options.hook;
const _getConnection = this.pool.getConnection.bind(this.pool);
this.pool.getConnection = function(cb) {
_getConnection(function(err, conn) {
if (err) return cb(err, null);
onQuery(conn, function(err) {
if (err) return cb(err, null);
onConnection(conn, function(err) {
if (err) return cb(err, null);
cb(null, conn);
});
});
});
function onConnection(conn, cb) {
if (!_hook || !_hook.callback || !_hook.callback.onConnection) return cb(null);
if (conn.__hook_onConnection) return cb(null);
conn.__hook_onConnection = true;
co.wrap(_hook.callback.onConnection)(new RDSConnection(conn)).then(function() {
cb(null);
}).catch(function(err) {
cb(err);
});
}
function onQuery(conn, cb) {
if (!_hook || !_hook.callback || !_hook.callback.onQuery) return cb(null);
if (conn.__hook_onQuery) return cb(null);
conn.__hook_onQuery = true;
const _query = conn.query;
conn.query = function query(sql, values, cb) {
const prevTime = Number(new Date());
const sequence = _query.call(conn, sql, values, cb);
const _callback = sequence._callback;
sequence._callback = function(...args) {
const ms = Number(new Date()) - prevTime;
_hook.callback.onQuery(_hook, ms, sequence, args);
_callback && _callback(...args);
};
return sequence;
};
cb(null);
}
};
[
'query',
'getConnection',
].forEach(method => {
this.pool[method] = promisify(this.pool[method]);
});
}
首先,拦截
pool.getConnection方法当系统从数据库连接池中获取到connection对象时,执行两个回调
onConnection和onQueryonConnection是在第一次创建connection对象时,执行一些初始化SQL语句,比如设置一些会话级别的变量,不是这里讨论的重点onQuery的作用就是拦截connection.query方法,在query执行前和执行后分别记录时间,从而得到SQL语句的执行时间,然后执行config配置中指定的回调函数hook.callback.onQuery
3. 回调hook.callback.onQuery
我们再回头看一下config配置文件中的回调函数是如何实现的
node_modules/egg-born-backend/config/config.default.js
function onQuery(hook, ms, sequence, args) {
if (!hook.meta.long_query_time || hook.meta.long_query_time < ms) {
const message = `threadId: ${sequence._connection.threadId}, ${ms}ms ==> ${sequence.sql}`;
console.log(chalk.keyword(hook.meta.color)(message));
}
}
首先判断
hook.meta.long_query_time,如果为0或者小于执行时间,就会执行输出使用模块
chalk,并使用指定的颜色值hook.meta.color输出SQL执行日志
4. 模块module-alias
由于我们改写了模块ali-rds的源代码,所以我们需要启用一个新的模块名称,在这里就是@zhennann/ali-rds,发布到npm仓库即可
那么,如何使新模块@zhennann/ali-rds生效呢?由于模块ali-rds是被模块egg-mysql所引用的。我们如果还要改写模块egg-mysql的源码,代价就未免太大了
在这里,我们引入模块module-alias,从而达到这样的效果:模块egg-mysql源码不变,仍然是const rds = require('ali-rds');,但实际上引用的却是@zhennann/ali-rds
模块
module-alias的机理,请参见:https://github.com/ilearnio/module-alias
这里,我们只需看一下如何使用模块module-alias
node_modules/egg-born-backend/index.js
const moduleAlias = require('module-alias');
moduleAlias.addAlias('ali-rds', '@zhennann/ali-rds');
结语
这样,我们就实现了一个轻巧的方案,不仅可以直接在Egg上层框架中提供缺省的SQL语句输出方案,而且还可以通过覆盖config参数hook.callback.onQuery提供自定义的输出方案
Egg上层框架CabloyJS是如何输出SQL语句日志的?的更多相关文章
- yii2输出sql语句
yii2如何输出具体的查询的sql语句: $query = User::find() ->where(['id'=>[1,2,3,4]) ->select(['username']) ...
- IBatis.net 输出SQL语句(七)
一.IBatis.net输出SQL语句到控制台 输出IBatis.net生成的SQL语句到控制台,能够方便调试. 如果要想输出IBatis.net的SQL语句到控制台,那么只需要做如下配置即可: &l ...
- NHibernate输出SQL语句
用了NHierbate之后,很少需要写原生的SQL语句,由于总是看不到SQL语句,所以有时候对SQL调优非常不利.因此产生了让NHibernate输出它所生成的SQL语句的想法,以便于后续调优. 一. ...
- Spring3+MyBatis3整合log4j无法输出SQL语句问题的解决
今天遇到了跟下面文章一模一样的问题,下面文章的解决方案很好,在这里记录保存一下. Spring3+MyBatis3整合无法输出SQL语句问题的解决
- Ibatis.Net 输出SQL语句学习(七)
一.IBatis.net输出SQL语句 输出IBatis.net生成的SQL语句,能够方便调试. 在MapperHelper类中添加GetSql方法: /// <summary> /// ...
- django框架 - 实时查看执行的sql语句
django框架采用的ORM模型,我们可以通过mysql的日志记录实时看到执行的sql语句,具体步骤如下: 第一步:找到mysql的配置文件 第二步:编辑mysql配置文件 第三步:重启mysql 第 ...
- Druid搭配log4j2输出SQL语句和结果
一.引言 其实Druid的内置了log4jdbc来显示SQL语句,虽然显示效果不如原生的log4jdbc效果好,但是因为内置所以不需要其他更多的配置. 二.使用 1. 创建基于druid的logger ...
- 配置p6spyLog输出sql完整日志
第一步: 配置maven <dependency> <groupid>p6spy</groupid> <artifactid>p6spy< ...
- Yii 2.0版本调试输出SQL语句
项目是基于框架Yii 2.0开发的. 今天梳理一些数据统计功能代码的时候,想把当前运行的sql语句打印出来,然后放到navicat工具里面运行,并分析一下运行效率和调优方案,之前大部分时候都是写增加. ...
随机推荐
- JavaScript面向对象的方式开发轮播图插件
轮播图是很多页面必不可少的组件.这里来使用面向对象方式开发一个插件.减去开发的痛楚 首先需要寻找对象:只有一个对象,轮播图!关键点在于找到这个对象所拥有的属性以及方法,通过代码实现出来,这是面向对象最 ...
- Python网络爬虫 - 爬取中证网银行相关信息
最终版:07_中证网(Plus -Pro).py # coding=utf-8 import requests from bs4 import BeautifulSoup import io impo ...
- &&与&,||与| 区别
1. &&和&都是表示与,区别是&&只要第一个条件不满足,后面条件就不再判断. 而&要对所有的条件都进行判断. public class Test { ...
- eBPF Cilium实战(2) - 底层网络可观测性
在之前的平台中,对于组件之间的网络流向不具备直接的可观测性,用户组件间通信出现问题,只能通过传统命令行工具进行手动排查,而 cilium 的 Hubble 服务可以提供 UI 界面向用户展示实时的流量 ...
- Codeforces Round #762 (Div. 3), CDE
(C) Wrong Addition Problem - C - Codeforces 题意 定义一种计算方式, 对于a+b=c, 给出a和c, 求b 题解 因为求法是从个位求得, 先求出来的最后输 ...
- 生成二维码,并且保存,指定位置的view成图片,并且保存到本地相册
效果图: 保存的图片效果是: 保存到本地的,是整个视图,不只是单单的二维码的图片, 在了解的一番过程之后,我知道了, 1.首先要去获取保存图片的写入权限:(使用 https://github.com/ ...
- gin框架使用【7.中间件使用】
在 gin中,通过默认的函数,构建一个实现了带默认中间件的 *Engine. r := gin.Default() 默认绑定了Logger和Recovery中间件,帮助我们进行日志输出和错误处理. f ...
- 阶段性总结linux(1)
学习安装linux系统 [网络连接方式] 桥接 ,好比所有人都在25期教室,公用这个教室的局域网段 192.168.11.0~192.168.11.255 教室内有60个同学,插上了网线,所有人都是 ...
- 巧用 JuiceFS Sync 命令跨云迁移和同步数据
近年来,云计算已成为主流,企业从自身利益出发,或是不愿意被单一云服务商锁定,或是业务和数据冗余,或是出于成本优化考虑,会尝试将部分或者全部业务从线下机房迁移到云或者从一个云平台迁移到另一个云平台,业务 ...
- GO语言学习——运算符
运算符 Go 语言内置的运算符有: 算术运算符 关系运算符 逻辑运算符 位运算符 赋值运算符 算术运算符 运算符 描述 + 相加 - 相减 * 相乘 / 相除 % 求余 注意: ++(自增)和--(自 ...