← 返回蜂巢洞察

如何使用NestJS的观察功能:开发者必备的可观测性实践指南

可观测性并不是一个新出现的问题,NestJS也绝不是第一个试图解决这个问题的生态系统。 但NestJS Observe之所以值得关注,是因为它提出了一个更为具体的问题:当可观测性系统能够理解其所监控的框架时,会发生什么呢? 在了解其实现机制之前,我们首先需要明确这个问题,以及为什么框架的上下文会对此产生重要影响。 可观测性问题 如果你曾经长期开发并维护过服务器应用程序,你肯定遇到过这样的情况:某个API莫名其妙地运行速度变慢了,但你却不知道原因何在。 你检查了日志,却发现没有任何明显的异常。数据库运行正常,也没有任何错误信息;在你的本地机器上,这个接口也能正常工作。 于是你添加了一些日志记录:

可观测性并不是一个新出现的问题,NestJS也绝不是第一个试图解决这个问题的生态系统。

但NestJS Observe之所以值得关注,是因为它提出了一个更为具体的问题:当可观测性系统能够理解其所监控的框架时,会发生什么呢?

在了解其实现机制之前,我们首先需要明确这个问题,以及为什么框架的上下文会对此产生重要影响。

可观测性问题

如果你曾经长期开发并维护过服务器应用程序,你肯定遇到过这样的情况:某个API莫名其妙地运行速度变慢了,但你却不知道原因何在。

你检查了日志,却发现没有任何明显的异常。数据库运行正常,也没有任何错误信息;在你的本地机器上,这个接口也能正常工作。

于是你添加了一些日志记录:

11:30:02 AM 日志:开始创建订单
11:34:11 AM 日志:检查库存信息
11:34:12 AM 日志:开始支付流程
11:39:03 AM 日志:支付完成

然后你再次执行了这个请求。现在你应该大致明白了问题出在哪里了,但同时你也刚刚建立了一个属于自己的简单追踪系统。

这就是事情变得有趣的地方。现代应用程序能够产生海量的信息:日志记录、指标数据、追踪信息、性能分析结果、错误报告、数据库查询语句、HTTP请求记录、后台作业信息、队列消息等等。

真正的挑战并不一定在于“收集更多的信息”,而在于能否回答一个更为简单的问题:

我的应用程序内部到底发生了什么?

对于NestJS应用程序来说,这个问题显得尤为重要,因为Nest本身就已经掌握了很多关于应用程序运行机制的信息。

Nest知道控制器、提供者、中间件、守卫、拦截器以及数据管道的作用;它也能知道GraphQL解析器何时被执行,或者微服务处理程序何时收到了消息。

那么,如果让可观测性系统利用这些信息会怎么样呢?这就是NestJS Observe的核心理念。在这篇文章中,我们将会探讨这个问题。

不过,我们不会仅仅把NestJS Observe看作是一个需要安装的普通包,而是会利用它来探索一个更重要的问题:具备框架感知能力的可观测性系统究竟能为我们带来什么好处?在什么情况下使用它才是最有意义的呢?

我们将从可观测性的基础知识开始讲起,构建一个简单的NestJS应用程序,引入一个真实的性能问题,然后使用NestJS Observe来对这个问题进行追踪与分析;最后,我们会将这种方法与更为中立的OpenTelemetry技术进行对比。

先决条件

要想顺利跟随本文的学习流程,你需要具备基本的TypeScript和NestJS知识,并且对HTTP API有一定的了解。

你不需要事先拥有可观测性方面的经验。我们在学习的过程中会逐步介绍相关概念。

我们将涵盖的内容:

Observability实际上在解决什么问题?

让我们把这个问题具体化。想象这样一个NestJS应用程序:有一个POST /orders请求被发送过来,处理这个请求花费了三秒钟。这就是我们的HTTP客户端所提供的全部信息。

图1. 一个API请求可能会触发多层后续操作

上图展示了为什么一个POST /orders请求会触发应用程序内部的多个处理环节,这也说明为什么仅仅通过请求耗时是无法了解具体时间花费在哪些地方的。

我们目前所知道的是:

POST /orders → 3秒钟

但这还不够。我们需要弄清楚这三秒钟是花在以下哪个环节上:

控制器 → 服务层 → 数据库

还是:

控制器 → 服务层 → 外部API

又或者可能是其他完全不同的原因:

2026-08-31 14:21:04
支付服务返回了HTTP 502错误代码

这样的日志记录确实很有用,但当你在分析某个具体请求时,可能仍然需要手动将这条日志与其他相关事件联系起来才能理解整个流程。

指标数据用于汇总某些现象发生的频率:

请求次数 = 1,240,231
错误率 = 2.4%
95%延迟时间 = 840毫秒

通过指标数据可以很容易地了解系统的发展趋势,比如“在昨天进行部署后,系统的延迟时间是否增加了?”。但是指标数据通常无法解释某个具体请求的完整处理过程。因此,我们还需要“跟踪信息”。

跟踪信息用于记录某个特定操作的执行过程:

例如:

POST /orders
│
├── 认证流程
├── OrdersController.create()
├── InventoryService.reserve()
├── PaymentService.charge()
└── 支付API接口

现在我们能够看到程序的执行路径了。更重要的是,我们还能了解各个操作所花费的时间。

接下来就是性能分析。性能分析能帮助我们解答另一个问题:运行时在哪些地方消耗了大量资源?假设某个接口响应速度很慢,但数据库查询或HTTP请求并没有明显的问题。

通过CPU性能分析,我们可以发现:

CPU
│
├── JSON序列化
├── 应用程序代码
├── 垃圾回收
└── 加密运算

这四种分析方式可以相互补充。

这是一个非常有用的思维模型:

图2:可观测性作为一个整体

这个图将主要的可观测性工具(日志、指标、跟踪数据及性能分析结果)归纳在一起,展示了它们各自如何揭示应用程序的行为,以及它们是如何相互补充的。

