title: elastic-apm-node 扩展篇 —— Egg date: 2019-07-12 21:18:53 tags: [Node.js,APM,Elastic-APM,Egg]
本篇是为 elastic-apm-node 编写拓展系列的第二篇,主要介绍 egg 框架的定制指南。
单独拿 egg 出来讲,是因为市面主流的 APM 工具几乎都没有为 egg 提供支持。一方面 egg 底层基于 koa ,并且 egg-router 也是 koa-router 的二次封装,两者相似以致插件可以平稳切换,agent 的补丁方式也基本是一致的。另一方面,agent 代码必须早于 egg 和 egg-router 的加载才能生效,egg-bin、egg-script 等生态决定了编写框架之上的插件很难做到零代码入侵。
APM agent 作为 npm package 不适合做这样的事,更好的方法是我们利用 egg 工具链的接口,在引入 agent 的代码层处理好 egg 的配置。
<!--more-->我们先考虑如何将 APM agent 早于 egg 执行,解决 patch 此框架主要问题,然后再完成定制化操作。
好在 egg 是提供了相关方法的,最底层的 API 是 startCluster
方法,可以传入 require
参数指明你要率先执行模块。
假设把 APM 的引入和配置放在了根目录 elastic-apm.js 中,新建一个类似下面这样的 app.js 作为 egg 的启动文件,用最原始的 node app.js
启动服务就好了。
require('egg').startCluster({
require: [ require.resolve('./elastic-apm') ],
});
!!! 不推荐:必须放弃 egg-bin 和 egg-scripts 作为启动器,需要自己补上很多操作。
要求
egg-bin
版本 >= 4.10.0
在 package.json
中添加 egg.require
配置,用法来自 issue 讨论。
{
"egg": {
"require": [ "./elastic-apm.js" ]
},
"scripts": {
"start": "egg-scripts start"
}
}
这是目前最推荐的做法,不影响 egg 工具链的正常使用。
感谢 @wenpin 提示
egg.require
配置只是 egg-bin 提供的接口,并在文档上明确记录。然而 egg-scripts 目前还没有提供此入口,只能通过 --require
导入,egg-bin 也同样支持,这得益于 egg-cluster 的实现。
{
"scripts": {
"start": "egg-scripts start --require=@yourscope/elastic-apm"
}
}
--require
不能使用相对路径,比起使用绝对路径,通过 npm 包来引入更方便一些。(egg-scripts 的 issues 关闭了,不然问问阻断他们支持 egg.require 的原因...)
仿照 instrumentation/koa.js,修改框架 name,注意开启 overwrite 选项,为了覆盖引用 koa 带来的标识。
'use strict'
module.exports = function (egg, agent, { version, enabled }) {
if (!enabled) return egg
// 注意开启 overwrite,这样才能覆盖 koa 标识
agent.setFramework({ name: 'egg', version, overwrite: true })
return egg
}
唯一需要修改的就是去掉 instrumentation/koa-router.js 的版本验证。考虑到 egg 引入的 koa 版本都是同时期最新,无须担心 egg 自身版本问题。
'use strict';
const shimmer = require('elastic-apm-node/lib/instrumentation/shimmer');
module.exports = function (Router, agent, { version, enabled }) {
if (!enabled) return Router
agent.logger.debug('shimming koa-router prototype.match function')
shimmer.wrap(Router.prototype, 'match', function (orig) {
return function (_, method) {
var matched = orig.apply(this, arguments)
if (typeof method !== 'string') {
agent.logger.debug('unexpected method type in koa-router prototype.match: %s', typeof method)
return matched
}
if (Array.isArray(matched && matched.pathAndMethod)) {
const layer = matched.pathAndMethod.find(function (layer) {
return layer && layer.opts && layer.opts.end === true
})
var path = layer && layer.path
if (typeof path === 'string') {
var name = method + ' ' + path
agent._instrumentation.setDefaultTransactionName(name)
} else {
agent.logger.debug('unexpected path type in koa-router prototype.match: %s', typeof path)
}
} else {
agent.logger.debug('unexpected match result in koa-router prototype.match: %s', typeof matched)
}
return matched
}
})
return Router
}
回到最初的 elastic-apm.js 文件,在其中设置 addPatch,大功告成。
'use strict';
const apm = require('elastic-apm-node').start({
// options
});
apm.addPatch('egg', require.resolve('./instrumentation/egg'))
apm.addPatch('@eggjs/router', require.resolve('./instrumentation/egg-router'))