启动 Agent

编辑

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

  • 确保 APM Agent 尽早启动,以及
  • 有一种方便的方法来配置 Agent。

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

启动方法

编辑

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

编辑

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

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

// Application main code goes here.

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

编辑

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

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

// Application main code goes here.

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

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

// Application main code goes here.

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

注意:从 elastic-apm-node 3.47.0 版本开始,“elastic-apm-node/start.js”将不会在 Node.js Worker 线程中启动 Agent。

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

编辑

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

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

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

# 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 Worker 线程中启动 Agent。新的 Worker 线程继承process.execArgv和环境,因此“elastic-apm-node/start.js”将再次执行。“start.js”导致在每个 Worker 线程中启动新的 APM Agent 被认为是意外情况,因此目前已禁用。

单独的 APM 初始化模块

编辑

如果您想避免提升的 ES 模块的陷阱,但仍然想要灵活地将配置对象传递给Agent 启动方法,那么一个不错的选择是编写一个单独的 JavaScript 或 TypeScript 模块来启动 Agent,并在主文件的顶部导入初始化模块。例如

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

// Application code starts here.

启动陷阱

编辑

本节显示使用某些技术启动 APM Agent 时有时会出现的细微的意外情况。使用任何生成已编译 JavaScript 的构建工具/系统的 Agent 的一般故障排除技巧是查看已编译的 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.
// ...

elastic-apm-node/start模块解决了这个问题。以下内容将有效

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

使用 Babel 的更完整的示例在此处

esbuild(如此处 esbuild 文档中所述)转换的 ES 模块用法也是如此。值得注意的是,TypeScript 在这方面遵循 ECMAScript 模块语义。

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

TypeScript 陷阱

编辑

TypeScript 是一种编译为 JavaScript 的语言,通过tsc TypeScript 编译器编译,然后通过node(或其他 JavaScript 解释器)执行。有时,生成的 JavaScript 在使用此 APM Agent 时会出现陷阱。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 语法到较旧的 JavaScript 语法,从 TypeScript),tree-shaking(删除未使用的代码部分),缩小,CSS/图像捆绑等。有很多捆绑器工具,包括:WebpackesbuildRollupParcel

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

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

  1. 从捆绑包中排除elastic-apm-nodeAPM Agent 模块;以及
  2. 可选地从捆绑包中排除您希望 APM Agent 检测的其他模块。

从捆绑包中“排除”模块foo(Webpack 将这些称为“外部”)意味着require('foo')期望“node_modules/foo/…”在运行时存在。这意味着您需要部署捆绑包文件以及排除的模块。这可能会或可能不会破坏您使用捆绑器的理由。

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

Webpack

编辑

Webpack 支持“外部”配置选项,以将其捆绑包中排除特定模块。至少必须将elastic-apm-agent模块设置为外部。此外,您希望 APM Agent 检测的任何模块(例如数据库客户端)也必须设置为外部。最简单的方法是使用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()],
};

esbuild

编辑

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

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

{
  "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 Agent 的更完整示例在此处