Khi ELK stack được deploy, dev thường gặp một nghịch lý buồn cười: log đầy ắp trong Elasticsearch, nhưng mở Kibana ra thì “không tìm thấy error nào”. Không phải Kibana bị hỏng — mà là cú pháp filter sai, field sai case, hoặc time range lệch. Bài viết này là starter kit cho developer backend bắt đầu với Kibana: hiểu data flow, filter đúng, dựng dashboard, và tương tác qua API để version control mọi thứ.

Đây là bài đầu tiên trong series Kibana từ A đến Z. Sau bài này bạn sẽ tự tin làm được:

  • Filter error log bằng KQL mà không bị “No results”
  • Tạo Saved Search + Dashboard qua GUI
  • Export saved objects thành NDJSON để commit vào git
  • Gọi Kibana REST API để tự động hoá
  • Tạo API key với scope giới hạn (đúng kiểu production-ready)

Bối cảnh: kiến trúc điển hình

Setup ELK + Serilog (C#/.NET backend) phổ biến hiện nay thường có dạng:

C# Apps (Serilog.Sinks.Http)
  ↓  POST JSON
Vector (HTTP server :8686)
  ↓  bulk index
Elasticsearch
  ↓  query
Kibana

Vector nhận log JSON từ Serilog, normalize timestamp, rồi bulk-index vào ES. Kibana đọc ES để render Discover/Dashboard. Đây là pipeline mình sẽ dùng làm ví dụ xuyên suốt bài.

Ba điểm cần nhớ về Serilog schema vì nó là nguồn cơn của 90% lỗi filter:

FieldVí dụ giá trịGhi chú
Level"Error", "Fatal", "Warning", "Information"PascalCase, giá trị viết hoa
MessageTemplate"Failed to process {OrderId}"Template gốc
RenderedMessage"Failed to process 123"Message đã render
Exceptionfull stack traceChỉ có khi log exception
Properties.*custom fieldsStructured properties
@timestampISO8601Do Vector inject từ Timestamp của Serilog

Nếu backend của bạn dùng Logback, Winston, Zap — schema sẽ khác. Nhưng nguyên tắc filter vẫn giống: field name và giá trị phân biệt hoa-thường.

Phần 1: Filter error log với Discover + KQL

Lỗi kinh điển

Dev quen stack khác thường gõ:

level: error

Kết quả: 0 results. Vì Serilog emit Level (chữ L hoa) với giá trị "Error" (chữ E hoa) chứ không phải "error".

Cú pháp đúng

Vào ≡ → Analytics → Discover, chọn data view app-logs-*, đặt time range “Last 24 hours”. Gõ vào thanh search KQL:

Level : "Error"

Bao gồm cả Fatal:

Level : ("Error" or "Fatal")

Loại bỏ Info + Warning (tương đương lấy Error + Fatal):

not Level : ("Information" or "Warning")

Lọc error của 1 app cụ thể:

Level : "Error" and Properties.ApplicationName : "order-service"

Chỉ log có Exception:

Exception : *

Nếu dropdown gợi ý value trống khi bạn click “Add filter” → field đang map là text (không aggregatable), dùng Level.keyword thay vì Level.

Debug checklist khi filter ra 0 kết quả

Chạy tuần tự trong Discover:

  1. Xoá hết filter → có log không? Nếu không → time range hoặc data view sai.
  2. Level : * → có không? Nếu không → field tên khác (có thể là level lowercase tuỳ logger).
  3. Expand 1 document → copy field name chính xác từ JSON panel → dùng đúng case.
  4. Bảo backend check xem có thật sự log Error nào trong window không — có thể code im ắng nên không có.

Phần 2: Pitfall với ES|QL

Kibana 8.x có tab ES|QL — ngôn ngữ query mới của Elastic. Một lần mình thấy dev viết:

FROM app-logs-2026.04.15 | LIMIT 1000
| WHERE Level != "Information" AND Level != "Warning"

0 kết quả. Nhưng data rõ ràng tồn tại.

Bug: ES|QL chạy tuần tự trái sang phải. Pipeline trên làm:

  1. LIMIT 1000 trước → lấy 1000 dòng đầu bất kỳ (99% là Information/Warning vì chúng chiếm đại đa số).
  2. WHERE sau → lọc trong 1000 dòng đó → 0 Error/Fatal.

Đúng phải là WHERE trước, LIMIT sau:

FROM app-logs-*
| WHERE Level == "Error" OR Level == "Fatal"
| SORT @timestamp DESC
| LIMIT 1000

Thêm mẹo nhỏ:

  • Dùng wildcard app-logs-* thay vì hardcode 1 ngày — tự bao nhiều index.
  • Không cần backtick quanh Level — nó không phải reserved word.
  • Luôn SORT @timestamp DESC trước LIMIT nếu muốn “N bản ghi mới nhất”.

Phần 3: Tạo Saved Search cho team

Gõ KQL mỗi lần không phải lúc nào cũng tiện. Saved Search cho phép cả team mở Discover là ra ngay cùng 1 view.

Các bước (GUI)

  1. Discover, đặt data view app-logs-*, time range “Last 24 hours”.
  2. Gõ KQL: Level : ("Error" or "Fatal").
  3. Bên trái panel Available fields — click + để add column: Level, Properties.ApplicationName, RenderedMessage, Exception.
  4. Click header @timestamp → sort New-Old.
  5. Góc phải trên → Save → Title: All Errors, Description mô tả ngắn → Save.

Từ giờ dev mở Discover → Open → chọn All Errors là xong. Thay đổi query hay column → Save lại để update.

Phần 4: Dashboard Error Overview

Saved Search là bảng phẳng. Dashboard cho phép visualization aggregate — thấy spike theo thời gian, tỉ lệ error theo app.

Panel 1: Error count over time (bar chart)

  • ≡ → Analytics → Dashboards → Create dashboard.
  • Set time range “Last 24 hours”.
  • Click Create visualization.
  • Top-right chọn Bar vertical stacked.
  • Horizontal axis: drag @timestamp từ field list.
  • Vertical axis: click vào → chọn function Count. Field để trống — Count không cần field, Kibana tự đặt tên hiển thị là “Count of records”.
  • Breakdown: drag Level → để Error/Fatal hiển thị màu khác nhau.
  • Thêm filter bằng thanh filter của panel: Level : ("Error" or "Fatal").
  • Title: “Errors over time”. Save and return.

Panel 2: Errors by Application (pie chart)

  • Create visualization → type Pie.
  • Slice by: Properties.ApplicationName.
  • Size by: Count of records (default).
  • Filter: Level : ("Error" or "Fatal").
  • Title: “Errors by Application”. Save and return.

Panel 3: Recent errors (table)

Đơn giản nhất: embed luôn Saved Search đã tạo.

  • Click Add from library (góc phải trên, cạnh Add panel).
  • Tab Search → chọn All Errors.
  • Kéo góc panel để resize.

Layout và save

  • Kéo-thả panel: bar chart full-width hàng 1, pie + saved search chia đôi hàng 2.
  • Save → Title: Error Overview, description, tick Store time with dashboard nếu muốn default time range → Save.

Phần 5: Export NDJSON — dashboard-as-code

Quan trọng nhất về mặt vận hành: không commit GUI state thì mất dashboard khi cluster die. Pattern an toàn: làm GUI → export NDJSON → commit vào git → CI/CD import khi cluster mới được provision.

Export qua GUI

  • ≡ → Management → Stack Management → Kibana → Saved objects.
  • Search: gõ Error để lọc.
  • Tick checkbox các object: All Errors (search), Error Overview (dashboard).
  • Góc phải trên → Export X objects → tick Include related objects (gom hết dependencies như lens, visualization) → Export.
  • Download file export.ndjson.

Commit vào git

mkdir -p infrastructure/kibana-objects
mv ~/Downloads/export.ndjson infrastructure/kibana-objects/error-overview.ndjson
git add infrastructure/kibana-objects/
git commit -m "feat: add error overview dashboard export"
git push

Import ở môi trường khác

  • Stack Management → Saved objects → Import (góc phải).
  • Drag-drop .ndjson.
  • Chọn Overwrite existing objects nếu muốn override, hoặc Create new with random IDs nếu muốn tách bạch.

Phần 6: Kibana REST API

Đây là lý do mình thích Kibana hơn nhiều log tool khác — mọi thứ làm được trên UI đều làm được qua API. Dùng cho CI/CD, automation, hoặc viết script riêng.

Auth methods

Ba cách cơ bản:

CáchHeaderKhi nào dùng
Basic authAuthorization: Basic <base64(user:pass)> hoặc curl -u user:passThử nhanh, script local
API keyAuthorization: ApiKey <encoded>CI/CD, app integration — có scope + expiry
Service tokenAuthorization: Bearer <token>Service-to-service (Kibana → ES dùng cái này)

Header bắt buộc cho mọi request ghi vào Kibana API: kbn-xsrf: true.

Ví dụ 1: Tạo Data View

KIBANA_URL="https://kibana.example.com"
curl -s -u "$KB_USER:$KB_PASS" \
  -H "kbn-xsrf: true" \
  -H "Content-Type: application/json" \
  -X POST "$KIBANA_URL/api/data_views/data_view" \
  -d '{
    "data_view": {
      "title": "app-logs-*",
      "name": "app-logs",
      "timeFieldName": "@timestamp"
    }
  }'

