Triển khai Grafana Faro trên Docker: Giải pháp RUM và Theo dõi Lỗi Frontend Toàn diện

DevOps tutorial - IT technology blog
DevOps tutorial - IT technology blog

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ế.

Share: