← 文章 / 编程开发
freeCodeCamp 5小时前 · 2026-10-06 02:31:51 · 6 阅读

NestJS Observe 实战指南:开发者可观测性手册

可观测性并不是什么新问题,NestJS 也绝不是第一个尝试解决它的生态。

但 NestJS Observe 之所以有意思,是因为它提出了一个更具体的问题:当可观测性理解自己正在观测的框架时,会发生什么?

在深入实现之前,我们先明确这个问题本身,以及框架上下文为什么重要。

可观测性问题

如果你开发和维护服务端应用足够久,很可能遇到过这种情况:API 不知为何变慢了,却搞不清楚原因。

翻日志,看不出什么异常。数据库正常运行,没有报错。接口在你本机上跑得好好的。

于是你加了几条日志:

11:30:02 AM LOG Starting order creation
11:34:11 AM LOG Inventory checked
11:34:12 AM LOG Payment started
11:39:03 AM LOG Payment completed

然后重新跑一遍请求。现在你大概知道时间花在哪里了,但与此同时,你也开始搭建自己的简易 tracing 系统了。

问题就出在这里。现代应用会产生海量的信息:日志、指标、traces、性能分析、错误、数据库查询、HTTP 调用、后台任务、队列消息等等。

真正的挑战未必是收集更多信息,而是能够回答一个简单得多的问题:

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

对于 NestJS 应用来说,这个问题格外有意思,因为 Nest 本身就对运行状态了如指掌。

它了解 controllers、providers、middleware、guards、interceptors 和 pipes,也知道 GraphQL resolver 什么时候被执行、微服务 handler 什么时候收到消息。

那么,如果让可观测性直接利用这些知识,会怎样?这正是 NestJS Observe 背后的理念,也是本文要探讨的内容。

不过,我们不打算把 Observe 仅仅当成又一个要安装的包,而是借它来理解一个更宏观的问题:框架感知的可观测性究竟能带来什么,又什么时候值得用?

我们会先从可观测性的基础讲起,搭建一个小型 NestJS 应用,制造一个真实的性能问题,用 Observe 对其进行埋点监控,最后再把这个方案与更框架中立的 OpenTelemetry 做个对比。

前置要求

想跟着做的话,你需要掌握基本的 TypeScript 和 NestJS 知识,并对 HTTP API 有一定了解。

不需要任何可观测性方面的经验,相关概念会在搭建过程中逐步介绍。

本文内容:

Observe 到底解决什么问题?

我们把问题具体化。想象这样一个 NestJS 应用:一个 POST /orders 请求进来了,耗时三秒。HTTP 客户端能告诉我们的就只有这么多。

Figure 1. A single API call can trigger more than one side effects

上图展示了一个 POST /orders 请求如何在应用内部触发多层处理,也说明了为什么光看请求耗时,我们并不知道时间都花在了哪里。

目前我们只知道:

POST /orders → 3 seconds

但这远远不够。我们想知道这三秒到底花在了哪里:

Controller → Service → Database

还是:

Controller → Service → External API

又或者是完全不同的原因:

Node.js process → CPU-heavy operation → Event loop contention

这正是可观测性要解决的问题。而 NestJS Observe 切入这个问题的方式很有意思:框架本身已经掌握了部分答案。

"可观测性"属于那种很容易被说得很虚的工程术语。一个实用的定义是:

可观测性(Observability)是指我们从系统暴露出的信息中,理解其内部状态和行为的能力。

这些信息被称为遥测数据(telemetry),最常见的遥测信号有:

  • 日志(logs)

  • 指标(metrics)

  • 链路追踪(traces)

  • 性能剖析(profiles)

它们各自回答不同的问题。

日志描述事件:发生了什么?

例如:

2026-08-31 14:21:04
Payment provider returned HTTP 502

这很有用,但如果你在排查某一个请求,可能还得手动把这条日志和其他发生的事情关联起来。

指标聚合信息:这件事发生的频率如何?

request_count = 1,240,231
error_rate = 2.4%
p95_latency = 840ms

