Bug đáng sợ nhất với dashboard không phải là chart sai layout. Mà là chart trông đúng, layout đẹp, nhưng con số sai. Stakeholder nhìn vào ra quyết định, và tới khi phát hiện thì đã chi tiền nhầm chỗ. Trong Kibana, hầu hết loại bug này đến từ hai chỗ: aggregation được dùng sai bản chất, và time bucket lệch ngầm.

Bài này gom các trường hợp đã debug trong vài năm vận hành Kibana. Không phải tutorial nhập môn; mà là một danh sách “bẫy” với cách phát hiện và fix.

Mục tiêu:

  • Nhận diện 6 loại pitfall phổ biến: cardinality approximate, terms shard size, time bucket UTC, doc_count vs metric sum, sum-of-rates, missing data
  • Biết cách validate dashboard trước khi share cho stakeholder
  • Có pattern reproducible để test aggregation độ chính xác

Phần 1: Cardinality không phải đếm chính xác

cardinality aggregation trong Elasticsearch là approximate. Nó dùng thuật toán HyperLogLog++ với sai số khoảng 1-6% mặc định.

Một lần mình thấy report tháng ghi “12,847 unique users”. Tháng sau “12,801”. Stakeholder hỏi: “Tại sao users giảm?”. Thực ra users không giảm, mà cardinality estimator return giá trị khác nhau giữa hai lần chạy.

Cách nhận diện

Trong Lens, khi pick metric type, có hai option dễ nhầm:

MetricBản chấtKhi nào dùng
Unique countcardinality aggregation, approximateKhi cardinality cao (triệu unique value) và sai số 1-6% chấp nhận được
Countvalue_count, đếm chính xácKhi đếm tổng số document

Nếu number nhỏ (< 40k) và bạn muốn exact, ép precision_threshold lên cao:

"aggs": {
  "unique_users": {
    "cardinality": {
      "field": "user_id.keyword",
      "precision_threshold": 40000
    }
  }
}

Kibana Lens không expose precision_threshold qua UI, nhưng nếu bạn dùng Vega hoặc TSVB thì có thể chỉnh. Hoặc workaround: dùng terms aggregation với size lớn rồi đếm bucket. Chính xác hơn nhưng tốn ES hơn.

Validate

Chạy hai query song song và so sánh:

GET app-logs-*/_search
{
  "size": 0,
  "aggs": {
    "approx": { "cardinality": { "field": "user_id.keyword" } },
    "exact": {
      "terms": { "field": "user_id.keyword", "size": 100000 },
      "aggs": { "count": { "value_count": { "field": "user_id.keyword" } } }
    }
  }
}

Nếu lệch dưới 5%, OK dùng cardinality. Nếu lệch trên 10%, cardinality không phù hợp; phải refactor.

Phần 2: Terms aggregation cắt mất nhóm nhỏ

Terms aggregation chỉ return top N bucket. Mặc định trong Kibana là 5 hoặc 10. Phần còn lại bị gom vào “Other” hoặc bị bỏ qua hoàn toàn.

Ca thực tế: pie chart “Top 10 lỗi” hiển thị error A chiếm 40%, error B 20%, … còn lại 30% gom vào “Other”. Dev đọc xong nghĩ “chỉ cần fix A và B”. Thực tế “Other” có 50 loại lỗi nhỏ, mỗi loại 0.5-2%, và một trong số đó là security incident chưa được phát hiện.

Fix

  1. Tăng size trong Lens panel: bên phải dropdown “Top values” → “Number of values” set 20-50.
  2. Bật “Group remaining as Other” chỉ khi chắc chắn không cần nhìn nhóm nhỏ.
  3. Tách dashboard: một panel “Top 10” + một panel “Long tail” với filter loại bỏ top 10.

Shard size pitfall

Còn một issue tinh tế hơn. Terms aggregation chạy distributed: mỗi shard return top N local, rồi node coordinator merge. Nếu một term phổ biến global nhưng không lọt top N ở shard nào, kết quả sẽ thiếu.

Setting cần biết: shard_size. Mặc định 1.5 * size + 10. Nếu data skew (vài shard có hầu hết document), tăng shard_size lên.

"aggs": {
  "top_errors": {
    "terms": {
      "field": "error_code.keyword",
      "size": 10,
      "shard_size": 1000
    }
  }
}