实际上,并不是每个应用程序都需要所有这些工具。关键是要拥有足够的信息来解答运行维护中遇到的问题。

从应用程序监控到基于框架的可观测性

传统的方法为我们提供了强大且标准化的遥测数据收集方式。但各种开发框架本身就已经了解我们的应用程序是如何构建和运行的。如果可观测性工具能够利用这些框架提供的信息,而不是将应用程序简单地视为一个通用进程,那会怎么样呢?

在开始进行可观测性分析之前

对于那些响应速度较慢的/orders接口,我们有几种处理方法。最简单的方法就是记录日志:

console.log('开始支付流程');
await paymentService.charge();
console.log('支付完成');

当应用程序规模还较小时,这种方法是有效的。但当应用规模扩大后,我们就可以引入指标分析:

payment_latency_ms
orders_created_total
payment_failures_total

这样我们就能了解大量请求的整体运行情况了。接下来,我们还可以使用分布式跟踪技术:

POST /orders
│
├── OrdersController
├── OrdersService
├── InventoryService
└── PaymentService

在这里,OpenTelemetry显得尤为重要。OpenTelemetry是一个与特定供应商无关的可观测性框架及工具包,用于生成、收集和导出遥测数据。

需要注意的是,OpenTelemetry本身并不是一个专门用于处理可观测性数据的后端系统。我们稍后会再讨论这个区别,因为它在将OpenTelemetry与其它工具进行比较时非常重要。

那么,为什么我们需要NestJS的Observe功能呢?

这就是有趣的地方所在。虽然通用的监控层可以记录HTTP请求的详细信息,但NestJS知道这个请求是经过了特定的应用程序结构来处理的。

例如:

HTTP请求 → 中间件 → 安全检查机制 → 拦截器 → 处理流程 → 控制器 → 提供者

这些并不是随意编写的JavaScript函数,而是Nest框架本身能够理解的概念。因此,对于那些紧邻该框架运行的可观测性系统而言,这些信息具有很高的价值。

与其仅仅看到:

HTTP GET /users/42 (执行耗时4秒)

我们反而可以了解:

HTTP GET /users/42 → UsersController.findOne() → UsersService.findUser() → UsersRepository.findById()

正是这种区别,使得框架级别的可观测性功能变得如此重要。

框架感知型可观测性

让我们给这个概念起个名字吧。框架感知型可观测性意味着这些可观测性工具能够理解框架的执行机制,而不会将整个应用程序简单地视为一个普通的进程。

传统的可观测性模型大致如下所示:

应用程序
    ↓
通用可观测性工具
    ↓
OpenTelemetry
    ↓
数据收集器
    ↓
可观测性后端

而采用框架集成式的方案则更类似于以下结构:

认识NestJS Observe

NestJS Observe是一款专为NestJS应用程序设计的官方可观测性工具。这个项目是开源的。它的目的并不是去重新发明日志记录、指标统计或追踪功能,因为这些现有的技术已经非常成熟且效果良好。它的价值在于能够将可观测性功能与NestJS的应用程序模型紧密结合起来。

目前该项目的文档中详细描述了如何对NestJS应用程序中的多种执行路径进行自动监测,包括:

  • HTTP请求

  • GraphQL查询

  • 微服务/RPC调用

  • BullMQ消息队列

  • 定时任务

此外,它还提供了运行时指标统计和性能分析功能,这些功能都值得我们进行测试。

在继续讨论之前,请注意:Observe目前仍是NestJS生态系统中相对较新的组件,因此它的API、支持的集成方案、定价策略以及功能内容都可能会发生快速变化。

那么,现在我们就不要再纸上谈兵了,实际动手构建一些东西吧。

但在继续之前,请稍等片刻。如果你想在自己搭建应用程序之前先了解这个工具的实际效果,NestJS目前提供了一个公开的Observe演示界面,你可以直接去体验一下。这个演示界面使用的是来自一个繁忙服务的真实数据集,因此你无需注册账户或安装任何软件,就可以查看请求信息、追踪记录、流程图、错误日志、后台任务以及警报信息。

我建议在继续之前,花几分钟时间仔细浏览一下这些内容。特别是,打开某个请求或跟踪流程,从高层次的操作一直追踪到它所执行的各个具体步骤。这样,你就能比仅仅查看截图或功能列表更清楚地理解我们在这个示例中正在构建什么。

我们的实践项目

我们将构建一个简单的结账API。它由三个小模块组成:

  • 订单模块:接收HTTP请求并协调整个结账流程。

  • 库存模块:模拟预留用户所需产品的过程。

  • 支付模块:模拟外部支付服务,并故意引入延迟。

请参见上图1,了解我们将要复现的整体流程。

订单模块处于请求流程的核心位置,而库存模块和支付模块则负责执行后续步骤。这样的结构正好能够帮助我们创建一个具有真实延迟效果的请求流程,进而观察Observe工具能否帮助我们找出时间被消耗在哪些地方。

我们的支付服务会故意设置较慢的响应速度。

我们的目标是看看是否能够在不手动为每个操作添加监控代码的情况下,识别出请求流程中时间被浪费在哪些环节。

我们并不是在构建一个完整的电子商务平台,只是通过创建一些具有复杂性的功能,来模拟出一个真实的可观测性问题罢了。

为了跟随这个教程进行学习,你需要安装最新版本的Node.js以及NestJS命令行工具。你不需要了解OpenTelemetry或任何现有的APM平台,也不需要使用生产环境中的应用程序。

创建NestJS应用程序

首先创建一个普通的NestJS项目:

$ npm i -g @nestjs/cli # 如果还没有安装的话
$ nest new nest-observe-demo
$ cd nest-observe-demo

执行nest new命令后,系统会询问你是否希望启用自动监控功能。请选择“是”。

然后启动项目:

$ npm run start:dev

接下来使用Nest CLI生成各个模块:

