启动代理编辑

有几种方法可以启动 Node.js APM 代理。选择最适合您的方法。选择方法时最重要的考虑因素是

  • 确保 APM 代理启动得足够早,以及
  • 拥有一个方便的方法来配置代理。

为了使 Node.js APM 代理能够完全发挥作用,它 必须在 require(...) 语句之前启动其他模块。APM 代理通过在导入过程中插入自身来自动检测模块。如果在 APM 代理启动之前导入给定模块,那么它将无法检测该模块。

启动方法编辑

require('elastic-apm-node').start(...)编辑

启动 APM 代理最常见的方法是要求 elastic-apm-node 模块并在主模块的顶部调用 .start() 方法。这允许您使用任何方法来 配置代理

const apm = require('elastic-apm-node').start({
  // Add configuration options here.
});

// Application main code goes here.

require('elastic-apm-node/start')编辑

启动代理的另一种方法是使用 elastic-apm-node/start 模块,该模块导入并启动代理。

const apm = require('elastic-apm-node/start');

// Application main code goes here.

此启动方法适用于那些使用 Babel 或 esbuild 等工具从使用 ES 模块(如以下示例)的代码转换为使用 CommonJS 的代码的人。它确保在同一文件中的其他导入之前启动 APM 代理。有关详细信息,请参阅下面的 提升的 ES 模块导入

import 'elastic-apm-node/start.js';

// Application main code goes here.

这种方法的一个限制是您不能使用选项对象配置代理,而是必须依赖于 其他配置方法之一,例如设置 ELASTIC_APM_... 环境变量。

注意:从 elastic-apm-node 版本 3.47.0 开始,"elastic-apm-node/start.js" 将 不会在 Node.js 工作线程中启动代理。

node -r elastic-apm-node/start.js ...编辑

启动代理的另一种方法是使用 -r elastic-apm-node/start.js 命令行选项到 node。这将在应用程序代码启动之前加载并启动 APM 代理。此方法允许您无需触碰任何代码即可启用代理。这是 监控 AWS Lambda 函数 和跟踪 Next.js 服务器 的推荐启动方法。

node -r elastic-apm-node/start.js app.js

还可以通过 NODE_OPTIONS 环境变量 指定 -r, --require 选项

# export ELASTIC_APM_...  # Configure the agent with envvars.
export NODE_OPTIONS='-r elastic-apm-node/start.js'
node app.js

注意:从 elastic-apm-node 版本 3.47.0 开始,"elastic-apm-node/start.js" 将 不会在 Node.js 工作线程 中启动代理。 新的工作线程继承 process.execArgv 和环境,因此 "elastic-apm-node/start.js" 会再次执行。由于 "start.js" 在每个工作线程中启动一个新的 APM 代理被认为是意外行为,因此目前已禁用。

单独的 APM 初始化模块编辑

如果您想避免 提升的 ES 模块带来的问题,但仍然希望能够将配置对象传递给 代理启动方法,那么一个不错的选择是编写一个单独的 JavaScript 或 TypeScript 模块来启动代理,并在主文件的顶部导入 初始化模块。例如

// initapm.ts
import apm from 'elastic-apm-node';
apm.start({
  serverUrl: 'https://...',
  secretToken: '...',
  // ...
})
// main.ts
import 'initapm'

// Application code starts here.

启动注意事项编辑

本节展示了使用某些技术启动 APM 代理时有时会出现的细微问题。使用代理与任何生成已编译 JavaScript 的构建工具/系统一起使用的一般故障排除技巧是查看已编译的 JavaScript,以查看 node 实际执行的是什么。

提升的 ES 模块导入编辑

当使用 Babel 或 esbuild 等工具将使用 ES 模块(即 import ... 语句)的代码转换为使用 CommonJS(即 require(...))的代码时,所有导入都会“提升”到模块的顶部,正确遵循 ECMAScript 模块 (ESM) 语义。这意味着 apm.start() 方法调用得太晚—— http 模块导入之后。

例如,对以下代码运行 Babel 不会及时启动 APM

import apm from 'elastic-apm-node';
apm.start() // This does not work.

import http from 'http';
// ...

Babel 将其转换为等效的

var apm = require('elastic-apm-node');
var http = require('http');
apm.start() // This is started too late.
// ...

the elastic-apm-node/start 模块 解决了这个问题。以下将起作用

import 'elastic-apm-node/start'; // This works.
import http from 'http';
// ...

使用 Babel 的更完整的示例 在这里

esbuild 翻译的 ES 模块使用情况也是如此(如 esbuild 文档中的解释)。值得注意的是,TypeScript 在这方面遵循 ECMAScript 模块语义。

另一个不错的选择是 使用单独的 APM 初始化模块 并先导入它。

TypeScript 注意事项编辑

