Vấn đề: Những điểm mù trong hiệu năng Frontend
Dashboard backend của bạn trông hoàn hảo không tì vết. Độ trễ API duy trì ổn định dưới 45ms, mức tải CPU dưới 20%, và Nginx ghi nhận tỷ lệ HTTP 200 lên tới 99,98%. Thế nhưng các ticket hỗ trợ vẫn liên tục đổ về: nút thanh toán bị lỗi âm thầm trên mobile Safari, chuyển trang trong Single-Page App bị đơ giữa chừng khi đang render, còn người dùng thì bỏ giỏ hàng vì cảm thấy trang web tải quá chậm.
Các chỉ số phía server không thể nhìn thấy những gì thực sự diễn ra bên trong trình duyệt của người dùng. Khi một lỗi React chưa được bắt làm unmount toàn bộ cây component, hoặc một banner 3MB làm chỉ số Cumulative Layout Shift (CLS) nhảy vọt lên 0.45, hệ thống log backend hoàn toàn không ghi nhận được gì. Grafana Faro thu thập chính xác các sự kiện này ngay tại nơi chúng phát sinh—bên trong trình duyệt client—và gửi runtime log, thông số hiệu năng cùng network trace trực tiếp về stack Grafana hiện có của bạn.
Telemetry Frontend: So sánh các lựa chọn
Các nhóm kỹ thuật thường cân nhắc ba giải pháp để theo dõi hiệu năng và lỗi phía client:
1. Các giải pháp SaaS APM thương mại (Datadog RUM, Sentry, New Relic)
Các nhà cung cấp SaaS mang lại khả năng thiết lập dễ dàng và dashboard đẹp mắt. Điểm trừ là gì? Chi phí tính theo mức sử dụng tăng lên rất nhanh. Một ứng dụng web người dùng có lưu lượng lớn với 5 triệu phiên truy cập mỗi tháng có thể dễ dàng tiêu tốn hàng nghìn USD hóa đơn APM hàng tháng. Ngoài ra, bạn cũng phải chuyển giao dữ liệu telemetry nhạy cảm của người dùng cho bên thứ ba.
2. Tự xây dựng endpoint Beacon nội bộ
Một số đội ngũ tự dựng một endpoint Go hoặc Node.js gọn nhẹ để tiếp nhận payload từ navigator.sendBeacon() và chuyển tiếp chúng sang Elasticsearch hoặc Loki. Cách này hoạt động tốt ở giai đoạn đầu. Nhưng chi phí bảo trì sẽ nhanh chóng tăng cao khi bạn cần phân tích phiên người dùng, hỗ trợ header trace của OpenTelemetry và theo dõi những thay đổi trong Core Web Vitals.
3. Grafana Faro kết hợp OpenTelemetry
Grafana Faro cung cấp một Web SDK dạng module, dung lượng nhẹ (khoảng 14KB sau khi gzip) giúp thu thập lỗi console, exception chưa bắt được, Web Vitals và hành trình người dùng. Được định tuyến qua Grafana Agent hoặc Alloy, Faro gửi trực tiếp các dòng log về Loki và distributed trace về Tempo. Mọi thứ đều nằm trọn vẹn trong hạ tầng của chính bạn.
Đánh đổi khi tự host Grafana Faro
Ưu điểm
- Toàn quyền sở hữu dữ liệu: Dữ liệu telemetry không bao giờ rời khỏi VPC của bạn. Điều này giúp việc tuân thủ các quy định như GDPR, HIPAA và các chính sách bảo mật nội bộ nghiêm ngặt trở nên đơn giản hơn.
- Chi phí có thể dự đoán: Không còn nỗi lo về các gói cước tính phí theo từng phiên người dùng. Chi phí chỉ tăng theo dung lượng lưu trữ ổ đĩa và tài nguyên compute mà bạn đang quản lý.
- Ngữ cảnh Trace hợp nhất: Faro truyền tải các header chuẩn W3C
traceparent. Bạn có thể trace một thao tác click chậm của người dùng từ event handler trong React xuyên suốt tới tận câu truy vấn PostgreSQL ở backend. - Luồng làm việc native trên Grafana: Dễ dàng chuyển đổi qua lại giữa log lỗi client trong Loki và distributed trace trong Tempo bằng các biến dashboard tiêu chuẩn và truy vấn Explore.
Nhược điểm
- Quản lý Pipeline: Bạn chịu trách nhiệm về các quy tắc lưu trữ dữ liệu (retention), thời gian uptime của collector và việc định cỡ dung lượng đĩa cho database.
- Xử lý Source Map: Để giải mã các trace từ bundle production đã được minify, bạn cần tự host một endpoint source map truy cập được hoặc tự cấu hình các storage bucket.
Tổng quan kiến trúc
Một kiến trúc self-hosted hoàn chỉnh kết nối năm thành phần trong Docker:
- Grafana Faro Web SDK: Chạy trên trình duyệt để thu thập lỗi, lượt click của người dùng và Core Web Vitals (LCP, INP, CLS).
- Grafana Agent (hoặc Grafana Alloy): Lắng nghe trên một cổng HTTP, nhận các batch JSON từ Faro, bóc tách metadata và phân phối dữ liệu xuống các hệ thống phía dưới.
- Grafana Loki: Đánh chỉ mục log console frontend, lỗi mạng và sự kiện phiên làm việc.
- Grafana Tempo: Tiếp nhận các OpenTelemetry trace span được sinh ra từ các yêu cầu fetch và XHR ở frontend.
- Grafana Dashboard: Trực quan hóa các chỉ số RUM và giúp kỹ sư chuyển đổi nhanh giữa trace và log chỉ với một cú click.
Việc liên kết một cú click ở frontend tới câu truy vấn chậm ở backend giúp rút ngắn thời gian debug từ hàng giờ xuống còn vài phút. Hãy cùng bắt tay vào xây dựng hệ thống.
Hướng dẫn triển khai từng bước với Docker Compose
Bước 1: Tạo stack Docker Compose
Tạo một thư mục tên là grafana-faro-stack và thêm file docker-compose.yml sau:
version: '3.8'
services:
grafana:
image: grafana/grafana:10.4.0
container_name: faro-grafana
ports:
- "3000:3000"
environment:
- GF_AUTH_ANONYMOUS_ENABLED=true
- GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
volumes:
- ./grafana/datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml
depends_on:
- loki
- tempo
loki:
image: grafana/loki:2.9.4
container_name: faro-loki
ports:
- "3100:3100"
command: -config.file=/etc/loki/local-config.yaml
tempo:
image: grafana/tempo:2.3.1
container_name: faro-tempo
ports:
- "3200:3200"
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP
command: [ "-config.file=/etc/tempo.yaml" ]
volumes:
- ./tempo/tempo.yaml:/etc/tempo.yaml
grafana-agent:
image: grafana/agent:v0.40.0
container_name: faro-agent
ports:
- "12347:12347" # Cổng nhận dữ liệu Faro
- "12345:12345" # Metrics nội bộ của Agent
volumes:
- ./agent/agent-config.yaml:/etc/agent/agent-config.yaml
command:
- -config.file=/etc/agent/agent-config.yaml
- -server.http.address=0.0.0.0:12345
depends_on:
- loki
- tempo
Bước 2: Cấu hình Grafana Agent để nhận Telemetry từ Faro
Tạo file agent/agent-config.yaml. Khối app_agent_receiver sẽ tiếp nhận payload từ trình duyệt, trích xuất metadata của phiên, đồng thời định tuyến log sang Loki và trace sang Tempo:
server:
log_level: info
integrations:
event_handler:
enabled: true
app_agent_receiver:
instance: production-web
server:
host: 0.0.0.0
port: 12347
cors_allowed_origins:
- "*"
sourcemaps:
download: true
logs:
send_to: loki
traces:
send_to: tempo
logs:
configs:
- name: loki
clients:
- url: http://loki:3100/loki/api/v1/push
traces:
configs:
- name: tempo
remote_write:
- endpoint: tempo:4317
insecure: true
Bước 3: Cấu hình lưu trữ cho Tempo
Tạo file tempo/tempo.yaml với cấu hình lưu trữ file cục bộ:
server:
http_listen_port: 3200
distributor:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
ingester:
trace_idle_period: 10s
max_block_bytes: 1_000_000
max_block_duration: 5m
compactor:
compaction:
block_retention: 24h
storage:
trace:
backend: local
local:
path: /tmp/tempo/traces
wal:
path: /tmp/tempo/wal
Bước 4: Thiết lập sẵn Data Source cho Grafana
Tạo file grafana/datasources.yaml để Grafana tự động đăng ký Loki và Tempo khi khởi động:
apiVersion: 1
datasources:
- name: Loki
type: loki
access: proxy
url: http://loki:3100
isDefault: true
- name: Tempo
type: tempo
access: proxy
url: http://tempo:3200
jsonData:
httpMethod: GET
tracesToLogs:
datasourceUid: 'Loki'
tags: ['job', 'instance', 'app']
mappedTags:
- key: 'service.name'
value: 'app'
Bước 5: Khởi động hệ thống
Khởi chạy toàn bộ container ở chế độ chạy ngầm (detached mode):
docker compose up -d
Kiểm tra đảm bảo tất cả các container đều ở trạng thái hoạt động bình thường:
docker compose ps
Bước 6: Tích hợp Faro Web SDK vào Frontend của bạn
Cài đặt gói SDK cốt lõi của Faro cùng gói tracing:
npm install @grafana/faro-web-sdk @grafana/faro-web-tracing
Khởi tạo Faro càng sớm càng tốt trong bundle frontend (chẳng hạn như trong src/index.tsx hoặc src/main.js):
import { initializeFaro, getWebInstrumentations } from '@grafana/faro-web-sdk';
import { TracingInstrumentation } from '@grafana/faro-web-tracing';
export const faro = initializeFaro({
url: 'http://localhost:12347/collect',
app: {
name: 'storefront-ui',
version: '2.1.4',
environment: 'production',
},
instrumentations: [
// Thu thập lỗi console, unhandled promise rejections và Web Vitals
...getWebInstrumentations({
captureConsole: true,
capturePerformance: true,
}),
// Lan truyền W3C trace headers qua các lệnh gọi fetch/XHR
new TracingInstrumentation(),
],
});
// Theo dõi các mốc sự kiện nghiệp vụ tùy chỉnh
faro.api.pushEvent('checkout_completed', {
cartTotal: '142.50',
itemCount: '3',
currency: 'USD',
});
Bước 7: Kiểm tra việc tiếp nhận và xem xét dữ liệu
Kiểm tra pipeline hoạt động bằng cách gửi một lỗi giả lập từ console của trình duyệt:
faro.api.pushError(new Error('StripeElementsError: Khởi tạo phần tử thẻ thất bại'));
Mở Grafana tại http://localhost:3000. Chuyển đến mục Explore, chọn data source Loki, và chạy truy vấn LogQL sau:
{app="storefront-ui"} |= "StripeElementsError"
Bản ghi log sẽ hiển thị chi tiết phiên bản trình duyệt, hệ điều hành, đường dẫn URL và dòng mã nguồn gây lỗi. Để theo dõi tổng quan hiệu năng của người dùng, hãy import Grafana Dashboard ID 17997. Bạn sẽ thấy ngay phân phối thực tế trên môi trường production của Largest Contentful Paint (mục tiêu: < 2.5s), Interaction to Next Paint (INP, mục tiêu: < 200ms) và Cumulative Layout Shift (< 0.1) trên các thiết bị thực tế.

