@nestjs/observe là gì? Cách NestJS giúp bạn nhìn thấy chuyện gì đang xảy ra trong production
Khi làm backend, có một tình huống gần như developer nào cũng từng gặp:
Local chạy bình thường.
Staging chạy bình thường.
Deploy production.
User báo API chậm.
Bạn mở log lên.
Không thấy error.
Bạn kiểm tra database.
Database vẫn chạy.
CPU server cũng chưa quá cao.
Và câu hỏi lúc này là:
Vậy cuối cùng nó chậm ở đâu?
Đây chính là bài toán mà Observability muốn giải quyết.
Và NestJS hiện đã có một giải pháp chính thức cho việc này:
npm install @nestjs/observe
Tên của package đó là:
@nestjs/observe
NestJS mô tả đây là SDK observability chính thức, có khả năng tự động theo dõi HTTP request, GraphQL, microservices, BullMQ jobs, cron jobs, traces, metrics và nhiều thông tin runtime khác.
Trước tiên: Observability là gì?
Có thể hiểu rất đơn giản:
Observability là khả năng nhìn vào bên trong hệ thống để biết nó đang làm gì.
Ví dụ bạn có API:
POST /registrations
User báo:
Đăng ký mất 4 giây.
Nếu chỉ dùng log thông thường, bạn có thể thấy:
POST /registrations
Registration completed
Nhưng bạn vẫn chưa biết:
4 giây đó nằm ở đâu?
Có thể là:
Controller
↓
Service
↓
Check player
↓
Query database
↓
Create registration
↓
Send email
↓
Push BullMQ job
Một bước nào đó có thể mất rất nhiều thời gian.
Observability giúp bạn nhìn thấy cả luồng đó.
Ví dụ:
POST /registrations 4.2s
RegistrationController.create 4.1s
│
├── PlayerService.find 40ms
│
├── RegistrationRepository.save 120ms
│
├── InvoiceService.create 180ms
│
└── EmailService.send 3.7s
Nhìn vào đây, bạn gần như biết ngay vấn đề:
EmailService.send = 3.7 giây
Không cần đoán.
Vậy @nestjs/observe làm gì?
Điểm đặc biệt của @nestjs/observe là nó không chỉ nhìn Node.js ở mức thấp.
Nó hiểu cấu trúc của NestJS.
Ví dụ một hệ thống monitoring thông thường có thể chỉ cho bạn biết:
POST /registrations
SQL query
HTTP request
Trong khi NestJS Observe có thể hiểu những thành phần như:
RegistrationController
RegistrationService
Guards
Interceptors
Resolvers
Queue consumers
Cron jobs
NestJS cho biết SDK này hook trực tiếp vào request lifecycle của framework, vì vậy telemetry có thể được biểu diễn theo chính các controller, provider và queue handler bạn viết.
Đây là khác biệt khá quan trọng.
Ví dụ thay vì nhìn thấy:
POST /api/registration
bạn có thể nhìn hệ thống theo logic của application:
RegistrationController.create()
RegistrationService.registerPlayer()
PaymentService.createInvoice()
Đối với developer, kiểu thông tin này hữu ích hơn rất nhiều.
Ví dụ thực tế
Giả sử bạn có:
@Post()
async register(@Body() dto: CreateRegistrationDto) {
return this.registrationService.create(dto);
}
Service:
async create(dto: CreateRegistrationDto) {
const player = await this.playerService.find(dto.playerId);
const registration = await this.registrationRepo.save({
playerId: dto.playerId,
eventId: dto.eventId,
});
await this.emailService.sendConfirmation(player);
return registration;
}
Một ngày user báo:
Registration API hôm nay rất chậm.
Trước đây bạn có thể bắt đầu thêm log:
console.time('find-player');
const player = await this.playerService.find(dto.playerId);
console.timeEnd('find-player');
Sau đó:
console.time('save-registration');
const registration =
await this.registrationRepo.save(...);
console.timeEnd('save-registration');
Rồi tiếp tục:
console.time('send-email');
await this.emailService.sendConfirmation(player);
console.timeEnd('send-email');
Code bắt đầu đầy những đoạn kiểu:
console.time()
console.log()
performance.now()
Và khi debug xong...
có khi bạn lại phải xóa chúng.
Với NestJS Observe
Ý tưởng là bạn instrument application một lần.
Ví dụ:
import { createObserveModule } from '@nestjs/observe';
export const {
ObserveModule,
ObserveInstrument,
} = createObserveModule();
Sau đó khai báo module:
@Module({
imports: [
ObserveModule.forRoot({
appKey: process.env.OBSERVE_APP_KEY,
appSecret: process.env.OBSERVE_APP_SECRET,
serviceId: 'backend-api',
}),
],
})
export class AppModule {}
Và ở bootstrap:
const app = await NestFactory.create(AppModule, {
instrument: ObserveInstrument,
});
SDK yêu cầu NestJS core từ 11.1.4 trở lên, nên project NestJS 11 tương đối mới đã có thể tích hợp, không nhất thiết phải nâng lên NestJS 12 trước.
Sau đó Nest có thể tự theo dõi gì?
Một trong những điểm hấp dẫn nhất là auto instrumentation.
Nó có thể theo dõi các loại workload như:
HTTP request
GraphQL
Microservices
BullMQ
Cron
Interval
Timeout
Ngoài ra SDK còn thu thập:
traces
runtime metrics
custom metrics
CPU profiles
errors
Nếu project của bạn có:
@Cron(...)
hoặc:
@Processor('email')
thì đây là thứ khá hữu ích.
Trace là gì?
Nếu chỉ nhớ một khái niệm về observability, hãy nhớ trace.
Trace có thể hiểu là:
Nhật ký hành trình của một request.
Ví dụ:
User
│
│ POST /registrations
▼
RegistrationController
│
▼
RegistrationService
│
├── MySQL
│
├── PaymentService
│
└── BullMQ
│
▼
EmailWorker
Toàn bộ hành trình đó có thể được coi là:
1 Trace
Bên trong trace có nhiều:
Span
Ví dụ:
Trace: POST /registrations
├── Controller 10ms
├── RegistrationService 380ms
├── MySQL INSERT 20ms
├── PaymentService 300ms
└── BullMQ enqueue 15ms
Mỗi đoạn là một span.
Nó giúp tìm API chậm như thế nào?
Giả sử API bình thường:
POST /registration
p50 = 120ms
p95 = 300ms
p99 = 600ms
Sau một release:
p50 = 150ms
p95 = 2.4s
p99 = 6.1s
Điều đó có nghĩa là phần lớn request vẫn tương đối ổn, nhưng một nhóm request bị chậm rất mạnh.
Bạn có thể mở một request chậm và nhìn waterfall:
RegistrationController.create
│
├── PlayerService.find
│ 35ms
│
├── RegistrationRepo.save
│ 40ms
│
└── PaymentGateway.create
4.8s
NestJS docs mô tả chính workflow này: route được gom theo route pattern, sau đó có thể kiểm tra percentile như p95 và mở từng slow execution để xem waterfall.
Đây chính là điểm observability mạnh hơn việc chỉ đọc log.
Không chỉ HTTP API
Đây có lẽ là phần mình thấy hữu ích nhất với backend NestJS thực tế.
Giả sử bạn có BullMQ:
@Processor('email')
export class EmailProcessor {
@Process()
async send(job: Job) {
// send email
}
}
Một job đôi lúc chạy:
30 giây
Bạn có thể muốn biết:
job nào chậm?
service nào chậm?
external API nào chậm?
Observe có instrumentation cho BullMQ.
Tương tự với cron job:
@Cron('0 11 * * *')
async sendBusBookingDigest() {
...
}
Bạn có thể theo dõi:
Cron started
↓ 120ms
Query bookings
↓ 840ms
Generate report
↓ 2.4s
Send email
Cron completed
Thay vì chỉ biết:
Cron ran successfully.
Metrics là gì?
Trace giúp xem một request cụ thể.
Metrics giúp nhìn toàn bộ hệ thống.
Ví dụ:
Request per second
CPU usage
Memory usage
Event loop delay
Error rate
Response time
Bạn có thể có dashboard kiểu:
Requests
12:00 150 req/min
13:00 210 req/min
14:00 900 req/min
và:
Error rate
12:00 0.1%
13:00 0.2%
14:00 8.4%
Ngay lập tức bạn biết:
14:00 có chuyện gì đó xảy ra.
Logs, Metrics và Traces khác nhau thế nào?
Có thể nhớ như sau:
Logs
→ Chuyện gì đã xảy ra?
Metrics
→ Hệ thống đang khỏe hay không?
Traces
→ Request đã đi qua những đâu?
Ví dụ:
LOG
Payment failed
cho biết có lỗi.
METRIC
payment_error_rate = 12%
cho biết lỗi đang xảy ra nhiều.
TRACE
Registration
↓
PaymentService
↓
PayPal API
timeout 5s
cho biết lỗi nằm ở đâu.
Ba thứ kết hợp lại mới tạo thành observability tốt.
Thế nó có giống Sentry không?
Có phần giống, nhưng trọng tâm khác nhau.
Sentry rất mạnh ở:
Exceptions
Stack traces
Error grouping
Release regression
Frontend errors
Còn observability/APM tập trung nhiều hơn vào:
Performance
Request tracing
Latency
Metrics
Infrastructure behavior
Distributed flows
Ví dụ:
API bị 500
Sentry rất hữu ích.
Nhưng:
API không lỗi,
chỉ từ 200ms tăng lên 4 giây.
Observability thường hữu ích hơn.
Trong thực tế hai thứ có thể tồn tại song song.
Điểm thú vị: không chạy collector trong request thread
Theo repository chính thức, telemetry được gửi thông qua một detached worker thread nhằm tránh làm request path chính phải chịu việc xử lý telemetry trực tiếp.
Có thể hình dung:
Request
Controller
↓
Service
↓
Database
↓
Response
không phải đợi:
send monitoring data
mỗi lần request hoàn thành.
Thay vào đó:
Application
│
└── telemetry
↓
Worker Thread
↓
Observe collector
Cách này giúp giảm ảnh hưởng của monitoring tới request chính.
Một điểm nữa khá hay: AsyncLocalStorage
Một vấn đề thường gặp trong NestJS là muốn theo dõi:
requestId
userId
traceId
tenantId
xuyên suốt nhiều service.
Ví dụ:
Controller
↓
Service A
↓
Service B
↓
Repository
NestJS Observe đã duy trì AsyncLocalStorage cho request, jobs và messages được instrument, đồng thời expose context đó thông qua TracerService.
Nghĩa là bạn có thể dùng trace context xuyên suốt call stack mà không cần tự viết toàn bộ middleware/context infrastructure.
Ví dụ một bug thực tế
Giả sử hệ thống có:
Parent registration
↓
Create registration
↓
Generate invoice
↓
Send email
User báo:
Một vài lần đăng ký mất gần 10 giây.
Log:
Registration created successfully.
Không có lỗi.
Database monitoring:
normal
CPU:
normal
Nhưng trace:
POST /registration 9.3s
├── ValidatePlayer 30ms
├── CreateRegistration 45ms
├── GenerateInvoice 80ms
└── SendConfirmationEmail 9.1s
Bây giờ vấn đề rất rõ:
Email provider chậm.
Bạn có thể quyết định đổi architecture:
Registration
↓
Save DB
↓
Push BullMQ job
↓
Return response
Sau đó:
BullMQ
↓
Send email
Response có thể giảm từ:
9.3s
xuống:
150ms
Đây là một ví dụ điển hình về giá trị của observability:
Không chỉ phát hiện lỗi, mà giúp developer hiểu hệ thống đang hoạt động như thế nào.
NestJS 11 có dùng được không?
Có.
Đây là điểm rất dễ hiểu nhầm vì native observability được nhấn mạnh như một tính năng của NestJS 12.
Nhưng SDK chính thức hiện yêu cầu:
@nestjs/core >= 11.1.4
Nếu sử dụng GraphQL thì yêu cầu phiên bản @nestjs/graphql phù hợp nữa.
Vì vậy nếu bạn đang chạy:
NestJS 11.2.x
thì hoàn toàn có thể thử.
Cài đặt
Cơ bản chỉ cần:
npm install @nestjs/observe
Tạo credentials từ NestJS Observe và cấu hình:
OBSERVE_APP_KEY=...
OBSERVE_APP_SECRET=...
Sau đó:
import { createObserveModule } from '@nestjs/observe';
export const {
ObserveModule,
ObserveInstrument,
} = createObserveModule();
Import:
@Module({
imports: [
ObserveModule.forRoot({
appKey: process.env.OBSERVE_APP_KEY,
appSecret: process.env.OBSERVE_APP_SECRET,
serviceId: 'my-api',
}),
],
})
export class AppModule {}
Bootstrap:
const app = await NestFactory.create(AppModule, {
instrument: ObserveInstrument,
});
await app.listen(3000);
Sau đó application bắt đầu gửi telemetry.
Có miễn phí không?
Theo README hiện tại của dự án, NestJS Observe có free tier lên tới:
300,000 events / tháng
Đối với project cá nhân hoặc môi trường staging, con số này có thể đủ để thử nghiệm.
Tuy nhiên với production traffic lớn, nên kiểm tra pricing hiện tại trước khi triển khai rộng.
Khi nào nên dùng?
Nếu hệ thống chỉ có:
10 API
1 database
ít traffic
thì có thể:
logs + Sentry
đã đủ.
Nhưng khi architecture bắt đầu thành:
HTTP API
│
├── MySQL
├── Redis
├── BullMQ
├── cron jobs
├── external APIs
├── email service
└── WebSocket
thì việc debug bằng log bắt đầu rất khó.
Lúc này observability trở nên cực kỳ giá trị.
Kết luận
@nestjs/observe không phải một logger mới.
Nó cũng không đơn giản là:
console.log phiên bản xịn.
Có thể hiểu nó như:
Một camera quan sát bên trong ứng dụng NestJS.
Thay vì chỉ biết:
API chậm.
bạn có thể biết:
API chậm
→ ở RegistrationService
→ cụ thể EmailService
→ request tới email provider
→ mất 4.7 giây.
Thay vì:
Cron đôi khi lỗi.
bạn có thể nhìn:
Cron
→ query DB
→ generate report
→ send email
→ exception.
Và đó chính là giá trị lớn nhất của observability:
giảm thời gian developer phải đoán.
Khi hệ thống production ngày càng phức tạp, việc biết hệ thống lỗi chưa đủ.
Quan trọng hơn là biết:
Nó lỗi ở đâu?
Chậm ở đâu?
Bắt đầu từ khi nào?
Request nào bị ảnh hưởng?
Service nào gây ra vấn đề?
@nestjs/observe là cách NestJS đang đưa khả năng đó trở thành một phần native hơn của framework.