src/
├── app.module.ts
├ ├── main.ts
│
├── orders/
│   ├── orders.controller.ts
│   ├── orders.service.ts
│   └── orders.module.ts
│
├── inventory/
│   ├── inventory.service.ts
│   └── inventory.module.ts
│
└── payments/
    ├── payments.service.ts
    └── payments.module.ts

我们的支付服务实际上代表了一个外部支付提供商。在本次教程中,我们会模拟网络延迟现象来进一步演示这个功能。

import { Injectable } from '@nestjs/common';

@Injectable()
export class PaymentsService {
  async charge(amount: number) {
    await new Promise((resolve) =>
      setTimeout(resolve, 750),
    );

    return {
      id: `payment_${Date.now()}`,
      amount,
      status: 'succeeded',
    };
  }
}

关键部分就是setTimeout()这个方法。我们故意创建了一个会导致延迟的操作。

在真实的应用程序中,这种延迟可能由以下原因造成:

  • 一个HTTP请求

  • 一次数据库查询

  • 对第三方API的调用

  • 另一个微服务的响应

对于我们的示例来说,延迟的具体来源并不重要。

添加库存信息

我们的库存服务将会运行得快很多:

import { Injectable } from '@nestjs/common'; @Injectable() export class InventoryService { async reserveproductId: string) { await new Promise((resolve) => setTimeout(resolve, 40), ); return { productId, reserved: true, }; } }

同样,这里的延迟也代表了外部操作所导致的等待时间。

创建订单服务

现在我们要将这两种操作结合起来:

// src/orders.service.ts import { Injectable } from '@nestjs/common'; import { InventoryService } from '../inventory/inventory.service'; import { PaymentsService } from '../payments/payments.service'; @Injectable() export class OrdersService { constructor( private readonly inventoryService: InventoryService, private readonly paymentsService: PaymentsService, ) {} async createOrder( productId: string, amount: number, ) { const inventory = await thisinventoryService.reserveproductId); const payment = await this/paymentsService.charge(amount); return { id: `order_${Date.now()}`, productId, amount, inventory, payment, }; } }

请注意一个重要的点:这里没有任何用于监控或处理异步操作的代码。目前来说,这些仅仅是一些应用程序逻辑而已。

创建控制器

在生成的控制器文件中,需要填写以下内容:

import { Body, Controller, Post } from '@nestjs/common'; import { OrdersService } from './orders.service'; @Controller('orders') export class OrdersController { constructor( private readonly ordersService: OrdersService, ) {} @Post() create( @Body() body: { productId: string; amount: number; }, ) { return this.ordersService.createOrder( bodyproductId, body.amount, ); } }

这个控制器被设计得非常简洁:它接收HTTP请求,提取productId和amount这两个参数,然后将实际的处理工作交给OrdersService来完成。

OrdersService是通过控制器的构造函数,利用NestJS的依赖注入机制来创建的。换句话说,我们并不会自己手动编写`new OrdersService()`这样的代码来创建该服务对象。相反,当应用程序启动时,Nest会自动生成这个服务并将其提供给控制器使用。这样一来,控制器就可以专注于处理与HTTP相关的功能,而让Nest来负责管理应用程序所依赖的各种组件。

下面的模块配置使得跨模块边界进行依赖注入成为可能。OrdersModule提供了OrdersService,同时导入了那些提供了InventoryService和PaymentsService的模块。这些被导出的服务使得其他需要使用它们的模块能够顺利地访问它们。这就是在这个示例中所需的NestJS依赖注入机制的基本原理。如果你想更深入地了解依赖注入系统的运作方式,可以参考相关的NestJS文档。

请确保你的各个模块正确地导入和提供了所需的服务。你可以通过这里了解更多关于NestJS依赖注入机制的信息。

import { Module } from '@nestjs/common';
import { OrdersController } from './orders.controller';
import { OrdersService } from './orders.service';
import { InventoryModule } from '../inventory/inventory.module';
import { PaymentsModule } from '../payments/payments.module';

@Module({
  imports: [
    InventoryModule,
    PaymentsModule,
  ],
  controllers: [OrdersController],
  providers: [OrdersService],
})
export class OrdersModule {}
@Module({
  providers: [InventoryService],
  exports: [InventoryService],
})
export class InventoryModule {}
@Module({
  providers: [PaymentsService],
  exports: [ PaymentsService],
})
export class PaymentsModule {}
最后,将OrdersModule导入到AppModule中。通常情况下,CLI会自动完成这个操作,但如果你没有使用CLI,还是需要手动检查一下。

测试应用程序

在添加监控功能之前,我们首先需要确认应用程序本身能否正常运行。这个请求会触发完整的结账流程:控制器接收请求后,OrdersService会先调用库存服务,然后再调用支付服务,最终API会返回整个交易的结果。

大约790毫秒的响应时间是故意设置出来的——这样在启用监控功能后,我们就可以针对这个具体的性能问题进行排查了。

运行以下命令重新启动应用程序:$ npm run start:dev
curl -X POST http://localhost:3000/orders \
  -H "Content-Type: application/json" \
  -d '{"productId":"book-123","amount":49}'
{
  "id": "order_...",
  "productId": "book-123",
  "amount": 49,
  "inventory": {
    "productId": "book-123",
    "reserved": true
  },
  "payment": {
    "id": "payment_...", 
    "amount": 49, 
    "status": "succeeded"
  }
}
这次请求的响应时间应该在40毫秒加上750毫秒左右,总计约790毫秒。

在较小的规模上,我们就已经遇到了问题

想象一下,有用户在生产环境中报告了这样的问题:“创建订单的速度很慢。”

  • 你可以重现这个问题:POST /orders ≈ 800ms

  • 那么问题就来了:“这800毫秒的时间去哪了?”当然,我们知道答案,因为是我们编写了这些有问题的代码。

但假设我们并不知道原因,或者系统的架构看起来是这样的:

