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:
| Field | Ví 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 |
Exception | full stack trace | Chỉ có khi log exception |
Properties.* | custom fields | Structured properties |
@timestamp | ISO8601 | Do 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:
- Xoá hết filter → có log không? Nếu không → time range hoặc data view sai.
- Gõ
Level : *→ có không? Nếu không → field tên khác (có thể làlevellowercase tuỳ logger). - Expand 1 document → copy field name chính xác từ JSON panel → dùng đúng case.
- Bảo backend check xem có thật sự log
Errornà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:
LIMIT 1000trước → lấy 1000 dòng đầu bất kỳ (99% là Information/Warning vì chúng chiếm đại đa số).WHEREsau → 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 DESCtrướcLIMITnế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)
- Discover, đặt data view
app-logs-*, time range “Last 24 hours”. - Gõ KQL:
Level : ("Error" or "Fatal"). - Bên trái panel Available fields — click
+để add column:Level,Properties.ApplicationName,RenderedMessage,Exception. - Click header
@timestamp→ sort New-Old. - 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
@timestamptừ 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ách | Header | Khi nào dùng |
|---|---|---|
| Basic auth | Authorization: Basic <base64(user:pass)> hoặc curl -u user:pass | Thử nhanh, script local |
| API key | Authorization: ApiKey <encoded> | CI/CD, app integration — có scope + expiry |
| Service token | Authorization: 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:
| Toggle | Tắt (mặc định) | Nên bật |
|---|---|---|
| Apply expiration date | Không bao giờ hết hạn | LUÔN LUÔN bật — compliance yêu cầu |
| Control security privileges | Kế 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 metadata | không có tag | bậ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ặcsecrets/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ệc | Nơi làm | Cú pháp / lưu ý |
|---|---|---|
| Filter error | Discover → KQL | Level : ("Error" or "Fatal") |
| Filter không ra kết quả | Check theo thứ tự | time range → data view → field case → value case |
| ES|QL query | Discover → ES|QL | WHERE TRƯỚC LIMIT |
| Saved Search | Discover → Save | Team share cùng view |
| Dashboard | Dashboards → Create | Count = số record, không cần field |
| Export code | Stack Management → Saved objects → Export | Include related objects |
| REST API auth | header | Authorization: ApiKey <encoded> |
| API key | Stack Management → Security → API keys | BẬT expiration + privileges |
| XSRF | Mọi POST/PUT/DELETE | Header 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.