Tradeoff: shard_size lớn = chính xác hơn nhưng tốn ES hơn. Bắt đầu với shard_size: 100 và tăng nếu thấy lệch.

Phần 3: Time bucket lệch múi giờ

Histogram trên @timestamp chia data thành bucket theo thời gian. Bug ngầm: bucket boundary phụ thuộc múi giờ.

Ví dụ: bucket “daily” trong UTC bắt đầu lúc 00:00 UTC = 07:00 giờ Việt Nam. Nếu user xem dashboard giờ VN nhưng ES bucket theo UTC, ngày “2026-05-17” trong dashboard sẽ chứa data từ 07:00 ngày 17 đến 06:59 ngày 18 giờ VN. Báo cáo “tổng giao dịch ngày 17” sẽ tính cả phần đầu ngày 18.

Fix qua time_zone

Kibana từ version 7+ tự dùng browser timezone cho time bucket khi render. Nhưng nếu bạn export query ra Vega, hoặc gọi ES trực tiếp, cần set time_zone explicit:

"aggs": {
  "by_day": {
    "date_histogram": {
      "field": "@timestamp",
      "calendar_interval": "day",
      "time_zone": "Asia/Ho_Chi_Minh"
    }
  }
}

Lưu ý calendar_interval (day, week, month) tôn trọng timezone. fixed_interval (24h) thì không. Khi data cần align với boundary lịch (như “ngày 17” chính xác), luôn dùng calendar_interval.

Kiểm tra trong Kibana

Vào Stack Management → Advanced Settings → dateFormat:tz. Nếu để Browser thì cell time format hiển thị theo browser, nhưng bucket boundary có thể vẫn UTC nếu ES bị query với time_zone mặc định.

Cách validate đơn giản: trong Discover, hover một bucket trên histogram, popup hiển thị “From X to Y”. Kiểm tra X và Y có align với 00:00 giờ local không.

Phần 4: doc_count vs metric sum

Một bar chart “tổng doanh thu theo ngày” có thể dùng:

  • count của document (mỗi document = 1 đơn hàng)
  • sum của field revenue

Hai cái rất khác. Nếu order có nested item, ES có thể index thành nhiều document (parent + nested), count sẽ phồng số lượng. Hoặc nếu order bị retry và index trùng, count sẽ tính trùng.

Best practice:

Bạn muốn đoMetric đúng
Số đơn hàngcardinality của order_id
Doanh thusum của revenue (lưu ý dedup)
Số user duy nhấtcardinality của user_id (với precision)
Số requestvalue_count (số document)

Pitfall storytelling: một dashboard hiển thị “10000 orders” theo count of records, nhưng số đơn hàng thật chỉ 6500. Lý do: pipeline retry index khi ES throttle, nên 35% order bị duplicate. Fix bằng cách dùng cardinality(order_id) thay vì count, hoặc dedupe ở ingest layer (Logstash filter fingerprint + override _id).

Phần 5: Sum of rates (anti-pattern)

Một dashboard SRE: “tổng request per second” tính bằng sum của metric rps (mỗi service báo cáo rps của nó). Nghe có lý? Sai.

Nếu service A báo cáo rps = 100 và service B báo cáo rps = 200, sum = 300 chỉ đúng nếu hai metric đo cùng một thời điểm. Nhưng nếu A báo cáo lúc 10:00:01 và B lúc 10:00:03, dashboard có thể double-count vì ES tính cả hai trong cùng 1-minute bucket.

Quy tắc: sum trên rate (per-second metric) phải qua avg-over-time trước. Trong Lens, dùng formula:

sum(average_over_time(rps, kql='', timeShift='1m'))

Hoặc tốt hơn: thay vì lưu rps, lưu raw count requests_total (counter monotonic increasing), rồi tính rate trong Kibana bằng differences() hoặc derivative aggregation.

"aggs": {
  "by_minute": {
    "date_histogram": { "field": "@timestamp", "fixed_interval": "1m" },
    "aggs": {
      "total_requests": { "max": { "field": "requests_total" } },
      "rate": {
        "derivative": { "buckets_path": "total_requests" }
      }
    }
  }
}

Pattern này là cách Prometheus tính rate. Kibana hỗ trợ tương tự qua derivative pipeline aggregation.

