前言
本篇是为 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 的配置。
在 egg 前 require
我们先考虑如何将 APM agent 早于 egg 执行,解决 patch 此框架主要问题,然后再完成定制化操作。
好在 egg 是提供了相关方法的,最底层的 API 是 startCluster
方法,可以传入 require
参数指明你要率先执行模块。
方案一:修改启动文件
假设把 APM 的引入和配置放在了根目录 elastic-apm.js 中,新建一个类似下面这样的 app.js 作为 egg 的启动文件,用最原始的 node app.js
启动服务就好了。
1 2 3
| require('egg').startCluster({ require: [ require.resolve('./elastic-apm') ], });
|
!!! 不推荐:必须放弃 egg-bin 和 egg-scripts 作为启动器,需要自己补上很多操作。
方案二:修改 package.json
要求 egg-bin
版本 >= 4.10.0
在 package.json
中添加 egg.require
配置,用法来自 issue 讨论。
1 2 3 4 5 6 7 8
| { "egg": { "require": [ "./elastic-apm.js" ] }, "scripts": { "start": "egg-scripts start" } }
|
这是目前最推荐的做法,不影响 egg 工具链的正常使用。
2019-10-17 更正
感谢 @wenpin 提示
egg.require
配置只是 egg-bin 提供的接口,并在文档上明确记录。然而 egg-scripts 目前还没有提供此入口,只能通过 --require
导入,egg-bin 也同样支持,这得益于 egg-cluster 的实现。
1 2 3 4 5
| { "scripts": { "start": "egg-scripts start --require=@yourscope/elastic-apm" } }
|
--require
不能使用相对路径,比起使用绝对路径,通过 npm 包来引入更方便一些。(egg-scripts 的 issues 关闭了,不然问问阻断他们支持 egg.require 的原因…)
编写补丁
instrumentation/egg.js
仿照 instrumentation/koa.js,修改框架 name,注意开启 overwrite 选项,为了覆盖引用 koa 带来的标识。
1 2 3 4 5 6 7 8
| 'use strict'
module.exports = function (egg, agent, { version, enabled }) { if (!enabled) return egg agent.setFramework({ name: 'egg', version, overwrite: true }) return egg }
|
instrumentation/egg-router.js
唯一需要修改的就是去掉 instrumentation/koa-router.js 的版本验证。考虑到 egg 引入的 koa 版本都是同时期最新,无须担心 egg 自身版本问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| '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,大功告成。
1 2 3 4 5 6 7
| 'use strict'; const apm = require('elastic-apm-node').start({ });
apm.addPatch('egg', require.resolve('./instrumentation/egg')) apm.addPatch('@eggjs/router', require.resolve('./instrumentation/egg-router'))
|