指标非常适合观察趋势,让你能快速回答“昨天发布之后延迟是不是变高了?”这类问题。但单个指标通常无法完整还原一个请求的全貌。所以我们还需要“链路追踪”。

链路追踪跟踪一次逻辑操作:这次操作的过程中发生了什么?

例如:

POST /orders
│
├── authentication
├── OrdersController.create()
├── InventoryService.reserve()
├── PaymentService.charge()
└── Payment API

现在我们能看到完整的执行路径。更重要的是,还能看到每个单独操作各自花了多长时间。

接下来是性能剖析。它回答的是另一个问题:运行时的资源花在哪里了?假设某个接口很慢,但又没有明显的慢数据库查询或 HTTP 请求。

CPU 剖析结果可能会显示:

CPU
│
├── JSON serialization
├── application code
├── garbage collection
└── cryptographic operations

这四种信号相辅相成。

下面是一个很有用的思维模型:

Figure 2: Observability as a set

这张图把主要的可观测性信号(日志、指标、链路追踪和性能剖析)归在一起,展示了每种信号如何从不同视角观察应用行为,以及它们如何互相补充。

实际上,并不是每个应用都需要所有信号。关键在于拥有足够的信息来回答运维层面的问题。

从应用监控到框架感知的可观测性

传统方案为我们提供了强大且标准化的遥测数据采集方式。但框架本身早已了解应用的结构和执行方式。如果可观测性能利用这些上下文,而不是把应用当作一个普通进程来对待,会怎样?

Observe 出现之前

要排查响应缓慢的 /orders 接口,我们可以采取几种方式。最简单的就是打日志:

console.log('Starting payment');
await paymentService.charge();
console.log('Payment completed');

这在应用规模较小时没问题。之后我们可能会引入指标(metrics):

payment_latency_ms
orders_created_total
payment_failures_total

这样就能理解大量请求的整体行为了。接着我们可能会引入分布式追踪:

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

这正是 OpenTelemetry 变得尤为重要的地方。OpenTelemetry 是一个厂商中立的可观测性框架和工具集,用于生成、采集和导出遥测数据。

它刻意不做可观测性后端。这个区别稍后会再讨论,因为在对比 OpenTelemetry 和 Observe 时,这一点很重要。

那为什么还需要 NestJS Observe?

这才是有意思的地方。通用的插桩层只能观测到一个 HTTP 请求,而 NestJS 知道这个请求经过了应用中的哪些特定结构。

比如:

HTTP Request → Middleware → Guard → Interceptor → Pipe → Controller → Provider

这些并不是随意的 JavaScript 函数,而是 Nest 自己能理解的概念。因此,一个贴近框架的可观测性系统,就能获取到潜在非常有价值的信息。

与其只能看到:

HTTP GET /users/42 (took 4s to execute)

我们还能进一步理解:

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

这个区别正是框架级插桩值得关注的原因。

框架感知的可观测性

给这个思路起个名字吧。框架感知可观测性(Framework-aware observability)指的是:埋点工具理解框架自身的执行模型,而不是把应用当成一个普通的进程来看待。

传统模式大致是这样的:

Application
    ↓
Generic instrumentation
    ↓
OpenTelemetry
    ↓
Collector
    ↓
Observability backend

而框架集成模式的流程更像是这样:

NestJS application
    ↓
NestJS lifecycle
    ↓
Framework-aware instrumentation
    ↓
Telemetry
    ↓
Observability backend

区别不一定在于最终的遥测数据格式,而在于埋点工具能自动理解多少上下文。

这正是 Observe 登场的地方。

认识 NestJS Observe

NestJS Observe 是专为 NestJS 应用打造的官方可观测性工具,项目开源。它的目标不是重新发明日志、指标或追踪——这些方案早已存在,而且效果不错。它的价值在于把可观测性与 NestJS 的应用模型深度整合。

根据当前的项目文档,它能自动对多条 NestJS 执行路径进行埋点,包括:

  • HTTP

  • GraphQL

  • 微服务 / RPC

  • BullMQ

  • 定时任务