Ví dụ 2: Export + Import saved object (workflow CI/CD)

Export 1 dashboard với full dependencies:

curl -s -u "$KB_USER:$KB_PASS" \
  -H "kbn-xsrf: true" \
  -H "Content-Type: application/json" \
  -X POST "$KIBANA_URL/api/saved_objects/_export" \
  -d '{
    "objects": [{"type":"dashboard","id":"<DASHBOARD_ID>"}],
    "includeReferencesDeep": true
  }' > error-dashboard.ndjson

Import vào môi trường mới (thêm overwrite=true nếu muốn ghi đè):

curl -s -u "$KB_USER:$KB_PASS" \
  -H "kbn-xsrf: true" \
  -X POST "$KIBANA_URL/api/saved_objects/_import?overwrite=true" \
  --form [email protected]

Pipeline CI/CD của mình thường là: push lên main repo <org>/<logs-repo> → GitHub Actions call API import vào ES staging → smoke test → promote sang production.

Ví dụ 3: Tạo Alert rule

Slack alert khi số error > 10 trong 5 phút.

Bước 1 — tạo Slack connector:

curl -s -u "$KB_USER:$KB_PASS" \
  -H "kbn-xsrf: true" \
  -H "Content-Type: application/json" \
  -X POST "$KIBANA_URL/api/actions/connector" \
  -d '{
    "name": "Slack #alerts",
    "connector_type_id": ".slack",
    "secrets": {"webhookUrl": "https://hooks.slack.com/services/..."}
  }'