OrdersService
    |
    +-- PostgreSQL
    |
    +-- Redis
    |
    +-- 库存服务
    |
    +-- 税务服务
    |
    +-- 支付服务提供商
    |
    +-- 防欺诈服务

现在该怎么办呢?这时,可观测性就显得非常重要了。

基本解决方案:日志记录

最初,我们可以添加如下代码:

console.log('开始查询库存信息');
await this.inventoryService.reserveproductId);
console.log('库存查询完成');
console.log('开始进行支付操作');
await this.paymentsService.charge(amount);
console.log('支付完成');

此时,我们的日志内容可能会是这样的:

开始查询库存信息
库存查询完成
开始进行支付操作
支付完成

从这些日志中,我们可以推断出库存查询的速度相对较快,而支付操作则耗时较长。但请注意,我们实际上是手动在应用程序的逻辑代码中添加了这些日志记录功能。随着应用程序规模的扩大,这种做法会变得越来越繁琐且成本高昂。

而且,我们仍然没有为这些请求创建结构化的记录格式。

跟踪信息能为我们提供什么

跟踪信息可以帮助我们了解程序的执行流程:

POST /orders
│
└── OrdersController.create()
    │
    └── OrdersService.createOrder()
        │
        ├── InventoryService.reserve()
        │
        └── PaymentsService.charge()

现在,如果为这些操作添加时间戳,日志内容就会变成这样:

POST /orders                       ~790ms
│
└── OrdersController.create        ~790ms
    │
    └── OrdersService.createOrder  ~790ms
        │
        ├── InventoryService        ~40ms
        │
        └── PaymentsService        ~750ms

问题就变得清晰了:导致系统运行缓慢的原因并不是未知的,而是支付操作耗时过多。

这就是“拥有日志记录”与“真正理解程序的执行流程”之间的区别。

让我们实际运用可观测性技术吧

通过执行`$ nest new`命令,你可以选择让代码默认被添加跟踪功能。如果你选择了这个选项,那么你可能会看到类似这样的错误信息:

[Nest] 87392  - 09/13/2026, 9:09:11 PM   ERROR [ObserveAgentWorker] 错误:遥测请求被拒绝(401错误)。请检查appKey和appSecret是否有效;应用程序的相关凭据是在启动时一次性读取的,因此如果不重启系统,这个问题将无法得到解决——后续再次发生的类似错误只会被记录下来,而不会被重新处理。

现在,让我们通过手动为NestJS应用程序添加监控功能来尝试同样的方法,首先来安装Observe插件:

$ npm install @nestjs/observe

Observe插件提供了各种辅助工具,可以帮助我们将它的监控功能集成到NestJS应用程序中。接下来,请在src文件夹下创建一个名为src/observe.ts的文件,并填写以下内容:

import { createObserveModule } from '@nestjs/observe';

export const {
  ObserveModule,
  ObserveInstrument,
} = createObserveModule();

这样我们就获得了所需的NestJS集成功能。

配置Observe插件

请更新app.module.ts文件:

import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { OrdersModule } from './orders/orders.module';
import { InventoryModule } from './inventory/inventory.module';
import { PaymentsModule } from './payments/payments.module';
import { ObserveModule } from './observe';

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),
    // 我们从环境配置文件中读取秘钥信息——这是一个异步操作,因此使用了forRootAsync()
    ObserveModule.forRootAsync({
      inject: [ConfigService],
      // useFactory会在Nest加载完.env文件后再读取环境变量值,而不会在@Module装饰器首次运行时就进行评估
      useFactory: (config: ConfigService) => ({
        appKey: config.getOrThrow('OBSERVE_APP_KEY'),
        appSecret: config.getOrThrow('OBSERVE_APP_SECRET'),
        serviceId: config.getOrThrow('OBSERVE_SERVICE_ID'),
      }),
    }),
    OrdersModule,
    InventoryModule,
    PaymentsModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

在这个配置过程中,发生了三件事。

ConfigModule.forRoot({ isGlobal: true })会读取我们的环境变量,并使ConfigService在整个应用程序中都能被使用。

ObserveModule.forRootAsync()指示Nest使用工厂函数而不是固定对象来初始化Observe监控功能。Nest会将ConfigService注入到这个工厂函数中,而我们只有在配置信息被完全加载之后才会读取Observe相关的凭据信息。

最后,serviceId为这些监控数据提供了一个唯一的标识符,这样Observe平台就能知道这些数据属于哪个应用程序或服务。

使用getOrThrow()也是有意为之的。如果这些必需的配置值中有任何一项缺失,应用程序在启动时就会明显出现错误,而不会在配置不完整的情况下仍然默默地运行下去。

这些凭据信息可以在创建服务时从Observe平台获取。请访问Observe平台来获取所需的凭据。如果你是Nest生态系统的新手,建议仔细阅读代码片段中的注释说明。

将它们添加到你的环境中:

OBSERVE_APP_KEY=...
OBSERVE_APP_SECRET=...

把它们当作其他应用程序的秘密信息一样来处理。

为Nest应用程序添加监控功能

现在更新`main.ts`文件:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ObserveInstrument } from './observe';

async function bootstrap() {
  const app = await NestFactory.create(AppModule, {
    instrument: ObserveInstrument,
  });
  await app.listen(process.env.PORT ?? 3000);
}
void bootstrap();

这才是关键所在。我们并没有手动为`OrdersController.create()`或`OrdersService.createOrder()`添加监控代码,而是让NestJS框架根据其自身的执行机制来自动实现监控功能。

正是这种做法使得这种方法与在各个地方随意添加日志记录的方式有所不同。

生成一些请求流量

重新启动应用程序:

for i in {1..20}; do
  curl -s -X POST http://localhost:3000/orders \
    -H "Content-Type: application/json" \
    -d '{"productId":"book-123","amount":49}' \
    > /dev/null
done

现在查看Observe监控面板,它的显示效果应该与下图相同:

图3:Observe监控面板中关于/orders端点的请求统计信息

Observe监控面板展示了与/orders端点相关的请求统计信息,让我们能够了解请求处理过程中的哪些环节耗时最长。

随着产品的不断发展,其用户界面和显示内容也可能会发生变化。现在我们可以回答这样一个问题:“在总共800毫秒的处理时间内,究竟是哪个环节耗费了最多的时间?”在实际应用环境中进行测试时,这一问题的意义会更加明显。

真正重要的是监控面板本身

人们很容易就此认为:“太棒了,NestJS现在已经具备监控功能了。”但实际上,我们所看到的远不止这些。真正有趣的地方在于这种监控机制所涉及的概念。

我们关注的不仅仅是HTTP、Node.js或数据库这些技术层面,更重要的是那些直接存在于我们的NestJS应用程序中的组件,比如控制器、服务等等。

这就是框架感知型监控功能的真正价值所在——因为框架本身就已经理解了这些组件的工作原理,而Observe工具就可以利用这一优势来生成相应的监控数据。

现在让我们通过将请求超时时间从750毫秒改为2500毫秒,来让这个应用程序真正遇到一些问题吧。

再次生成一些请求流量。现在,处理这些请求所花费的时间应该大约为40毫秒加上2500毫秒,总计约2540毫秒。我们就假设自己正在排查一个生产环境中的问题吧。

不要直接查看源代码。首先应该从追踪日志入手,问问自己:“为什么/orders这个接口需要花费2.5秒钟才能完成请求处理?”

通过这些追踪日志,你应该能够追踪到从POST /orders开始,经过OrdersService.createOrder,最终到达PaymentsService.charge这一整个处理流程,从而找出导致性能下降的环节。这就是良好的可观测性应该能够帮助我们做到的。

如何深入分析追踪日志

自动监控与手动监控的区别

自动监控功能非常强大,但它并不能理解所有情况。Nest虽然能够覆盖HTTP请求、控制器等各个层面,但它无法自动识别出某个函数对您的业务来说究竟有多重要:

async calculateCheckoutDiscount(cart: Cart) {
  // 复杂的业务逻辑处理
}

也许某个操作确实需要花费400毫秒的时间,而且它是整个结账流程中最为关键的部分之一,但框架本身并不一定能自动意识到这一点。

这时,手动监控就派上了用场。

一般来说,它们的关系是这样的:

自动监控
            +
手动监控
            =
有用的应用性能数据

自动监控为我们提供了基础的数据框架,而手动监控则能够为这些数据增添具体的业务意义。

不过这里有一个容易犯的错误:一旦开始使用追踪工具,人们就会忍不住想要为每一个函数都添加监控代码:

functionA()
functionB()
functionC()
functionD()
functionE()
functionF()

但这样做其实是不对的。并不是所有的功能都需要被监控。我们的目标不是创建出最庞大的追踪日志,而是获得有用的性能数据。

通常来说,以下这些情况适合进行手动监控:

  • 那些耗时较长、成本较高的业务操作

  • 对外部服务的调用

  • 关键的业务流程

  • 计算量较大的任务

  • 延迟时间对用户体验有重要影响的操作

  • 重要的业务事件

例如:

checkout.calculatePrice
fraud.evaluate
payment.authorize
invoice.generate

这些函数名称本身就蕴含着具体的意义。

当追踪日志不够用时,该如何使用指标数据

假设我们想知道系统中创建了多少笔订单,那么追踪日志并不是分析这个问题的最佳工具。

我们需要的是指标数据,比如orders.created、payments_FAILED或者checkout.duration。这些指标才能帮助我们了解整体的运行情况。

例如:

orders-created_total
    12,430
paymentFailures_total
    183
checkout_latency_p95
    840ms

现在我们可以提出这样的问题,并得到相应的答案:“在凌晨2点进行那次更新之后,系统性能是不是变差了?”

“跟踪信息”可以用来解答“某个特定操作发生了什么?”,而“指标数据”则能说明“整个系统中正在发生什么”。这两种方式都非常有用,尤其是当你的应用程序开始获得发展势头时。

错误也是一种监控数据

请看以下代码:

try {
  await this.paymentsService.charge(amount);
} catch (error) {
  return {
    status: 'pending',
  };
}

从应用程序的角度来看,这个异常已经被处理掉了。但从运营管理的角度来看,我们仍然非常关心这种异常的发生。

这就引出了一个重要的区别:应用程序的正确性与运营管理的可见性并不总是一致的。

某个错误可能被应用程序处理掉了,但它仍然对整个运营过程具有重要意义。因此,监控系统需要同时记录那些被处理的故障以及那些未得到处理的崩溃事件。

超越HTTP:追踪应用程序中的各种操作流程

到目前为止,我们只关注了HTTP请求。不过,真正的NestJS应用程序通常不会仅限于处理HTTP请求。它们往往还会包含BullMQ作业、定时任务、RPC处理程序、GraphQL解析器以及其他形式的后台工作。Observe项目目前已经为这些特定于NestJS的执行路径提供了自动监控功能。

以订单处理流程为例:最初的HTTP请求用于创建订单,随后会将支付操作放入队列中:

POST /orders -> 创建订单 -> 将支付操作加入队列 ->> 通过BullMQ传递给支付处理程序

现在假设有用户反馈:“我的订单已经创建好了,但支付过程却花费了十分钟。”实际上,HTTP请求本身可能只在几百毫秒内就完成了。而这个延迟可能是由队列中的等待时间、支付处理程序的运行时间、下游服务的响应时间,或是支付提供商的原因造成的。

这种调试问题与单纯查找响应速度慢的HTTP接口是完全不同的。当某个逻辑操作需要跨越多个进程边界时,跟踪功能就显得尤为重要了——因为整个操作过程中关键的部分可能发生在原始请求完成之后。