它还提供运行时指标和性能分析能力,这让它值得我们实际测试一番。

不过在继续之前要注意:Observe 在 NestJS 生态中还比较新,它的 API、支持的集成方式、定价和功能都可能快速变化。

好了,废话不多说,动手做点东西吧。

不过先等等。如果你想先看看实际效果再自己搭应用,NestJS 现在有一个公开的 Observe 演示面板,可以直接体验。它基于一个繁忙服务的生成数据集运行,无需注册账号或安装任何东西,就能浏览请求、追踪、瀑布图、错误、后台任务和告警。

建议先花几分钟点开看看再继续。特别是打开一个 request 或 trace,从高层操作一路追到它执行的具体工作。这比看截图或功能列表更能让你直观理解我们这个示例要做的事情。

我们的实战项目

我们要构建一个简单的 checkout API,它包含三个小组件:

  • Orders:接收 HTTP 请求,协调整个 checkout 流程。

  • Inventory:模拟预占商品库存。

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

整体流程见图 1。

Orders 组件位于请求的中心,Inventory 和 Payments 则代表执行路径上更下游的工作。这样的结构刚好能构造一个真实的慢请求,之后就能验证 Observe 能否告诉我们时间都花在了哪里。

支付服务会被故意设置得很慢。

我们的目标是:在不手动埋点的情况下,找出请求耗时都花在了哪里。

我们不是要做一个电商平台,只是构造刚好够用的复杂度,来复现一个真实的可观测性问题。

跟着做的话,你需要一个较新的 Node.js 和 NestJS CLI。不需要了解 OpenTelemetry,不需要现有的 APM 平台,也不需要一个生产环境的应用。

创建 NestJS 应用

先创建一个常规的 NestJS 项目:

$ npm i -g @nestjs/cli # if not installed yet
$ nest new nest-observe-demo
$ cd nest-observe-demo

nest new 命令会提示 "Would you like to enable auto-instrumented observability (@nestjs/observe)?",出现提示时选 yes 😀

启动项目:

$ npm run start:dev

然后用 Nest CLI 生成各模块:

$ nest g module orders
$ nest g controller orders
$ nest g service orders

$ nest g module inventory
$ nest g service inventory

$ nest g module payments
$ nest g service payments

项目结构大致如下:

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 reserve(productId: 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 this.inventoryService.reserve(productId);

    const payment =
      await this.paymentsService.charge(amount);

    return {
      id: `order_${Date.now()}`,
      productId,
      amount,
      inventory,
      payment,
    };
  }
}

注意一个重要细节:这里没有任何可观测性代码,只有纯粹的业务代码,至少目前是!

创建控制器

在生成的 controller 文件里填入以下内容:

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(
      body.productId,
      body.amount,
    );
  }
}

这个 controller 特意写得很薄:它只负责接收 HTTP 请求、提取 productId 和 amount,然后把真正的工作交给 OrdersService 处理。

OrdersService 是通过 NestJS 的依赖注入(DI)由 controller 的构造函数提供的。换句话说,我们不会自己 new OrdersService(),而是由 Nest 在应用启动时创建这个服务并注入给 controller。这样 controller 可以专注于处理 HTTP 相关的逻辑,依赖管理则交给 Nest。

下面的 module 配置就是让依赖注入能够跨 module 生效的关键。OrdersModule 提供 OrdersService,并导入导出了 InventoryService 和 PaymentsService 的模块。这些 exports 让这些服务可以被导入它们的模块使用。这就是本示例需要的全部 NestJS DI 知识。相关链接仅在你想深入了解依赖注入机制时才有必要阅读。

请确保你的模块正确 import 并 provide 所需的服务。你可以在这里进一步了解 Nest 的 DI。

比如下面这段代码:

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 {}

记得在对应模块中 export 这些服务,这样 OrdersModule 才能使用它们:

@Module({
  providers: [InventoryService],
  exports: [InventoryService],
})
export class InventoryModule {}

以及:

@Module({
  providers: [PaymentsService],
  exports: [PaymentsService],
})
export class PaymentsModule {}

