Giới thiệu
Trong môi trường microservice hiện đại, việc theo dõi hành vi của hệ thống qua logging và tracing trở thành yếu tố quyết định để nhanh chóng phát hiện lỗi, tối ưu hiệu năng và đảm bảo tính ổn định. NestJS - một framework dựa trên Node.js, được thiết kế theo kiến trúc modular, cung cấp khả năng mở rộng cao, nhưng nếu không kết hợp với các công cụ quan sát thích hợp, các dự án sẽ nhanh chóng rơi vào trạng thái "đen box". Bài viết này sẽ hướng dẫn chi tiết cách tích hợp OpenTelemetry để thu thập trace, đồng thời sử dụng Elastic Stack (Elastic APM, Elasticsearch, Kibana) làm nền tảng lưu trữ và phân tích log, trace. Các ví dụ sẽ được triển khai trên một dự án RESTful API sử dụng NestJS và TypeORM, giúp bạn nắm bắt quy trình từ cài đặt, cấu hình tới triển khai thực tế.
.png)
Khái niệm logging và tracing trong kiến trúc microservice
Logging và tracing, dù có mục tiêu chung là quan sát hệ thống, nhưng chúng phục vụ các nhu cầu khác nhau:
- Logging: Ghi lại các sự kiện, thông tin trạng thái, lỗi, hoặc bất kỳ dữ liệu nào mà developer muốn lưu trữ. Log thường có dạng văn bản, có thể được cấu trúc (JSON) hoặc không cấu trúc.
- Tracing: Ghi lại chuỗi các cuộc gọi (request) xuyên suốt các service, từ khi nhận request đầu vào tới khi trả về kết quả cuối cùng. Trace giúp xác định thời gian thực thi của từng bước, phát hiện bottleneck và hiểu luồng dữ liệu trong môi trường phân tán.
Trong một hệ thống microservice, một request thường đi qua nhiều service, mỗi service lại có thể gọi thêm các service khác. Khi không có tracing, việc xác định service nào gây ra độ trễ sẽ rất khó khăn. Ngược lại, nếu chỉ có tracing mà không có log chi tiết, bạn sẽ thiếu thông tin ngữ cảnh để debug lỗi.
Cài đặt OpenTelemetry trong NestJS
Thêm các package cần thiết
npm install @opentelemetry/api @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node @opentelemetry/exporter-trace-otlp-http @nestjs/terminus
Gói @opentelemetry/sdk-node cung cấp SDK cho môi trường Node.js, @opentelemetry/auto-instrumentations-node tự động instrument các thư viện phổ biến (Express, HTTP, MySQL, PostgreSQL, …). Chúng ta sẽ sử dụng exporter OTLP HTTP để gửi trace tới Elastic APM.
Cấu hình OpenTelemetry Module
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { diag, DiagConsoleLogger, DiagLogLevel } from '@opentelemetry/api';
// Bật log debug cho OpenTelemetry (tùy chọn)
diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.INFO);
const traceExporter = new OTLPTraceExporter({
// URL của Elastic APM server, thường là http://localhost:8200/api/apm/traces
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || 'http://localhost:8200/api/apm/traces',
});
const sdk = new NodeSDK({
traceExporter,
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start()
.then(() => console.log('✅ OpenTelemetry SDK started'))
.catch((error) => console.error('❌ OpenTelemetry SDK failed to start', error));
Đoạn code trên thường được đặt trong file otel.ts và được import ngay khi ứng dụng khởi động (ví dụ trong main.ts).
Ghi log có cấu trúc với Winston
Thiết lập Winston logger
import { createLogger, format, transports } from 'winston';
import { utilities as nestWinstonModuleUtilities } from 'nest-winston';
export const logger = createLogger({
level: process.env.LOG_LEVEL || 'info',
format: format.combine(
format.timestamp(),
format.json(), // Log dạng JSON để dễ dàng ingest vào Elasticsearch
),
defaultMeta: { service: 'user-service' },
transports: [
new transports.Console({
format: format.combine(
format.colorize(),
nestWinstonModuleUtilities.format.nestLike(),
),
}),
new transports.File({ filename: 'logs/error.log', level: 'error' }),
new transports.File({ filename: 'logs/combined.log' }),
],
});
Winston là thư viện logging phổ biến trong Node.js, hỗ trợ nhiều transport (console, file, HTTP). Khi kết hợp với format.json(), mỗi bản ghi sẽ có cấu trúc chuẩn, giúp Elastic Stack tự động map các trường.
Kết hợp Winston với OpenTelemetry
Để đồng bộ log với trace, chúng ta có thể thêm trường trace_id và span_id vào mỗi bản ghi log. OpenTelemetry cung cấp API để lấy context hiện tại.
import { logger } from './logger';
import { trace, context } from '@opentelemetry/api';
export function logWithTrace(level: string, message: string, meta: any = {}) {
const span = trace.getSpan(context.active());
const traceId = span?.spanContext().traceId;
const spanId = span?.spanContext().spanId;
logger.log({
level,
message,
...meta,
trace_id: traceId,
span_id: spanId,
});
}
Trong các service hoặc controller, thay vì gọi logger.info() trực tiếp, chúng ta sẽ dùng logWithTrace('info', 'User created', { userId }). Khi log được gửi tới Elasticsearch, các trường trace_id và span_id cho phép bạn liên kết log với trace trong Kibana.
Gửi trace tới Elastic APM
Cấu hình Elastic APM agent
Elastic cung cấp một agent APM cho Node.js, tuy nhiên khi đã dùng OpenTelemetry, chúng ta chỉ cần thiết lập endpoint và secret token (nếu có). Dưới đây là ví dụ cấu hình trong file .env:
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:8200/api/apm/traces OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer YOUR_SECRET_TOKEN
Elastic APM sẽ nhận trace theo chuẩn OTLP và tự động hiển thị trong UI. Để kiểm tra, mở Kibana → APM → Traces và bạn sẽ thấy các request được phân tách theo service.
Ví dụ thực tế: Xây dựng API người dùng
Entity User với TypeORM
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
@Entity('users')
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ length: 100 })
name: string;
@Column({ unique: true })
email: string;
@Column({ select: false })
password: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
Entity trên mô tả bảng users trong PostgreSQL. Chú ý sử dụng select: false cho trường password để tránh vô tình trả về khi query.
Service và Controller
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';
import { logWithTrace } from '../logger/trace-logger';
@Injectable()
export class UserService {
constructor(@InjectRepository(User) private readonly userRepo: Repository) {}
async create(dto: { name: string; email: string; password: string }) {
const user = this.userRepo.create(dto);
const saved = await this.userRepo.save(user);
logWithTrace('info', 'User created', { userId: saved.id, email: saved.email });
return saved;
}
async findAll() {
const users = await this.userRepo.find({ select: ['id', 'name', 'email', 'createdAt'] });
logWithTrace('debug', 'Fetched user list', { count: users.length });
return users;
}
}
import { Controller, Get, Post, Body } from '@nestjs/common';
import { UserService } from './user.service';
@Controller('users')
export class UserController {
constructor(private readonly userService: UserService) {}
@Post()
async create(@Body() dto: any) {
return this.userService.create(dto);
}
@Get()
async findAll() {
return this.userService.findAll();
}
}
Trong mỗi phương thức, chúng ta gọi logWithTrace để ghi log kèm trace context. Khi một request POST /users được gửi, OpenTelemetry sẽ tự động tạo một span, và log sẽ chứa trace_id và span_id tương ứng.
Best practices và các lưu ý
- Log ở mức độ phù hợp: Tránh log quá chi tiết (debug) trong môi trường production, vì sẽ làm tăng chi phí lưu trữ. Sử dụng mức
infocho các sự kiện quan trọng,errorcho ngoại lệ. - Định dạng log chuẩn JSON: Giúp Elastic ingest nhanh, hỗ trợ truy vấn bằng Kibana DSL.
- Gắn context vào log: Luôn thêm
trace_id,span_id,user_id(nếu có) để có thể liên kết log với trace và business entity. - Giới hạn độ sâu của trace: Đối với các request nội bộ (service-to-service) có thể bỏ qua một số bước không cần thiết để giảm overhead.
- Kiểm tra performance: Sử dụng công cụ
wrkhoặcabđể benchmark trước và sau khi bật OpenTelemetry, đảm bảo overhead < 5%. - Triển khai cấu hình qua environment variables: Đảm bảo các giá trị như
OTEL_EXPORTER_OTLP_ENDPOINTvàLOG_LEVELcó thể thay đổi mà không cần rebuild image.
Kết luận
Việc tích hợp OpenTelemetry, Winston và Elastic Stack mang lại một hệ thống quan sát toàn diện, cho phép bạn theo dõi cả log và trace trong một giao diện thống nhất. Nhờ đó, việc phát hiện lỗi, tối ưu hiệu năng và duy trì hệ thống microservice trở nên dễ dàng hơn rất nhiều. Khi đã nắm vững quy trình cài đặt và best practices, bạn có thể mở rộng sang các service khác, hoặc tích hợp thêm các công cụ như Grafana, Prometheus để có dashboard thời gian thực.
Để nâng cao kỹ năng xây dựng API chuẩn, an toàn và hiệu quả hơn, Tham khảo khóa học "RESTful API với NestJS & TypeORM" tại đây.
.jpg)