在这种情况下,上下文信息就变得非常关键了。一个操作在系统中移动的过程中,会携带诸如跟踪ID、跨度ID、请求ID、服务名称、运行环境以及区域等信息:

HTTP请求
      |
      +---- 服务A
      |
      +---- 队列
              |
              +---- 处理程序
                      |
                      +---- 服务B

与其手动浏览每个服务的日志来判断哪些记录属于同一个操作,分布式跟踪技术为我们提供了一种将这些相关操作关联起来的方法。这就是分布式跟踪机制以及OpenTelemetry的上下文传播模型所追求的核心目标之一。

当应用程序本身成为问题时,可观测性还有另一个重要的维度需要考虑。

想象一下,一条追踪记录显示了以下信息:

POST /orders
≈ 2秒

但是并没有数据库查询缓慢、外部API响应迟缓、队列延迟,也没有明显的依赖问题。那么,如果问题是出在Node.js进程本身上呢?

这时我们就需要另一种类型的监控数据。我们可以询问这个进程是否受到了CPU资源的限制、内存使用量是否在增加、垃圾回收是否导致了延迟、事件循环是否处于高压状态,或者某个函数是否消耗了过多的CPU资源。

这就是运行时指标和性能分析发挥作用的地方。Observe目前的功能包括运行时指标和CPU性能分析,除此之外还提供了应用程序监控工具。因此,应用程序的可观测性并不一定局限于HTTP请求本身;有时候,问题实际上出在运行时环境本身。

还有一个重要的限制需要记住:一条追踪记录并不能自动帮助我们找到问题的根本原因。

假设我们看到这样的数据:

POST /orders       3秒
PaymentsService    2.9秒

我们虽然确定了问题发生的环节,但并不一定了解其原因。 PaymentsService运行缓慢的原因可能有很多:支付服务提供商的反应速度慢、数据库查询耗时过长、网络延迟增加、连接池已满,或者该服务正在尝试重试某些操作。

可观测性可以帮助我们缩小排查范围,但它并不能替代工程师的判断力。一个优秀的可观测性系统可以让问题排查过程更快,但并不能使排查工作变得完全没有必要。

Observe在NestJS、OpenTelemetry及更广泛的生态系统中扮演的角色

在这个阶段,将Observe放在更大的可观测性体系框架中来看待是很有意义的。

Observe和OpenTelemetry并不是两种可以互换的产品。OpenTelemetry是一个与特定供应商无关的可观测性框架和工具包,它为应用程序和基础设施提供了一种标准化的方式,用于生成、收集和导出监控数据。

一个简化的架构可能如下所示:

NestJS
   |
OpenTelemetry
   |
Collector
   |
   +---- Grafana
   +---- Jaeger
   +---- Datadog
   +---- New Relic
   +---- 其他后端服务

而Observe则具有更强的针对性:

NestJS
   |
Observe
   |
Observe平台

因此,两者之间的区别并不在于“其中一个具备追踪功能,而另一个没有”。一个更有意义的问题是:

在NestJS的执行模型中,有多少部分应该由监控系统自动识别和处理呢?

OpenTelemetry的最大优势在于其跨平台的兼容性。想象一下,如果一家企业同时运行着8个NestJS服务、2个Go语言服务、4个Python服务以及3个.NET服务,他们肯定不希望为每种服务都使用完全不同的可观测性解决方案。他们需要的是统一的监控数据格式、术语、后端接口以及运维流程。

这恰恰就是OpenTelemetry旨在解决的问题。

“Observe”则采取了不同的实现方式。想象这样一个组织:它拥有十二个基于NestJS构建的服务,而开发人员习惯使用控制器、提供者、模块、拦截器等NestJS相关的概念来进行开发。如果可观测性工具能够自动理解这些概念,那么开发者的工作体验将会变得更加便捷,因为遥测数据本身就是用工程师们已经熟悉的语言和框架来表达的。

这些不同的方法并不一定非得相互竞争。

一个合理的架构可能如下所示:

图4:展示NestJS、OpenTelemetry以及可观测性后端的架构图。

上述架构体现了分层设计的理念:NestJS提供了框架层面的支持,OpenTelemetry实现了标准的遥测功能与跨平台互操作性,而可观测性后端则负责数据的存储、可视化展示、警报触发以及数据分析等工作。

框架本身可以提供执行环境信息以及相应的监控工具;遥测标准则能确保数据格式的一致性、上下文的传递机制以及不同系统之间的兼容性;后端服务则负责数据的实际存储、查询、可视化处理以及告警通知等操作。

这种分工是合理的。

其实,将可观测性功能集成到应用程序框架和运行时环境中,并不是只有NestJS在这么做。

以.NET生态系为例,它的框架和运行时诊断机制都是围绕“活动日志”“指标”以及“日志记录”等概念来设计的,而OpenTelemetry则为这些数据的关联分析和导出提供了标准化的方式。

Java领域也有像Spring Boot这样的框架,以及Micrometer这样的工具库,它们都配备了成熟的可观测性功能。

Python则有Django和FastAPI等框架与OpenTelemetry进行了集成;Go语言的应用程序则通常会在HTTP、gRPC等技术中使用OpenTelemetry进行监控。

虽然各种实现方式有所不同,但总体趋势是相同的:可观测性功能越是贴近应用程序的实际执行机制,它就越能够自动捕获有用的信息。

这就是我们研究“Observe”这个工具的原因所在。NestJS并不是第一个推出可观测性功能的框架,而且针对特定框架开发的监控工具也无法取代整个生态系。相反,NestJS是在探索:对于在NestJS执行模型下工作的开发者来说,可观测性功能应该具备什么样的特性。

在这里确实存在一个权衡问题:如果一个可观测性系统对NestJS的理解越深入,那么它对使用NestJS的开发人员来说就越有用;但这种专业化也会导致该工具的通用性降低。