最后,把 OrdersModule 导入到 AppModule 中。如果你用的是 CLI,这一步会自动完成;但如果没有用 CLI,就手动检查一下。

测试应用

在加入可观测性之前,先确认应用本身能正常运行。这个请求会走完整个下单路径:controller 接收请求,OrdersService 依次调用 Inventory 和 Payments,最后 API 返回合并后的结果。

大约 790ms 的响应时间是刻意为之的:这样等 Observe 启用后,我们就有了一个具体的性能问题可以排查。

再次启动应用,运行 $ 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"
  }
}

这个请求耗时大约 40ms + 750ms ≈ 790ms。

在更小规模下问题已经出现了

想象一下,有用户在生产环境反馈:"下单很慢。"

  • 你能复现这个问题:POST /orders ≈ 800ms。

  • 接着问题来了:"这 800ms 都花在哪了?"当然,我们知道答案,因为这个烂代码就是我们自己写的。

但假设你不知道,或者这个服务实际上长这样:

OrdersService
    |
    +-- PostgreSQL
    |
    +-- Redis
    |
    +-- Inventory service
    |
    +-- Tax service
    |
    +-- Payment provider
    |
    +-- Fraud service

现在怎么办?这正是可观测性发挥价值的地方。

最基础的方案:日志

一开始,我们可以加上:

console.log('开始处理库存');
await this.inventoryService.reserve(productId);
console.log('库存处理完成');
console.log('开始支付');
await this.paymentsService.charge(amount);
console.log('支付完成');

这时日志可能是这样的:

开始处理库存
库存处理完成
开始支付
支付完成

我们可以推断出库存处理相对较快,而支付很慢。但注意我们做了什么:在业务逻辑周围手动添加了埋点代码。随着应用规模增长,这种做法的成本会越来越高。

而且我们依然没有为请求建立起结构化的表示。

Trace 能带给我们什么

Trace 会给我们一棵执行树:

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

再想象一下在树上附上耗时数据:

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

问题一目了然:这个接口不再"莫名地慢",而是支付操作占用了大部分时间。

这就是"有日志"和"理解执行过程"之间的区别。

让 Observe 派上用场

使用 $ nest new 命令创建项目时,你本可以选择默认开启代码埋点。如果你当时接受了这个选项,可能会见过类似这样的报错:

[Nest] 87392  - 09/13/2026, 9:09:11 PM   ERROR [ObserveAgentWorker] Error: Telemetry rejected (401). Check that appKey and appSecret are valid; the application is taken from the key. Credentials are read once at start-up, so this will not recover without a restart - further rejections are counted, not logged.

下面我们手动完成同样的事情,为 NestJS 应用接入埋点,首先安装 Observe:

$ npm install @nestjs/observe