Phần 6: Missing data và sparse buckets

Khi data không tới đều (sensor offline 5 phút), bar chart sẽ có gap. Kibana mặc định không hiện gap mà skip bucket, tạo cảm giác “data liên tục”.

Vấn đề: nếu metric là avg(temperature) thì bucket trống → không hiện điểm → đường lookup line chart bị nối qua, làm dữ liệu nhìn smooth giả tạo.

Fix

  1. Trong Lens, settings panel của axis: chọn Missing values → Show as zero hoặc Hide.
  2. Với line chart, chọn Connect null values: Off để gap hiện rõ.
  3. Trong query, dùng extended_bounds để force date_histogram tạo bucket trống:
"aggs": {
  "by_minute": {
    "date_histogram": {
      "field": "@timestamp",
      "fixed_interval": "1m",
      "extended_bounds": {
        "min": "2026-05-17T00:00:00",
        "max": "2026-05-17T23:59:59"
      }
    }
  }
}

Tradeoff: hiện gap đúng nhưng dashboard “trông xấu” hơn. Đó là intentional. Stakeholder thấy gap mới hỏi “tại sao mất data?” và discover được upstream issue.

Phần 7: Validation workflow

Trước khi share dashboard cho stakeholder, chạy checklist:

  1. Cross-check với source: lấy 1-2 con số trên dashboard, compare với COUNT() từ DB nguồn hoặc log file gốc. Lệch dưới 1% thì OK.
  2. Time zone test: chuyển browser timezone (Chrome DevTools → Sensors → Timezone), refresh dashboard. Số ổn định không? Nếu nhảy nhiều thì có bug timezone.
  3. Top N + Other: nếu có “Other” bucket, expand xem nó chiếm bao nhiêu %. Quá 10% là dashboard không phản ánh đúng.
  4. Sample size: cardinality < 40000 → ép precision_threshold cao. Cardinality > 1M → chấp nhận sai số nhưng note rõ.
  5. Outlier check: chạy Lens với percentile 95th, 99th để xem có giá trị bất thường skew metric không. Một order $1M nhập nhầm có thể làm trung bình tháng tăng 30%.
  6. Date range edge: thử time range “Last 1 hour” và “Last 30 days”. Số có scale tỉ lệ thuận hợp lý không?

Phần 8: Bảng pitfall tổng kết

PitfallTriệu chứngFix
Cardinality approximateUnique count nhảy giữa các lần xemÉp precision_threshold hoặc dùng terms count
Terms top N cut-offPie chart có “Other” lớnTăng size, tách Top vs Long tail panel
Shard size skewTerms result lệch khi data unbalancedTăng shard_size
Time bucket UTCBucket “ngày X” lệch 7htime_zone: Asia/Ho_Chi_Minh, dùng calendar_interval
Duplicate document countOrder count cao bất thườngcardinality(order_id) hoặc dedup ingest
Sum of ratesRPS double-countLưu counter, tính rate bằng derivative
Sparse data hiddenLine chart nhìn smooth giảextended_bounds + “Connect null values: Off”

Cheatsheet

ViệcAggregation đúng
Đếm documentvalue_count (chính xác)
Đếm unique low cardinalityterms với size lớn
Đếm unique high cardinalitycardinality với precision_threshold
Sum chính xácsum field numeric
Tổng theo thời giandate_histogram + sub-aggregation
Rate per secondderivative trên counter
Top Nterms với shard_size cao
Percentilepercentiles (HDR algorithm khi cần precision)

Lời kết

Dashboard sai không phải vì Kibana sai, mà vì aggregation được dùng không khớp với câu hỏi business. Hai loại lỗi đắt nhất là cardinality approximate trên số nhỏ và time bucket lệch múi giờ. Cả hai đều ngầm; cả hai đều dẫn tới quyết định sai. Đầu tư 30 phút validate dashboard trước khi share luôn tiết kiệm hơn 3 ngày debug sau khi stakeholder phản hồi.

Bài tiếp theo trong series Kibana từ A đến Z mở Part 3 về Alerts. Bắt đầu với alert rules: ES query, threshold, và burn rate. Đây là cách biến log từ kho lưu trữ thành cảnh báo proactive. Nếu bạn đang gặp pitfall nào khác lạ với aggregation, comment để mình thêm vào bản update.