如果你的组织已经大量投资在了NestJS上,那么针对NestJS框架开发的监控工具可能会非常有用;但如果你需要管理多种语言编写的数十个服务,那么基于OpenTelemetry的通用架构可能更为合适。没有哪种方法能适用于所有环境,最终的选择还是取决于具体的需求和场景。

如何在实际应用中评估和利用观测功能

在做出相关决策时,还有一个很容易被忽视的因素:成本。

当人们比较各种可观测性解决方案时,他们往往会将决策简化为如下这样的对比:

工具A = X美元
工具B = Y美元
OpenTelemetry是免费的

但这种比较是不全面的。

一个可观测性解决方案的实际成本可能包括软件费用、基础设施建设成本、存储费用、开发人员的工作时间、维护费用、配置费用,以及处理各类问题的运营成本。

虽然OpenTelemetry本身是开源的,但如果你基于它构建一个可观测性平台,仍然需要有人来负责管理数据收集工具、存储系统、仪表盘、警报机制、数据保留策略、安全措施以及系统的升级工作。

使用托管型平台可以减轻部分基础设施方面的负担,但这种便利也是有代价的;相反,开源技术栈能让你拥有更多的控制权,不过运营这样的技术栈同样需要投入成本。

因此,在做出生产环境部署决策之前,务必先查看官方的定价信息。在撰写本文时,Observe项目宣传称其免费版本每月可处理30万条事件数据,这使得进行实验变得相对容易。但基于事件数量的计费方式引出了另一个重要概念:遥测数据的总量。

一个应用程序请求可能会产生HTTP请求、多个数据跟踪记录、日志信息、错误报告以及其他类型的遥测数据。在流量较大的情况下,这些数据的数量会迅速增加。因此,对于可观测性系统来说,也需要进行容量规划。

然而,并不是遥测数据越多就越好。

想象一下,如果为每个事件都添加以下这些信息:

userId
requestId
transactionId
tenantId
email

虽然这样能提供更多的上下文信息,但同时也可能会增加数据量、提高存储需求、引发隐私问题、导致费用增加,并使查询操作变得更加复杂。

有些信息绝对不能随意发送到可观测性平台中。在处理授权头信息、Cookie文件、密码、API密钥、令牌、支付信息和个人资料时,必须格外谨慎。

例如,以下这种做法是绝对不可取的:

span.setAttribute(
  'headers',
  JSON.stringify(request.headers),
);

这样做可能会导致你的遥测后端收到不必要的授权令牌或Cookie文件。

遥测数据就是普通的数据,应该像对待生产环境中的数据一样认真对待它。

在流量较大的情况下,采样机制也会变得非常重要。对于小型应用程序来说,记录所有数据可能是合理的;但对于那些每秒要处理数千个请求的系统而言,就需要减少需要保留的遥测数据量。

一种可行的策略可能是:

100%的错误日志
100%的慢速请求记录
10%的成功请求信息

具体的策略选择完全取决于应用程序的实际需求和运行环境。重要的是,要实现可观测性功能,确实需要投入相应的技术和资源。

那么,我究竟在什么情况下才应该认真考虑使用NestJS Observe呢?

对于那些基于NestJS构建的应用程序来说,它显然是一个合适的选择——尤其是当现有的可观测性基础设施还不完善时,如果团队希望获得有用的生产环境监控数据,却又不想自己花费精力去搭建和维护整个可观测性平台,那么NestJS Observe就显得非常有用。

当应用程序不仅仅使用传统的HTTP API,而是大量运用BullMQ、GraphQL、微服务、定时任务等其他框架能够识别的执行机制时,NestJS Observe的作用就会更加明显。

另外,如果开发人员习惯用NestJS相关的概念来思考和设计应用程序,那么使用NestJS Observe也会带来很多便利。例如:

OrdersController
OrdersService
PaymentService

如果遥测数据也能反映这些相同的概念,那么在出现问题时,开发人员在处理应用程序和可观测性系统之间的信息时,所需要消耗的认知资源就会减少。

不过,这并不意味着每个基于NestJS构建的应用程序都适合使用NestJS Observe。

如果你已经拥有一个完善的、基于OpenTelemetry的可观测性解决方案,其中包括数据收集工具、仪表盘、警报系统、服务水平目标监控功能、跟踪机制、指标统计功能以及跨服务的数据关联分析能力,那么再引入另一个平台反而可能会导致不必要的重复工作。

对于那些采用多种编程语言构建的应用程序来说,这一原则也同样适用。如果你的公司同时使用NestJS、.NET、Go、Java、Python和Rust等多种技术,那么一个标准化的遥测架构可能比特定于某种框架的便利性更为重要。

你还需要考虑自己的后端技术和部署需求。如果你的组织已经选择了一个成熟的可观测性平台,那么该平台的集成能力和数据导出功能就显得非常重要;而如果你出于合规性或架构方面的原因需要自行托管相关组件,那么也需要记住:一个开源的监控工具库并不意味着相应的托管型可观测性平台也能轻松实现自我托管。

最后,并不是所有的应用程序都需要复杂的高级可观测性功能。对于那些仅由一名开发人员维护、运营风险极低的小型内部API来说,使用完整的可观测性解决方案可能并不必要。

评估像NestJS Observe这样的工具,最好的方法并不是对比各种功能列表,而是通过实际实验来验证它的效果。

你可以选取一个具有代表性的应用程序,然后故意引入一些可控的故障情况,例如:

1. 数据库查询速度变慢
2> 外部API响应时间延长
3> 错误率增加
4> 需要大量CPU资源的操作
5> 后台任务执行速度变慢

然后,测量一个真正重要的指标:工程师需要花费多长时间才能找出问题的原因?

先尝试使用现有的技术栈,然后再试试新的技术栈。

这样的评估方式比单纯统计仪表盘的数量或指标数值要有意义得多。可观测性的真正价值在于它能够帮助团队更快地诊断问题。