Observe 包提供了将埋点集成到 Nest 应用的辅助工具。接下来在 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 }),
    // 我们从 env 文件加载密钥——这是异步操作(所以要用 forRootAsync()
    ObserveModule.forRootAsync({
      inject: [ConfigService],
      // useFactory 会把读取环境变量的时机推迟到 Nest 加载 .env 之后,而不是在 @Module 装饰器首次执行时就对 process.env 求值
      useFactory: (config: ConfigService) => ({
        appKey: config.getOrThrow<string>('OBSERVE_APP_KEY'),
        appSecret: config.getOrThrow<string>('OBSERVE_APP_SECRET'),
        serviceId: config.getOrThrow<string>('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 集成基于框架自身的执行模型为应用完成插桩。

这正是这种做法与到处添加日志语句的本质区别。

制造一些流量

重启应用:

$ npm run start:dev

发起几个请求,模拟一定的负载:

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 控制台,应该能看到类似下面截图的界面:

Figure 3: /order request metrics from the observe dashboard

Observe 控制台展示了 /orders 端点的遥测数据,让我们可以总览请求活动和延迟情况,进而排查请求链路中哪个环节最耗时。

具体 UI 和可用视图可能随产品迭代而变化。现在我们已经能回答这个问题了:“800ms 里到底哪个代码块占用了大部分时间?”在真实应用负载下,这个问题会更有意思。

真正重要的不是控制台

到这里很容易就想停下来说:“不错,NestJS 有 tracing 了。”但这其实低估了我们看到的东西。真正有意思的是它的词汇体系。

我们看到的不再只是 HTTP、Node.js 或 db 这类通用概念,而是 NestJS 应用里真实存在的概念:controller、provider、service 等等。

这就是框架感知插桩(framework-aware instrumentation)的潜在价值所在。框架本来就了解这些组件,Observe 可以借助这种理解来生成遥测数据。

接下来把 timeout 从 750 改成 2500,让应用真正出点问题。

再次生成一些流量。现在这个端点大约需要 40ms + 2500ms ≈ 2540ms。假装我们正在排查一起生产事故。

不要看源代码,从 trace 入手,问自己:“为什么 /orders 要花 2.5 秒?”

通过 trace,你应该能从 POST /orders 追溯到 OrdersService.createOrder,再到 PaymentsService.charge,从而定位到慢操作。这正是优秀的可观测性应该支撑的工作流程。

如何超越第一条 Trace

自动插桩 vs 手动插桩

自动插桩很强大,但它不可能理解一切。Nest 知道 HTTP、controller 这些东西,但它不会自动知道某个函数对你的业务有多重要,比如这个:

async calculateCheckoutDiscount(cart: Cart) {
  // complicated business logic
}

这个操作可能耗时 400ms,而且是结算流程中最关键的环节之一,但框架未必能推断出来。

这时手动插桩就派上用场了。

总的思路是:

Automatic instrumentation
            +
Manual instrumentation
            =
Useful application telemetry

自动插桩搭起骨架,手动插桩赋予应用层面的具体含义。

这里有个容易踩的坑:一旦接触到 tracing,很容易忍不住给每个函数都加上插桩:

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

但不要这么做。别把所有东西都插桩。目标不是造出最大的 trace,而是造出有用的遥测数据。

适合手动插桩的对象通常有:

  • 开销大的业务操作

  • 外部调用

  • 关键工作流

  • 耗时较长的计算

  • 对延迟敏感的操作

  • 重要的业务事件

比如这样的命名:

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

这些名字本身就带着业务含义。

指标:光有链路追踪还不够

假设我们想知道创建了多少订单。这时候用 trace 并不合适。

我们需要的是指标(metric),比如 orders.created、payments.failed 或 checkout.duration。指标给我们的是全局的聚合视图。

例如:

orders.created_total
    12,430
payment.failures_total
    183
checkout_latency_p95
    840ms

有了这些,我们就能提出并回答这样的问题:“凌晨 2 点发布新版本之后,系统是不是变差了?”

trace 回答的是“这个特定操作发生了什么?”,而 metrics 回答的是“整个系统正在发生什么?”。两者都有价值,尤其当你的应用开始有真实流量时。

错误也是遥测数据

看看下面这段代码:

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

从应用的角度看,这个异常已经被处理了。但从运维的角度看,异常本身的发生可能仍然是我们非常关心的。

这就引出一个重要的区分:应用层面的正确性和运维层面的可见性并不总是一回事。

一个错误可能已被应用妥善处理,但同时对运维来说依然重要。这就是为什么可观测性不仅要覆盖未捕获的崩溃,也要覆盖被处理过的失败。

不止 HTTP:追踪应用内的完整工作流

到目前为止,我们只看了 HTTP 请求。而真实的 NestJS 应用很少只有 HTTP。它们往往还包含 BullMQ 任务、定时任务、RPC handler、GraphQL resolver 等各种后台工作。Observe 项目目前已经为其中若干 Nest 特有的执行路径提供了自动埋点。

以一个订单处理流程为例:初始的 HTTP 请求创建订单,然后把支付任务放入队列:

POST /orders -> Create order -> Queue payment -> BullMQ -> Payment worker

现在想象一下,有用户反馈:“订单创建了,但支付等了十分钟。”HTTP 请求本身可能几百毫秒就完成了,而延迟可能出在队列、worker、下游服务,或者支付服务商那里。

这和排查一个普通的慢 HTTP 接口完全是两回事。当一个逻辑操作跨越了多个进程边界时,链路追踪就特别有价值——因为问题的关键环节,可能发生在原始请求早已结束之后。

这时候上下文(context)就很重要了。一个操作在系统中流转时,可以携带 trace ID、span ID、request ID、服务、环境、地域等信息:

HTTP request
      |
      +---- Service A
      |
      +---- Queue
              |
              +---- Worker
                      |
                      +---- Service B

与其在每个服务的日志里手动翻找、试图拼出属于同一次操作的记录,分布式追踪为我们提供了一种把工作片段关联起来的方式。这正是分布式追踪和 OpenTelemetry 上下文传播模型背后的核心思想之一。

当问题出在应用本身时,可观测性还有另一个重要维度。

假设一条 trace 告诉你:

POST /orders
≈ 2 seconds

但没有慢查询、没有慢的外部 API、没有队列延迟,也看不出依赖服务有问题。如果问题出在 Node.js 进程本身呢?

这时候就需要另一种类型的遥测数据了:进程是不是 CPU 瓶颈?内存占用是否持续增长?垃圾回收是不是拖慢了延迟?事件循环是不是压力大?某个函数是不是在狂吃 CPU?

这正是运行时指标和性能分析(profiling)的用武之地。Observe 目前的功能除了应用插桩之外,还包括运行时指标和 CPU profiling。所以应用可观测性不必止步于 HTTP 请求本身——有时问题恰恰出在执行应用的运行时上。

还有一个重要局限值得记住:trace 不会自动告诉你根因。

假设我们看到:

POST /orders       3 seconds
PaymentsService    2.9 seconds

我们定位到了问题,但还没真正解释它。PaymentsService 变慢,可能是因为支付服务本身慢、某条数据库查询耗时过长、网络延迟升高、连接池耗尽,或者服务方正在重试某个操作。

可观测性能收窄排查范围,但不能替代工程判断。好的可观测性系统能加快调查速度,但不能让调查变得多余。

Observe 的定位:NestJS、OpenTelemetry 与更广阔的生态

到了这里,有必要把 Observe 放进更大的可观测性版图中来看。

Observe 和 OpenTelemetry 并不是两个可以随意互换的产品。OpenTelemetry 是一个厂商中立的可观测性框架和工具集,为应用和基础设施提供标准化方式来生成、采集和导出遥测数据。

一个简化后的架构大致如下:

NestJS
   |
OpenTelemetry
   |
Collector
   |
   +---- Grafana
   +---- Jaeger
   +---- Datadog
   +---- New Relic
   +---- other backend

Observe 则更“有主见”一些:

NestJS
   |
Observe
   |
Observe platform

所以有意思的区别并不是“一个有 tracing,另一个没有”。更值得问的问题是:

埋点工具应该自动理解多少 NestJS 的执行模型?

OpenTelemetry 的最大优势在于可移植性。想象一家公司运营着八个 NestJS 服务、两个 Go 服务、四个 Python 服务和三个 .NET 服务。你肯定不想要四套完全不同的可观测性架构,而是希望遥测数据、上下文、术语、后端和运维实践保持统一。

这正是 OpenTelemetry 设计出来要解决的问题。

Observe 做的是另一种取舍。想象一个拥有十二个 NestJS 服务的组织,开发者的思维天然围绕 controller、provider、module、guard、interceptor、resolver、队列这些 NestJS 概念展开。如果可观测性工具能自动理解这些概念,开发者体验就会更简单,因为遥测数据直接用工程师理解应用时已经在用的语言来表达。

这两种路线并不必然互相竞争。

一个实用的架构可以是这样的:

图 4:NestJS、OpenTelemetry 与可观测性后端的架构示意图

上图展示了一种分层架构:NestJS 提供框架层面的上下文,OpenTelemetry 提供标准化的遥测数据和互操作能力,可观测性后端则负责存储、可视化、告警和分析。

框架提供执行上下文和框架级的埋点,遥测标准提供统一的语义、上下文传播和互操作能力,后端提供存储、查询、可视化和告警分析,各司其职。

这种职责分离是合理的。

在把可观测性向应用框架和运行时靠拢这件事上,NestJS 并非孤例。

比如 .NET 生态,框架和运行时的诊断机制就围绕 Activity、指标和日志等概念构建,再由 OpenTelemetry 提供标准化的关联与导出方式。

Java 拥有围绕 Spring Boot 等框架和 Micrometer 等类库的成熟可观测性工具。

Python 对 Django、FastAPI 等框架都有 OpenTelemetry 集成,Go 应用则普遍在 HTTP、gRPC 及其他类库上使用 OpenTelemetry 埋点。

具体实现各有不同,但大方向是一致的:可观测性越贴近应用的执行模型,就越有可能自动捕获到更有价值的上下文。

这正是 Observe 值得关注的有趣之处。NestJS 并没有发明可观测性,框架专属的埋点也不是对整个生态的替代。它更像是一次框架层面的押注:让在 NestJS 执行模型中开发的开发者,获得更顺手的可观测性体验。

这里存在一个天然的权衡:可观测性系统对 NestJS 理解得越深,对 NestJS 开发者就越有用,但这种特化也会降低它的可移植性。

如果贵公司的技术栈重度依赖 NestJS,那么框架相关的上下文信息可能价值很大。但如果你们在多种语言上运营着几十个服务,一套基于 OpenTelemetry 的统一架构可能更重要。没有哪种方案天然适用于所有环境,关键看架构本身的需求。

如何在真实应用中评估 Observe

决策中还有一个容易被忽视的因素:成本。

人们在比较可观测性方案时,往往会把问题简化成这样:

工具 A = $X
工具 B = $Y
OpenTelemetry = 免费

这种比较并不完整。

一套可观测性方案的真实成本,可能涵盖软件、基础设施、存储、工程人力、维护、配置,以及处理事故的运营成本。

OpenTelemetry 本身是开源的,但如果要围绕它搭建一个可观测性平台,总得有人负责运营 collector、存储、仪表盘、告警、数据保留策略、安全和升级。

托管平台能减轻一部分基础设施负担,但这种便利是有代价的。反过来,开源技术栈给你更多控制权,但运营这套技术栈同样需要成本。

因此,在做出生产环境决策之前,务必对照官方最新的定价页面核实价格。截至撰写本文时,Observe 项目宣传的免费额度是每月 30 万条事件,这让上手试验的门槛相对较低。但按事件计费也引出了另一个重要概念:遥测数据量。

一个应用请求可能产生一次 HTTP 操作、多个 span、日志、错误以及其他遥测数据。在高流量的场景下,数据量会快速增长。所以可观测性也需要自己的容量规划。

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

想象一下你给每个事件都附上以下字段:

userId
requestId
transactionId
tenantId
email

你确实获得了更多上下文,但同时也可能带来了更高的基数(cardinality)、更大的存储需求、隐私风险、更贵的账单,以及更复杂的查询。

有些信息绝对不该随意发送到可观测性平台。尤其是 authorization headers、cookies、密码、API keys、token、支付信息和个人信息,务必格外小心。

举个例子,下面这种做法不能不加考虑地使用:

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

否则你可能把认证令牌或 Cookie 直接发送到遥测后端。

遥测数据也是数据,要像对待生产数据一样对待它。

当流量变大时,采样也会变得重要。对小应用来说,全量记录完全没问题;但一个每秒处理数千请求的系统,可能就需要减少保留的遥测量。

采样策略大概可以是:

错误请求 100% 记录
慢请求 100% 记录
成功请求记录 10%

具体策略完全取决于应用本身和它的运维需求。关键在于:可观测性本身也是需要认真设计的工程。

那么什么情况下值得认真考虑 NestJS Observe?

重度使用 NestJS 的应用显然是首选场景,尤其是还没有现成可观测性基础设施、团队又希望获得有用的生产环境可见性,而不想自己从零搭建并运维整套可观测性平台的时候。

当应用不只是常规 HTTP API,而是大量使用 BullMQ、GraphQL、微服务、定时任务等执行路径时,它就更有价值了——这些正是框架感知型埋点能够理解的场景。

如果开发者习惯用 NestJS 概念来思考应用结构,比如:

OrdersController
OrdersService
PaymentService

让遥测数据反映同样的概念,可以降低排查故障时在应用和可观测性系统之间来回切换的认知负担。

当然,这并不意味着每个 NestJS 应用都应该用它。

如果你已经有一套成熟的基于 OpenTelemetry 的可观测性体系,包含 collector、仪表盘、告警、SLO、tracing、metrics、日志以及跨服务关联,再引入一个平台就可能造成不必要的重复。

多语言混用的架构也是同样的道理。如果公司同时运行 NestJS、.NET、Go、Java、Python 和 Rust,一套标准化的遥测架构可能比框架层面的便利更重要。

你还需要考虑后端和部署需求。如果公司已经标准化使用某个可观测性平台,那么可用的集成和导出方式就很关键。如果出于合规或架构原因必须自托管,请记住: instrumentation 库开源,并不代表托管的可观测性平台就能自托管。

最后,并非每个应用都需要复杂的可观测性。一个只有一名开发者、运营风险很小的内部小 API,可能没必要上全套可观测性栈。

评估 Observe 这类工具,最好的方式不是对照功能清单,而是针对真实问题做一次实验。

找一个有代表性的应用,人为制造几个可控的故障:

1. Slow database query
2. Slow external API
3. Increased error rate
4. CPU-heavy operation
5. Slow background job

然后衡量一个真正重要的指标:工程师需要多久才能定位原因?

先用现有技术栈测一遍,再用新栈测一遍。

这比数有多少个仪表盘或指标有意义得多。可观测性的实际价值,更接近于诊断耗时。

如果今天排查一次故障要两小时,而更好的遥测能把时间缩短到二十分钟,这对团队来说就是实打实的运营价值。

结语

Observe 最有意思的地方,并不在于 NestJS 又多了一种生成 trace 的方式——这类能力我们早就有很多了。

更有意思的思路是:框架本身可以成为可观测性模型的一部分。

想想 NestJS 已经掌握了哪些信息:Module、Controller、Pipe、Queue consumer 等等。这些不是随意的代码片段,而是有意义的应用边界。

如果遥测能自动理解这些边界,可观测性就更贴近开发者的心智模型。工程师不必再学习一套全新的应用表示方式,可观测性系统可以直接建立在他们已经熟悉的概念之上。

这可能是件大事。

与此同时,框架感知并不意味着所有可观测性相关的事情都要塞进框架里。NestJS 没必要变成 framework + APM + 日志聚合 + 指标后端 + 分布式追踪的大杂烩。

这些是不同的关注点。

框架天生适合提供执行上下文、生命周期信息、组件边界和框架级别的埋点。

遥测标准可以提供统一的语义、上下文传播、互操作性和厂商中立性;可观测性后端则负责存储、查询、可视化、告警和分析。

这种分层很有价值,因为每一层都能专注于自己最擅长的事。

所以,如果你刚开始接触可观测性,我建议你先记住这个最简单的思维模型:

日志(Logs)
发生了什么?

指标(Metrics)
发生的频率有多高?

链路(Traces)
这次操作过程中发生了什么?

性能剖析(Profiles)
运行时把资源花在了哪里?

框架感知埋点
框架本身对这个操作的执行过程已经掌握了什么?

这就是它们之间的联系。

框架传统上的职责是帮我们构建应用——路由、依赖注入、校验、中间件、认证、队列、调度,以及其他各种构建模块。

但框架同时也了解这些模块是如何执行的。

这意味着,同样的知识有望帮助我们诊断、剖析、追踪、度量和调试所构建的应用。

框架不仅知道应用的结构,还掌握一些关于应用行为表现的信息。

而这些都是很有价值的信息。

原始来源: freeCodeCamp

评论 (0)