Quan Sát Hệ Thống: Lambda Powertools và X-Ray Tracing

K
Kai··5 min read

Sản phẩm chạy được đầu cuối, nhưng khi có sự cố ở production, "chạy được" không đủ. Cần nhìn được một request đã đi qua đâu, chậm ở khâu nào, và log của nó nói gì. Phần V của series là về vận hành như thật, và viên gạch đầu là quan sát. Bài này gắn công cụ để mỗi request để lại dấu vết đọc được.

Mục tiêu

Gắn Lambda Powertools vào handler để có log JSON có cấu trúc, trace qua X-Ray, và metric tùy biến; bật X-Ray cho các hàm; rồi đọc một trace thật để thấy đường request đi qua DynamoDB và EventBridge. Chi phí X-Ray và CloudWatch cho lượng test nhỏ không đáng kể.

Vì sao log thường không đủ

Một dòng console.log("resolved " + code) đọc được bằng mắt nhưng máy khó lọc. Khi có hàng nghìn dòng, bạn muốn lọc theo mã link, theo request id, theo việc có phải cold start không, hay nối một dòng log với trace của đúng request đó. Điều đó cần log có cấu trúc: mỗi dòng là một JSON với các trường nhất quán. Lambda Powertools là bộ thư viện chính thức của AWS lo đúng ba việc quan sát: Logger (log cấu trúc), Tracer (X-Ray), và Metrics (đẩy metric dạng EMF).

Gắn Powertools

Ba instance dùng chung cho mọi handler, đặt ở một file:

import { Logger } from "@aws-lambda-powertools/logger";
import { Tracer } from "@aws-lambda-powertools/tracer";
import { Metrics } from "@aws-lambda-powertools/metrics";

export const logger = new Logger({ serviceName: "url-shortener" });
export const tracer = new Tracer({ serviceName: "url-shortener" });
export const metrics = new Metrics({ namespace: "UrlShortener", serviceName: "url-shortener" });

Để các lời gọi AWS SDK hiện thành subsegment trong trace, bọc client bằng tracer.captureAWSv3Client. Vì client DynamoDB nằm ở file dùng chung, chỉ cần bọc một lần:

import { tracer } from "./powertools.js";
const client = tracer.captureAWSv3Client(new DynamoDBClient({}));

Handler resolve dùng middy để ghép ba middleware Powertools, cách làm chuẩn được khuyến nghị:

const lambdaHandler = async (event) => {
  // ...GetItem, PutEvents như cũ...
  logger.info("link resolved", { code });
  metrics.addMetric("LinkResolved", MetricUnit.Count, 1);
  return { statusCode: 301, headers: { location: target }, body: "" };
};

export const handler = middy(lambdaHandler)
  .use(captureLambdaHandler(tracer))      // tao subsegment cho handler trong X-Ray
  .use(injectLambdaContext(logger))       // gan request id, cold_start... vao moi log
  .use(logMetrics(metrics));              // day metric khi handler ket thuc

Bật X-Ray cho mọi hàm chỉ là một dòng trong template:

Globals:
  Function:
    Tracing: Active

SAM tự thêm quyền ghi X-Ray cho hàm khi bật Tracing: Active, không phải khai IAM tay.

Log có cấu trúc, đọc thật

Sau khi deploy và mở vài lần một link, dòng log của resolve không còn là chuỗi tự do mà là JSON đầy đủ ngữ cảnh:

{
  "level": "INFO",
  "message": "link resolved",
  "timestamp": "2026-05-25T17:27:37.027Z",
  "service": "url-shortener",
  "cold_start": false,
  "function_name": "url-shortener-ResolveLinkFunction-rc7lUf4kG5u5",
  "function_request_id": "80bd3dd5-692f-487b-b8ba-2301c997e8d3",
  "function_memory_size": "128",
  "xray_trace_id": "1-6a148688-3996dca03cc76b0d1cfb676b",
  "code": "oi90ICl"
}

Mỗi trường ở đây là một thứ lọc được. cold_start cho biết request này có rơi vào môi trường mới không. function_request_id nối mọi log của cùng một lần chạy. Và xray_trace_id nối dòng log này với trace tương ứng trong X-Ray, nên từ một log lỗi có thể nhảy thẳng sang xem request đó đã đi qua đâu. Trường code là cái ta tự thêm, để lọc theo từng link.

Trace: request đi qua đâu

X-Ray ghi lại đường một request đi qua các dịch vụ. Lấy một trace của resolve và in cây segment cho thấy cấu trúc:

- url-shortener-ResolveLinkFunction      (segment cua ham)
  - Overhead
  - ## resolve-link.handler              (subsegment do captureLambdaHandler tao)
    - Events                             (loi goi EventBridge PutEvents)
    - DynamoDB                           (loi goi DynamoDB GetItem)
- DynamoDB                               (node dich vu downstream)
- Events                                 (node dich vu downstream)

Đây là service map dưới dạng cây. Hàm resolve gọi hai dịch vụ, DynamoDB để tra link và EventBridge để phát sự kiện, và cả hai hiện thành subsegment riêng nhờ captureAWSv3Client. Subsegment ## resolve-link.handler là phần thân handler, do middleware tracer tạo. Hai node ở cấp ngoài cùng (DynamoDB, Events) là các dịch vụ downstream mà X-Ray vẽ thành các nút trong service map trên console.

Giá trị thực tế nằm ở chỗ mỗi subsegment có thời lượng riêng. Khi một request chậm, trace cho thấy phần lớn thời gian nằm ở đâu: đọc DynamoDB, phát sự kiện, hay phần code của bạn. Không có trace thì "API chậm" chỉ là phỏng đoán; có trace thì biết chính xác khâu nào để sửa.

Metric tùy biến

metrics.addMetric("LinkResolved", MetricUnit.Count, 1) đẩy một metric mỗi lần resolve thành công, theo định dạng EMF (Embedded Metric Format) mà CloudWatch tự nhận từ log. Từ đó CloudWatch dựng được biểu đồ số lượt resolve theo thời gian, và bài sau sẽ đặt alarm trên các metric như vậy. Khác với việc tự gọi API CloudWatch, EMF chỉ là ghi log đúng định dạng nên không thêm độ trễ vào request.

🧹 Dọn dẹp

aws dynamodb scan --table-name url-shortener --query 'Items[].{PK:PK.S,SK:SK.S}' --output text | \
  while read pk sk; do aws dynamodb delete-item --table-name url-shortener \
    --key "{\"PK\":{\"S\":\"$pk\"},\"SK\":{\"S\":\"$sk\"}}"; done
aws cognito-idp admin-delete-user --user-pool-id "$POOL" --username obs@example.com

Giữ stack cho bài sau.

Tổng kết

Hệ thống giờ quan sát được. Powertools Logger cho log JSON có cấu trúc mang sẵn request id, cold start và trace id; Tracer dựng trace X-Ray với từng lời gọi dịch vụ thành subsegment có thời lượng riêng; Metrics đẩy số liệu qua EMF mà không thêm độ trễ. Từ một dòng log có thể nhảy sang trace, từ trace thấy được request chậm ở đâu.

Có log, trace và metric rồi thì bước tiếp là dùng chúng để biết khi nào hệ thống có vấn đề mà không phải ngồi nhìn. Bài sau dựng dashboard và alarm trên CloudWatch, định nghĩa vài SLO cho API, và đặt cảnh báo để khi tỉ lệ lỗi hay độ trễ vượt ngưỡng thì có người được báo, thay vì chờ người dùng phàn nàn.