如果现在解决一个问题的时间需要两小时,而使用了更好的遥测工具后这个时间缩短到了二十分钟,那么这种差异对团队来说就具有重大的实际意义。

最后的思考

Observe功能中最有趣的地方并不在于NestJS现在又多了一种生成追踪数据的方法——其实多年来我们本来就有其他方法可以做到这一点。 更值得关注的是,这个框架本身就可以成为可观测性体系的一部分。 想想NestJS已经掌握哪些概念:模块、控制器、管道、队列消费者等等。这些都不是随意编写的代码片段,它们代表着应用程序中具有实际意义的结构边界。 如果遥测系统能够自动理解这些结构边界,那么可观测性功能就会更符合开发者的思维模式。这样一来,就无需让工程师去学习另一种完全不同的方式来描述他们的应用程序,因为可观测性系统可以直接利用他们已经熟悉的概念来进行工作。 这无疑是一个非常重要的进步。 不过,框架的功能并不意味着所有与可观测性相关的工作都必须由该框架来完成。NestJS并不需要同时承担APM、日志聚合、指标后端以及分布式追踪等功能。 这些都属于不同的范畴。 框架在提供执行上下文、生命周期信息、组件边界以及框架级别的监控功能方面,确实具有得天独厚的优势。 遥测标准可以确保各种工具具备统一的语义结构、上下文传递机制、互操作性以及厂商中立性;而可观测性后端则能够负责数据的存储、查询、可视化展示、警报触发以及分析工作。 这种分工是非常有意义的,因为这样每一层都可以专注于自己最擅长的领域。 因此,如果你刚刚接触可观测性这个概念,我建议你可以先建立起这样一个简单的思维模型:
日志:发生了什么?
指标:这种情况发生的频率是多少?
追踪数据:这次操作过程中具体发生了哪些事情?
性能分析报告:运行时在哪些地方消耗了最多的资源?
基于框架的监控功能:框架本身已经掌握了关于这次操作是如何进行的哪些信息?
这就是它们之间的联系所在。 传统上,框架的主要作用是帮助我们构建应用程序。它们提供了路由管理、依赖注入、数据验证、中间件、身份认证、队列处理、调度等功能。 但框架同时也了解这些组件是如何被执行的。 这意味着同样的这些知识,也可以帮助我们诊断问题、分析程序性能、追踪操作流程、测量各项指标以及排除故障。 框架不仅知道应用程序的结构,还了解应用程序的实际运行行为。 而这种信息显然非常宝贵。

相关文章

技术实践

如何打破人工智能编程代理的循环机制

你很可能已经看过这样的情景:在使用人工智能编码工具构建的应用程序中,出现了某些故障。你让这个工具去修复这些问题,它似乎也“修复”了它们,但实际上错误依然存在,或者又出现了新的问题。 于是你会说“还是不行”。工具会再次尝试修复。二十分钟后,你会发现代码中的错误更多了,可用的功能也更少了,而且完全不知道该如何让应用程序恢复到正常状态。 人们常常用“人工智能反而使问题更加严重”或者“工具陷入了无限循环的修复模式”这样的表述来描述这种情况。这并不是某个特定产品的缺陷,像Replit、Cursor、Claude Code、Base44这类工具都会出现同样的问题,因为这种故障的本质是系统性的。 本教程将解

阅读全文
技术实践

如何在Kubernetes环境中诊断并解决AI推理过程中的延迟问题

现在是星期四,大约两点二十分。两周前,你们的团队推出了一款内部辅助工具。测试效果还不错,以至于财务部门的某个人询问这种工具是否能够阅读合同文件。消息很快传开了,今天,所有人第一次同时使用这款工具。 支持渠道收到了同样的投诉,但这些投诉是以六种不同的方式表达的。是系统出现故障了吗?我的设备只是一直在循环加载中……今天早上它还能正常工作啊。有人发了一张显示加载指示器的截图,但没有附上任何说明文字,看来他们觉得这样挺有意思的。 于是你打开了控制面板。 所有节点都已准备就绪,容器也在运行中,CPU使用率仅为20%。没有发生重启现象,也没有进入崩溃循环状态,也没有任何警报被触发。根据Kubernetes

阅读全文
技术实践

如何为您的开发团队制定端点数据丢失预防策略

开发人员的笔记本电脑中存储着比大多数人意识到的更多敏感数据:API密钥、数据库登录凭据、测试环境的配置信息,有时甚至还包括为了“测试”而下载的正式生产环境数据副本。 最终用户要为75%的内部数据丢失事件负责,而这些事件大多属于意外情况,并非恶意行为所致。对于开发团队来说,这种风险主要集中在终端设备上——也就是那些用于编写代码、进行测试以及部署代码的机器上。 以下是制定保护这些终端设备的策略的方法,而且这一策略不会影响团队的正常工作效率。 目录 如何制定终端数据丢失预防策略 步骤1:确定终端设备中敏感数据的存放位置 步骤2:将访问控制作为基础措施来实施 步骤3:强化终端设备的操作系统安全 步骤4

阅读全文
技术实践

如何负责任地使用Lovable产品

过去,开发应用程序往往就像在没有任何说明书的情况下组装家具,而且还会缺少一半的螺丝。如今,像Lovable这样的人工智能工具可以帮助你用简单明了的语言描述自己的需求,从而将一个想法转化为可运行的网页应用。 这确实很令人兴奋——但同时也意味着一种责任。 Lovable能帮助你快速行动、尝试各种想法,并创造出实用的软件。不过,速度绝不能取代周密的思考。由人工智能生成的程序可能会存在安全问题、导致用户使用体验混乱、包含不准确的信息,或者其代码在演示环境中可以正常运行,但在实际使用中却会出故障。 在这份指南中,你将学习到如何在实际使用Lovable的过程中兼顾安全性、隐私性、可访问性以及用户的安全。我

阅读全文