TypeScript 是一种编译为 JavaScript 的语言,通过 tsc TypeScript 编译器进行编译,然后通过 node(或其他一些 JavaScript 解释器)执行。有时生成的 JavaScript 会出现使用此 APM 代理的问题。TypeScript 假设模块导入没有副作用,因此如果 apm 变量未使用,它将 省略以下导入

import apm from 'elastic-apm-node/start'; // Be careful

可以使用以下方法避免省略

import 'elastic-apm-node/start';

或者使用类似以下内容

import apm from 'elastic-apm-node/start'; apm; // Ensure import is kept for its side-effect.

TypeScript 5.0 引入了 --verbatimModuleSyntax 选项,可以避免这种省略。

捆绑器和 APM编辑

JavaScript 捆绑器是将多个 JavaScript 文件捆绑到一个或几个 JavaScript 文件中以执行的工具。它们通常还包括其他功能,例如编译(从较新的 JavaScript 语法到较旧的语法,从 TypeScript)、树摇动(删除未使用的代码部分)、缩小、CSS/图像捆绑等。捆绑器工具有很多,包括:WebpackesbuildRollupParcel

捆绑器的主要用例是提高浏览器应用程序的性能,其中减少单独文件的大小和数量有助于减少网络和 CPU 开销。对于使用 node 执行的服务器端 JavaScript 代码,此用例通常不那么强大。但是,某些工具会将捆绑器用于服务器端 JavaScript,不一定是为了捆绑,而是为了其他一些功能。

不幸的是,使用捆绑器通常会破坏 APM 代理。将多个模块捆绑到一个文件中必然意味着用处理返回模块对象的自定义捆绑器代码替换 require(...) 调用。但是 APM 代理依赖于这些 require(...) 调用来检测模块。对此没有自动修复。解决方法是

  1. elastic-apm-node APM 代理模块从捆绑包中排除;以及
  2. 可以选择从捆绑包中排除其他模块,这些模块是您希望 APM 代理进行检测的。

从捆绑包中“排除”模块 *foo*(Webpack 将其称为“外部”)意味着 require('foo') 预计在运行时存在“node_modules/foo/…​”。这意味着您需要部署捆绑包文件 *以及* 排除的模块。这可能会或可能不会破坏您使用捆绑程序的原因。

本节的其余部分展示了如何使用各种捆绑程序配置外部。如果您知道我们尚未记录的捆绑程序机制,请 告知我们。

Webpackedit

Webpack 支持 “外部” 配置选项以从其捆绑包中排除特定模块。至少,*elastic-apm-agent* 模块必须设置为外部。此外,您希望 APM 代理检测的任何模块(例如数据库客户端)也必须设置为外部。最简单的方法是 使用 webpack-node-externals 模块将所有“node_modules/…​”设置为外部.

对于 webpack@5,请确保您的“webpack.config.js”具有以下内容

const nodeExternals = require('webpack-node-externals');

module.exports = {
    // ...

    // Set these so Webpack emits code using Node's CommonJS
    // require functions and knows to use Node's core modules.
    target: 'node',
    externalsPresets: {
        node: true
    },

    // This tells Webpack to make everything under
    // "node_modules/" external.
    externals: [nodeExternals()],
};

对于 webpack@4,externalsPresets 配置变量不存在,因此请使用

const nodeExternals = require('webpack-node-externals');

module.exports = {
    // ...

    target: 'node',
    externals: [nodeExternals()],
};

esbuildedit

esbuild 支持将模块/文件标记为 “外部” 到捆绑包。至少,*elastic-apm-agent* 模块必须设置为外部才能使 APM 代理正常工作。此外,您希望 APM 代理检测的任何模块(例如数据库客户端)也必须设置为外部。

以下是一个用于“package.json”的示例构建脚本,用于捆绑 Node.js 应用程序(以“src/index.js”作为入口点,目标为 node v14.x,并确保 pg PostgreSQL 模块被检测)

{
  "scripts": {
    "build": "esbuild src/index.js --outdir=dist --bundle --sourcemap --minify --platform=node --target=node14 --external:elastic-apm-node --external:pg"
  }
}

可以通过以下方式调用它

npm run build

或者 esbuild 配置可以放入构建脚本中,并通过 node esbuild.build.js 调用。

// esbuild.build.js
require('esbuild').build({
    entryPoints: ['./src/index.js'],
    outdir: 'dist',
    bundle: true,
    platform: 'node',
    target: 'node14',
    sourcemap: true,
    minify: true,
    external: ['elastic-apm-node', 'pg']
}).catch(() => process.exit(1))

除了手动将特定依赖项列为“外部”之外,还可以使用以下 esbuild 选项来排除 所有 依赖项

esbuild ... --external:'./node_modules/*'

使用 esbuild 和 APM 代理的更完整的示例 在此