Bước 2 — tạo ES query rule, reference connector ID vừa tạo:

curl -s -u "$KB_USER:$KB_PASS" \
  -H "kbn-xsrf: true" \
  -H "Content-Type: application/json" \
  -X POST "$KIBANA_URL/api/alerting/rule" \
  -d '{
    "name": "Error burst",
    "rule_type_id": ".es-query",
    "consumer": "alerts",
    "schedule": {"interval": "1m"},
    "params": {
      "index": ["app-logs-*"],
      "timeField": "@timestamp",
      "esQuery": "{\"query\":{\"match\":{\"Level\":\"Error\"}}}",
      "size": 100,
      "threshold": [10],
      "thresholdComparator": ">",
      "timeWindowSize": 5,
      "timeWindowUnit": "m"
    },
    "actions": [{
      "group": "query matched",
      "id": "<CONNECTOR_ID>",
      "params": {"message": "{{context.hits}} errors in last 5min"}
    }]
  }'

Phần 7: Tạo API Key an toàn

Vì sao cần API key thay vì basic auth

  • Rotate được mà không đổi password chính.
  • Scope quyền riêng — key chỉ đọc được 1 index, không full admin.
  • Expiry — set 90 ngày → tự hết hạn, tuân thủ SOC2/ISO.
  • Audit — ES log được key nào gọi API nào.

Tạo qua GUI

≡ → Management → Stack Management → Security → API keys → Create API key.

Form sẽ thấy các toggle:

ToggleTắt (mặc định)Nên bật
Apply expiration dateKhông bao giờ hết hạnLUÔN LUÔN bật — compliance yêu cầu
Control security privilegesKế thừa full quyền của user tạo (có thể là superuser)LUÔN LUÔN bật trừ khi cần full quyền
Add metadatakhông có tagbật nếu cần tracking (owner, purpose)

Cảnh báo quan trọng: Nếu không bật “Control security privileges” và bạn đang login bằng superuser → API key sinh ra sẽ có toàn quyền cluster. Leak key này = mất cluster.

Sau khi bật “Control security privileges”, paste role descriptor JSON:

{
  "logs_reader": {
    "cluster": ["monitor"],
    "indices": [
      {
        "names": ["app-logs-*"],
        "privileges": ["read", "view_index_metadata"]
      }
    ]
  }
}

Key này chỉ đọc được app-logs-*, không tạo index mới, không xoá data. An toàn để embed vào script.

Tạo qua Dev Tools Console

≡ → Management → Dev Tools. Paste:

POST /_security/api_key
{
  "name": "ci-dashboard-import",
  "expiration": "90d",
  "role_descriptors": {
    "logs_reader": {
      "cluster": ["monitor"],
      "indices": [
        {
          "names": ["app-logs-*"],
          "privileges": ["read", "view_index_metadata"]
        }
      ]
    }
  }
}

Click icon play (Ctrl/Cmd+Enter). Response:

{
  "id": "VnJ5...",
  "name": "ci-dashboard-import",
  "expiration": 1731456000000,
  "api_key": "dGhpc19pc19hX3Rlc3Q...",
  "encoded": "Vm5KNUxqb3VMaTAuLi4="
}

Copy field encoded ngay — chỉ hiện 1 lần duy nhất. Dùng trực tiếp:

curl -H "Authorization: ApiKey <encoded>" "$KIBANA_URL/api/data_views"

Quản lý key

List key của mình:

GET /_security/api_key?username=<your-username>

Revoke 1 key:

DELETE /_security/api_key
{
  "ids": ["VnJ5..."]
}

Lưu key ở đâu

Tuyệt đối không commit key vào git. Các lựa chọn an toàn:

  • Local dev: .env (đã gitignore) hoặc secrets/ folder.
  • CI/CD: GitHub Actions Secrets / GitLab CI Variables / Jenkins Credentials.
  • Production runtime: AWS Secrets Manager, HashiCorp Vault, Kubernetes Secrets + External Secrets Operator.

Checklist trước khi dùng API key production

  • Expiration set (30-90 ngày)
  • Privileges scoped tới index tối thiểu cần thiết
  • Metadata tag owner + purpose
  • Đưa vào secret manager, không hardcode
  • Calendar reminder rotate trước 7 ngày hết hạn

Cheatsheet

Gói gọn bài thành bảng tra nhanh:

ViệcNơi làmCú pháp / lưu ý
Filter errorDiscover → KQLLevel : ("Error" or "Fatal")
Filter không ra kết quảCheck theo thứ tựtime range → data view → field case → value case
ES|QL queryDiscover → ES|QLWHERE TRƯỚC LIMIT
Saved SearchDiscover → SaveTeam share cùng view
DashboardDashboards → CreateCount = số record, không cần field
Export codeStack Management → Saved objects → ExportInclude related objects
REST API authheaderAuthorization: ApiKey <encoded>
API keyStack Management → Security → API keysBẬT expiration + privileges
XSRFMọi POST/PUT/DELETEHeader kbn-xsrf: true

Lời kết

Kibana không khó — chỉ là mỗi ngách nhỏ có một pitfall riêng. Nắm được field case, pipeline order của ES|QL, XSRF header, và quy tắc tạo API key có scope là đủ để dev backend tự chủ với logging stack mà không phải mở ticket cho DevOps.

Bài tiếp theo trong series Kibana từ A đến Z sẽ đi sâu vào lens + canvas cho việc xây report, và cách thiết kế index lifecycle cho production. Nếu có topic cụ thể bạn muốn mình viết — ví dụ “alert rule nâng cao”, “role-based access cho multi-team”, “Kibana behind reverse proxy” — cứ drop comment hoặc email.