SIEM/Log Management CHUẨN HÓA CẤU TRÚC VÀ QUY CÁCH FORMAT CODE VEGA

Tài liệu này quy định các tiêu chuẩn bắt buộc khi viết mã Vega nhằm đảm bảo các dashboard có khả năng Responsive, không bị sập giao diện khi thiếu dữ liệu, tối ưu hóa hiệu năng truy vấn OpenSearch và đồng bộ về mặt thẩm mỹ.

0. GIỚI THIỆU TỔNG QUAN VỀ VEGA VISUALIZATION


0.1. Vega là gì?


Vega (và phiên bản rút gọn Vega-Lite) là một ngôn ngữ lập trình khai báo (Declarative Language) dùng để thiết kế các biểu đồ và đồ họa trực quan hóa dữ liệu. Thay vì phải viết các vòng lặp Javascript phức tạp để vẽ từng đường nét, với Vega, bạn chỉ cần mô tả biểu đồ dưới định dạng JSON. Vega sẽ biên dịch JSON này thành các bản vẽ Canvas hoặc SVG đằng sau hậu trường (dựa trên thư viện D3.js nền tảng).
Trong hệ sinh thái Elastic Stack (Kibana) và OpenSearch Dashboards, Vega được tích hợp sẵn như một plugin để xây dựng các Custom Visualization.
0.2. Mục đích và Tại sao lại sử dụng Vega?
Các công cụ kéo thả có sẵn của OpenSearch (như Gauge ,TSVB, Aggregation Based) rất dễ sử dụng nhưng lại có những giới hạn cứng ngắc. Chúng ta sử dụng Vega khi cần phá vỡ các giới hạn đó:
  1. Kiến trúc Đa truy vấn (Multi-Query): Các công cụ thường chỉ cho phép truy vấn trên một Index Pattern duy nhất. Vega cho phép bạn lấy dữ liệu từ nhiều index khác nhau, thậm chí gọi API từ bên ngoài, sau đó kết hợp (Join) chúng lại trên cùng một biểu đồ (Ví dụ: nối CPU của Host với IOPS của Datastore).
  2. Biến đổi dữ liệu tùy chỉnh (Custom Transforms): Khả năng sử dụng các hàm toán học, logic (if/else), chuỗi (Regex) để xử lý dữ liệu ngay trên RAM của trình duyệt trước khi vẽ, giúp sửa lỗi dữ liệu thô mà không cần can thiệp vào Logstash/Pipeline.
  3. Tùy biến giao diện tuyệt đối (Pixel-Perfect Control): Bạn có toàn quyền quyết định từng pixel: biểu đồ dạng đồng hồ (Gauge), dạng tổ ong, thêm icon, thay đổi màu sắc linh hoạt theo điều kiện (Conditional Formatting), hoặc tạo ra các dashboard lồng ghép bảng biểu và đồ thị.

0.3. Cấu trúc tuần tự của một tệp cấu hình Vega


Để trình biên dịch Vega hiểu và vẽ đúng, một tệp JSON Vega luôn được xây dựng theo một hệ thống phân lớp logic từ ngoài vào trong. Người lập trình cần tuân thủ đúng thứ tự cấu trúc sau:
1. Khai báo nền tảng (Metadata & Config)
  • Nằm ở đầu file, bao gồm: $schema, autosize, padding, background, config.
  • Mục đích: Khai báo phiên bản Vega đang dùng, xác định khoảng lề, màu nền tổng thể, và phông chữ mặc định cho toàn bộ bản vẽ.
2. Khai báo Tín hiệu (Signals)
  • Chứa khối signals: [...].
  • Mục đích: Đây là các biến số động. Nó dùng để bắt kích thước màn hình hiện tại (width, height), tính toán trước các tọa độ trung tâm, hoặc lưu trữ các thông số ngưỡng cảnh báo. Khi màn hình co giãn, Signals sẽ tự động cập nhật và báo cho các phần khác vẽ lại.
3. Khai báo Dữ liệu (Data & Transforms)
  • Chứa khối data: [...].
  • Mục đích: Nơi định nghĩa nguồn cấp dữ liệu (url, query, aggs). Quan trọng nhất là phần transform nằm bên trong, dùng để nhào nặn dữ liệu: định dạng lại ngày tháng, cắt chuỗi chữ, lọc bỏ giá trị null, hoặc làm toán (fold, formula).
4. Khai báo Thang đo (Scales)
  • Chứa khối scales: [...].
  • Mục đích: Làm cầu nối giữa "Dữ liệu" và "Hình học". Nó quy đổi các con số (ví dụ: CPU từ 0-100%) thành độ dài pixel trên màn hình (ví dụ: chiều cao cột từ 0-400px), hoặc quy đổi các trạng thái thành mảng màu sắc cụ thể (Xanh, Vàng, Đỏ).
5. Chú giải và Trục tọa độ (Legends & Axes)
  • Chứa khối legends: [...] và axes: [...].
  • Mục đích: Vẽ ra các thước đo (trục X, trục Y) và các bảng chú thích màu sắc để người dùng hiểu được biểu đồ đang biểu diễn số liệu gì.
6. Khai báo Hình khối hiển thị (Marks)
  • Chứa khối marks: [...].
  • Mục đích:Đây là phần thực thi cuối cùng. Từ các tọa độ (Signals) và dữ liệu đã xử lý (Data), Marks sẽ quyết định dùng gì để vẽ:
    • rect: Vẽ hình chữ nhật, cột (Bar chart).
    • line: Vẽ đường thẳng (Timeseries chart).
    • arc: Vẽ hình vòng cung, biểu đồ tròn, đồng hồ.
    • text: Hiển thị chữ số, nhãn dán.
    • group: Đóng gói một nhóm các nét vẽ lại với nhau để dễ căn lề và tạo hiệu ứng đồ họa lớp (Layering).

1. NGUYÊN TẮC AN TOÀN DỮ LIỆU (NULL SAFETY)


Lỗi phổ biến nhất trên OpenSearch Dashboards là Cannot read properties of undefined (reading 'value'), xảy ra khi dải thời gian thay đổi dẫn đến một số bucket hoặc metric bị rỗng (máy ảo tắt, exporter mất kết nối).
Quy định viết biểu thức Transform (Formula):
- Không viết tắt theo dạng: datum.metric.val.value || 0. Biểu thức này sẽ gây sập toàn bộ panel nếu một trong các cấp thư mục (metric hoặc val) không tồn tại.
- Sử dụng kiểm tra an toàn đa tầng (Multi-level Null Safety Check):
{
"type": "formula",
"as": "cpu_pct",
"expr": "datum.cpu && datum.cpu.v && datum.cpu.v.value != null ? datum.cpu.v.value : 0"
}
- Giới hạn biên dữ liệu: Đối với các chỉ số phần trăm hoặc tỉ lệ, luôn cần ép biên để tránh các giá trị lạ:
{
"type": "formula",
"as": "cpu_safe",
"expr": "datum.cpu_pct > 100 ? 100 : datum.cpu_pct < 0 ? 0 : datum.cpu_pct"
}

2. KIẾN TRÚC TRUY VẤN DỮ LIỆU & KHÓA THỜI GIAN (REALTIME)


Quy định về kiểm soát dải thời gian (Time Range Constraints)
Tùy thuộc vào mục đích hiển thị của panel, mã nguồn cấu hình dải thời gian phải tuân theo một trong hai hình thức:
- Panel Linh hoạt (Theo Time Picker tổng): Sử dụng khi cần phân tích lịch sử.
"%context%": true,
"%timefield%": "@timestamp"
- Panel Realtime Cứng: Đối với các panel giám sát realtime chu kỳ ngắn (ví dụ: 5 phút gần nhất), bắt buộc gỡ bỏ %context% và đưa trực tiếp bộ lọc range vào cấu hình query của OpenSearch. Điều này ngăn chặn việc người dùng chọn nhầm dải thời gian quá dài làm treo trình duyệt hoặc quá tải cụm OpenSearch Cluster.
"url": {
"index": "metrics-prometheus-*",
"body": {
"size": 0,
"query": {
"bool": {
"filter": [
{
"range": {
"@timestamp": {
"gte": "now-5m",
"lte": "now"
}
}
}
]
}
},
"aggs": { ... }
}
}
Quy định về cấu trúc liên kết dữ liệu (Data Join / Lookup):
- Hạn chế Script Aggregation: Không lạm dụng script chạy Regex trong thân truy vấn OpenSearch vì lý do bảo mật hệ thống và hiệu năng.
- Kiến trúc Đa Truy vấn (Multi-Query Pipeline): Khi cần kết hợp các nhóm metric không có chung trường định danh từ gốc (ví dụ: Metric CPU/RAM của VM đi theo IP Host, nhưng Metric Datastore đi theo Tên ổ đĩa), cần tách thành các tập dữ liệu độc lập (data blocks) và sử dụng bộ biến đổi lookup của Vega để gom nhóm trên RAM trình duyệt:
{
"name": "combined_data",
"source": "host_metrics",
"transform": [
{
"type": "lookup",
"from": "datastore_metrics",
"key": "host_ip",
"fields": ["host_ip"],
"values": ["disk_pct"]
}
]
}

Responsive


Mọi panel Vega phải tự động tính toán lại kích thước dựa trên khung chứa (Container) của OpenSearch. Không được sử dụng các kích thước cố định bằng số cho sơ đồ marker chính.
Khai báo tín hiệu chuẩn (Standard Signals)
Mọi file cấu hình bắt buộc phải có các signals nền tảng sau:
"signals": [
{ "name": "width", "update": "containerSize()[0] || 900" },
{ "name": "height", "update": "containerSize()[1] || 400" },
{ "name": "chartX", "value": 60 },
{ "name": "chartY", "value": 80 },
{ "name": "chartW", "update": "max(200, width - chartX - 40)" },
{ "name": "chartH", "update": "max(150, height - chartY - 60)" }
]
chartW và chartH là không gian hiển thị thực tế của biểu đồ sau khi đã trừ đi khoảng cách lề (padding) cho tiêu đề và trục tọa độ. Mọi marks, scales bên trong group biểu đồ phải tham chiếu theo hai tín hiệu này.

4. CĂN CHỈNH ĐỒ HỌA TUYỆT ĐỐI (PIXEL-PERFECT ALIGNMENT)


Đối với các dạng biểu đồ thanh ngang dạng bảng (Horizontal Bar Chart) hoặc Bảng danh sách kết hợp Marker, việc sử dụng hệ trục tọa độ band mặc định khi số lượng bản ghi ít sẽ gây ra hiện tượng méo thanh bar hoặc chữ bị lệch so với hình khối.
Quy tắc tính toán vị trí theo dòng (Absolute Row Math)
- Tính toán vị trí y tuyệt đối dựa trên số thứ tự dòng (rank) trong phần transform để cố định khoảng cách giữa các dòng (ví dụ: 45px mỗi dòng):
{ "type": "window", "ops": ["row_number"], "as": ["rank"] },
{ "type": "formula", "as": "y_pos", "expr": "(datum.rank - 1) * 45 + 22" }
- Quy tắc căn đường tâm: Khi vẽ nhiều thành phần trên cùng một dòng (Thanh Bar, Khung số STT, Chữ hiển thị), tất cả các marks này phải sử dụng chung thuộc tính yc (Center Y) trỏ thẳng vào tín hiệu y_pos tuyệt đối để đảm bảo đồng trục ngang hoàn hảo:
// Khung chữ nhật (Mark Rect)
"yc": { "field": "y_pos" }, "height": { "value": 28 }

// Nhãn văn bản (Mark Text)
"y": { "field": "y_pos" }, "baseline": { "value": "middle" }, "dy": { "value": 1 }

5. CÁC HÀM XỬ LÝ CHUỖI ĐẶC THÙ TRONG VEGA EXPRESSION


Hệ thống biểu thức của Vega không phải là JavaScript thuần túy mà là một tập hợp con (Subset). Một số hàm JS thông dụng sẽ bị báo lỗi Unrecognized function.
Bảng tra cứu quy chuẩn hàm xử lý chuỗi:
Thao tácCú pháp
Tìm vị trílastindexof(str, '.')
Khớp Regextest(/regex/, str)
Đổi chữ thườnglower(str)
Cắt chuỗisubstring(str, 0, 5)
Tách mảngsplit(str, '.')

6. MẪU KHUNG CẤU HÌNH VEGA CHUẨN (SPEC SKELETON)


Mẫu khung bar:
{
"$schema": "https://vega.github.io/schema/vega/v5.json",
"autosize": { "type": "fit", "contains": "padding" },
"padding": 10,
"background": "#020b14",
"config": {
"view": { "stroke": "transparent" },
"font": "Arial, sans-serif"
},
"signals": [
{ "name": "width", "update": "containerSize()[0] || 900" },
{ "name": "height", "update": "containerSize()[1] || 400" },
{ "name": "chartX", "value": 60 },
{ "name": "chartY", "value": 80 },
{ "name": "chartW", "update": "width - chartX - 20" },
{ "name": "chartH", "update": "height - chartY - 60" }
],
"data": [
{
"name": "es_data",
"url": {
"index": "metrics-prometheus-*",
"body": {
"size": 0,
"query": {
"bool": {
"filter": [
{ "range": { "@timestamp": { "gte": "now-5m", "lte": "now" } } }
]
}
},
"aggs": {
"main_terms": {
"terms": { "field": "tags.esxhostname.keyword", "size": 10 }
}
}
}
},
"format": { "property": "aggregations.main_terms.buckets" },
"transform": [
{ "type": "formula", "as": "clean_id", "expr": "split(datum.key, '.')[0]" },
{ "type": "window", "ops": ["row_number"], "as": ["rank"] },
{ "type": "formula", "as": "y_pos", "expr": "(datum.rank - 1) * 45 + 22" }
]
}
],
"scales": [
{
"name": "xscale",
"type": "linear",
"domain": [0, 100],
"range": [0, { "signal": "chartW" }]
}
],
"marks": [
{
"type": "group",
"encode": {
"update": {
"x": { "signal": "chartX" },
"y": { "signal": "chartY" },
"width": { "signal": "chartW" },
"height": { "signal": "chartH" }
}
},
"axes": [
{ "orient": "bottom", "scale": "xscale", "grid": true, "gridColor": "#ffffff", "gridOpacity": 0.05 }
],
"marks": [
{
"type": "rect",
"from": { "data": "es_data" },
"encode": {
"enter": { "height": { "value": 28 }, "cornerRadiusRight": { "value": 4 } },
"update": {
"x": { "value": 0 },
"x2": { "scale": "xscale", "value": 50 },
"yc": { "field": "y_pos" },
"fill": { "value": "#18b4ff" }
}
}
},
{
"type": "text",
"from": { "data": "es_data" },
"encode": {
"enter": { "align": { "value": "left" }, "baseline": { "value": "middle" }, "dy": { "value": 1 }, "fill": { "value": "#ffffff" } },
"update": {
"x": { "value": -50 },
"y": { "field": "y_pos" },
"text": { "field": "clean_id" }
}
}
}
]
}
]
}
1781705269943.png

Mẫu cho dạng Gauge:
{
"$schema": "https://vega.github.io/schema/vega/v5.json",
"autosize": {
"type": "fit",
"contains": "padding"
},
"padding": 0,
"background": "#020b14",
"config": {
"view": { "stroke": "transparent" },
"font": "Arial, sans-serif"
},
"signals": [
{ "name": "width", "update": "containerSize()[0] || 400" },
{ "name": "height", "update": "containerSize()[1] || 300" },

// TÍNH TOÁN HỆ TỌA ĐỘ TRUNG TÂM
{ "name": "cx", "update": "width / 2" },
{ "name": "cy", "update": "height * 0.65" },
{ "name": "radius", "update": "min(width, height) * 0.35" },
{ "name": "innerRadius", "update": "radius * 0.78" },

// KIỂM SOÁT DỮ LIỆU ĐẦU VÀO (NULL SAFETY)
// TODO: Sửa data('es_data')[0] trỏ tới đúng tên biến trả về từ Aggregations
{
"name": "metric_value",
"update": "data('es_data')[0] && data('es_data')[0].giatri.value != null ? data('es_data')[0].giatri.value : 0"
},
{ "name": "metric_percent", "update": "metric_value > 100 ? 100 : metric_value < 0 ? 0 : metric_value" },

// TÍNH TOÁN LƯỢNG GIÁC CHO KIM CHỈ & GÓC VẼ
{ "name": "start_angle", "update": "-PI / 2" },
{ "name": "end_angle", "update": "-PI / 2 + (metric_percent / 100) * PI" },
{ "name": "needle_angle", "update": "-PI / 2 + (metric_percent / 100) * PI" },
{ "name": "needle_len", "update": "radius * 0.82" },
{ "name": "needle_x", "update": "cx + sin(needle_angle) * needle_len" },
{ "name": "needle_y", "update": "cy - cos(needle_angle) * needle_len" },

// CẤU HÌNH NGƯỠNG CẢNH BÁO MÀU SẮC (THERESHOLDS)
{ "name": "metric_color", "update": "metric_percent < 50 ? '#43e06b' : metric_percent < 75 ? '#ffc928' : metric_percent < 90 ? '#ff8a00' : '#ff4d4d'" },
{ "name": "metric_status", "update": "metric_percent < 50 ? 'Trạng thái: Ổn định' : metric_percent < 75 ? 'Trạng thái: Cần theo dõi' : metric_percent < 90 ? 'Trạng thái: Sử dụng cao' : 'Nguy hiểm: Quá tải!'" }
],
"data": [
{
"name": "es_data",
"url": {
"index": "metrics-prometheus-*",
"body": {
"size": 0,
"query": {
"bool": {
"filter": [
{ "range": { "@timestamp": { "gte": "now-5m", "lte": "now" } } }
// TODO: Thêm filter term vào đây
]
}
},
"aggs": {
"giatri": {
"avg": { "field": "gauge.value" } // TODO: Đổi hàm avg/max/sum và field tương ứng
}
}
}
},
"format": { "property": "aggregations" }
},
{
"name": "ticks",
"values": [
{ "label": "0%", "value": 0 },
{ "label": "25%", "value": 25 },
{ "label": "50%", "value": 50 },
{ "label": "75%", "value": 75 },
{ "label": "100%", "value": 100 }
],
"transform": [
{ "type": "formula", "as": "angle", "expr": "-PI / 2 + (datum.value / 100) * PI" },
{ "type": "formula", "as": "x1", "expr": "cx + sin(datum.angle) * (radius + 2)" },
{ "type": "formula", "as": "y1", "expr": "cy - cos(datum.angle) * (radius + 2)" },
{ "type": "formula", "as": "x2", "expr": "cx + sin(datum.angle) * (radius + 16)" },
{ "type": "formula", "as": "y2", "expr": "cy - cos(datum.angle) * (radius + 16)" },
{ "type": "formula", "as": "lx", "expr": "cx + sin(datum.angle) * (radius + 38)" },
{ "type": "formula", "as": "ly", "expr": "cy - cos(datum.angle) * (radius + 38)" }
]
}
],
"marks": [
{
"type": "rect",
"encode": {
"enter": {
"x": { "value": 8 }, "y": { "value": 8 },
"width": { "signal": "width - 16" }, "height": { "signal": "height - 16" },
"cornerRadius": { "value": 14 },
"fill": { "value": "#061525" }, "fillOpacity": { "value": 0.94 },
"stroke": { "value": "#18b4ff" }, "strokeWidth": { "value": 1.5 }, "strokeOpacity": { "value": 0.5 }
}
}
},
{
"type": "text",
"encode": {
"enter": {
"x": { "value": 30 }, "y": { "value": 35 },
"text": { "value": "TÊN BIỂU ĐỒ GAUGE" },
"fontSize": { "value": 22 }, "fontWeight": { "value": "bold" },
"fill": { "value": "#f3f7ff" }, "baseline": { "value": "middle" }
}
}
},

// NỀN ĐỒNG HỒ (Màu tối)
{
"type": "arc",
"encode": {
"enter": {
"x": { "signal": "cx" }, "y": { "signal": "cy" },
"startAngle": { "value": -1.5707963268 }, "endAngle": { "value": 1.5707963268 },
"innerRadius": { "signal": "innerRadius" }, "outerRadius": { "signal": "radius" },
"fill": { "value": "#1b2a3a" }, "fillOpacity": { "value": 0.95 }
}
}
},

// DẢI MÀU HIỂN THỊ GIÁ TRỊ THỰC TẾ
{
"type": "arc",
"encode": {
"enter": {
"x": { "signal": "cx" }, "y": { "signal": "cy" },
"startAngle": { "signal": "start_angle" },
"innerRadius": { "signal": "innerRadius" }, "outerRadius": { "signal": "radius" }
},
"update": {
"endAngle": { "signal": "end_angle" },
"fill": { "signal": "metric_color" }
}
}
},

// VẼ VẠCH CHIA (TICKS)
{
"type": "rule",
"from": { "data": "ticks" },
"encode": {
"enter": {
"x": { "field": "x1" }, "y": { "field": "y1" },
"x2": { "field": "x2" }, "y2": { "field": "y2" },
"stroke": { "value": "#d8e6f5" }, "strokeWidth": { "value": 2 }
}
}
},

// CHỮ SỐ TRÊN VẠCH CHIA
{
"type": "text",
"from": { "data": "ticks" },
"encode": {
"enter": {
"x": { "field": "lx" }, "y": { "field": "ly" },
"text": { "field": "label" },
"align": { "value": "center" }, "baseline": { "value": "middle" },
"fontSize": { "value": 14 }, "fontWeight": { "value": "bold" },
"fill": { "value": "#9fb0c2" }
}
}
},

// KIM CHỈ (NEEDLE)
{
"type": "rule",
"encode": {
"enter": {
"x": { "signal": "cx" }, "y": { "signal": "cy" },
"strokeWidth": { "value": 8 }, "strokeCap": { "value": "round" }
},
"update": {
"x2": { "signal": "needle_x" }, "y2": { "signal": "needle_y" },
"stroke": { "signal": "metric_color" }
}
}
},

// CHỐT TRUNG TÂM (BASE PIN)
{
"type": "symbol",
"encode": {
"enter": {
"x": { "signal": "cx" }, "y": { "signal": "cy" },
"size": { "value": 1500 }, "shape": { "value": "circle" },
"fill": { "value": "#071827" }, "stroke": { "value": "#29455d" }, "strokeWidth": { "value": 2 }
}
}
},
{
"type": "symbol",
"encode": {
"enter": {
"x": { "signal": "cx" }, "y": { "signal": "cy" },
"size": { "value": 600 }, "shape": { "value": "circle" }
},
"update": {
"fill": { "signal": "metric_color" }
}
}
},

// CHỮ SỐ HIỂN THỊ LỚN Ở GIỮA
{
"type": "text",
"encode": {
"enter": {
"x": { "signal": "cx" }, "y": { "signal": "height - 60" },
"align": { "value": "center" }, "baseline": { "value": "middle" },
"fontSize": { "value": 55 }, "fontWeight": { "value": "bold" }
},
"update": {
"text": { "signal": "format(metric_percent, '.1f') + '%'" },
"fill": { "signal": "metric_color" }
}
}
},

// NHÃN TRẠNG THÁI BÊN DƯỚI
{
"type": "text",
"encode": {
"enter": {
"x": { "signal": "cx" }, "y": { "signal": "height - 25" },
"align": { "value": "center" }, "baseline": { "value": "middle" },
"fontSize": { "value": 16 }, "fill": { "value": "#8ea0b5" }
},
"update": {
"text": { "signal": "metric_status" }
}
}
}
]
}
1781705446031.png

Mẫu cho dạng Timeseries:
{
"$schema": "https://vega.github.io/schema/vega/v5.json",
"autosize": {
"type": "fit",
"contains": "padding"
},
"padding": 0,
"background": "#020b14",
"config": {
"view": { "stroke": "transparent" },
"font": "Arial, sans-serif"
},
"signals": [
{ "name": "width", "update": "containerSize()[0] || 900" },
{ "name": "height", "update": "containerSize()[1] || 400" },

// TÍNH TOÁN VÙNG VẼ BIỂU ĐỒ (INNER CHART AREA)
{ "name": "chartX", "value": 75 },
{ "name": "chartY", "value": 145 },
{ "name": "chartRight", "value": 175 },
{ "name": "chartBottom", "value": 100 },
{ "name": "chartW", "update": "width - chartX - chartRight" },
{ "name": "chartH", "update": "height - chartY - chartBottom" },

// TỰ ĐỘNG SCALE TRỤC Y TẠO HEADROOM 25%
{ "name": "maxValue", "update": "data('max_data')[0] && data('max_data')[0].max_val != null ? data('max_data')[0].max_val : 1" },
{ "name": "yMax", "update": "maxValue <= 10 ? 10 : ceil(maxValue * 1.25 / 100) * 100" },

// TRÍCH XUẤT GIÁ TRỊ MỚI NHẤT (LATEST) CHO BẢNG KPI
{ "name": "lastMetric1", "update": "data('latest_metric1')[0] && data('latest_metric1')[0].val1 != null ? data('latest_metric1')[0].val1 : 0" },
{ "name": "lastMetric2", "update": "data('latest_metric2')[0] && data('latest_metric2')[0].val2 != null ? data('latest_metric2')[0].val2 : 0" }
],
"data": [
{
"name": "time_buckets",
"url": {
"%context%": true,
"%timefield%": "@timestamp",
"index": "metrics-prometheus-*",
"body": {
"size": 0,
"aggs": {
"histogram": {
"date_histogram": {
"field": "@timestamp",
"interval": { "%autointerval%": true },
"min_doc_count": 0,
"extended_bounds": {
"min": { "%timefilter%": "min" },
"max": { "%timefilter%": "max" }
}
},
"aggs": {
"metric1_docs": {
"filter": {
"bool": {
"filter": [
// TODO: Filter cho đường số 1
{ "term": { "name.keyword": "your_metric_1_name" } }
]
}
},
"aggs": { "v": { "avg": { "field": "gauge.value" } } }
},
"metric2_docs": {
"filter": {
"bool": {
"filter": [
// TODO: Filter cho đường số 2
{ "term": { "name.keyword": "your_metric_2_name" } }
]
}
},
"aggs": { "v": { "avg": { "field": "gauge.value" } } }
}
}
}
}
}
},
"format": { "property": "aggregations.histogram.buckets" },
"transform": [
{ "type": "formula", "as": "ts", "expr": "toDate(datum.key)" },
{ "type": "formula", "as": "val1", "expr": "datum.metric1_docs && datum.metric1_docs.v && datum.metric1_docs.v.value != null ? datum.metric1_docs.v.value : null" },
{ "type": "formula", "as": "val2", "expr": "datum.metric2_docs && datum.metric2_docs.v && datum.metric2_docs.v.value != null ? datum.metric2_docs.v.value : null" }
]
},

// TÁCH DỮ LIỆU VÀ LỌC NULL ĐỂ VẼ LINE KHÔNG BỊ ĐỨT
{ "name": "line1_data", "source": "time_buckets", "transform": [{ "type": "filter", "expr": "datum.val1 != null" }] },
{ "name": "line2_data", "source": "time_buckets", "transform": [{ "type": "filter", "expr": "datum.val2 != null" }] },

// TÌM LATEST VALUE (Dòng dữ liệu gần nhất)
{
"name": "latest_metric1", "source": "line1_data",
"transform": [
{ "type": "window", "sort": { "field": "ts", "order": "descending" }, "ops": ["row_number"], "as": ["rn"] },
{ "type": "filter", "expr": "datum.rn == 1" }
]
},
{
"name": "latest_metric2", "source": "line2_data",
"transform": [
{ "type": "window", "sort": { "field": "ts", "order": "descending" }, "ops": ["row_number"], "as": ["rn"] },
{ "type": "filter", "expr": "datum.rn == 1" }
]
},

// TÌM MAX VALUE ĐỂ SCALE TRỤC Y
{
"name": "max_data", "source": "time_buckets",
"transform": [
{ "type": "fold", "fields": ["val1", "val2"], "as": ["metric", "value"] },
{ "type": "filter", "expr": "datum.value != null" },
{ "type": "aggregate", "fields": ["value"], "ops": ["max"], "as": ["max_val"] }
]
},

// TẠO LƯỚI TRỤC Y (Y-TICKS) CHUẨN XÁC
{
"name": "y_ticks",
"values": [{ "v": 0 }, { "v": 0.25 }, { "v": 0.5 }, { "v": 0.75 }, { "v": 1 }],
"transform": [
{ "type": "formula", "as": "value", "expr": "datum.v * yMax" },
{ "type": "formula", "as": "label", "expr": "format(datum.value, '.0f')" }
]
},

// CHỐNG ĐÈ CHỮ TRỤC X BẰNG CÁCH TÍNH TOÁN BƯỚC NHẢY (STEP)
{
"name": "x_labels", "source": "time_buckets",
"transform": [
{ "type": "window", "ops": ["row_number"], "as": ["rn"] },
{ "type": "formula", "as": "step", "expr": "max(1, ceil(length(data('time_buckets')) / 6))" },
{ "type": "filter", "expr": "datum.rn == 1 || datum.rn % datum.step == 0 || datum.rn == length(data('time_buckets'))" },
{ "type": "formula", "as": "time_label", "expr": "timeFormat(datum.ts, '%H:%M')" }
]
}
],
"scales": [
{
"name": "xscale", "type": "time",
"domain": { "data": "time_buckets", "field": "ts" },
"range": [0, { "signal": "chartW" }]
},
{
"name": "yscale", "type": "linear",
"domain": [0, { "signal": "yMax" }],
"range": [{ "signal": "chartH" }, 0],
"nice": false, "zero": true
}
],
"marks": [
// 1. KHUNG NỀN PANEL
{
"type": "rect",
"encode": {
"enter": {
"x": { "value": 8 }, "y": { "value": 8 },
"width": { "signal": "width - 16" }, "height": { "signal": "height - 16" },
"cornerRadius": { "value": 14 },
"fill": { "value": "#061525" }, "fillOpacity": { "value": 0.94 },
"stroke": { "value": "#18b4ff" }, "strokeWidth": { "value": 1.5 }, "strokeOpacity": { "value": 0.5 }
}
}
},

// 2. TIÊU ĐỀ BIỂU ĐỒ
{
"type": "text",
"encode": {
"enter": {
"x": { "value": 30 }, "y": { "value": 58 },
"text": { "value": "TÊN BIỂU ĐỒ TIMESERIES" },
"fontSize": { "value": 28 }, "fontWeight": { "value": "bold" }, "fill": { "value": "#f3f7ff" }
}
}
},
{
"type": "text",
"encode": {
"enter": {
"x": { "value": 30 }, "y": { "value": 113 },
"text": { "value": "(Đơn vị: % hoặc IOPS...)" },
"fontSize": { "value": 21 }, "fontWeight": { "value": "bold" }, "fill": { "value": "#8ea0b5" }
}
}
},

// 3. ĐƯỜNG LƯỚI TRỤC Y VÀ NHÃN (GRID & Y-AXIS LABELS)
{
"type": "rule", "from": { "data": "y_ticks" },
"encode": {
"enter": {
"x": { "signal": "chartX" }, "x2": { "signal": "chartX + chartW" },
"y": { "signal": "chartY + scale('yscale', datum.value)" },
"stroke": { "value": "#ffffff" }, "strokeOpacity": { "value": 0.14 },
"strokeWidth": { "value": 1 }, "strokeDash": { "value": [3, 4] }
}
}
},
{
"type": "text", "from": { "data": "y_ticks" },
"encode": {
"enter": {
"x": { "signal": "chartX - 18" },
"y": { "signal": "chartY + scale('yscale', datum.value)" },
"text": { "field": "label" },
"align": { "value": "right" }, "baseline": { "value": "middle" },
"fontSize": { "value": 16 }, "fontWeight": { "value": "bold" }, "fill": { "value": "#f3f7ff" }
}
}
},

// 4. TRỤC X VÀ NHÃN THỜI GIAN
{
"type": "rule",
"encode": {
"enter": {
"x": { "signal": "chartX" }, "x2": { "signal": "chartX + chartW" },
"y": { "signal": "chartY + chartH" },
"stroke": { "value": "#c8d3df" }, "strokeOpacity": { "value": 0.75 }, "strokeWidth": { "value": 1.3 }
}
}
},
{
"type": "text", "from": { "data": "x_labels" },
"encode": {
"enter": {
"x": { "scale": "xscale", "field": "ts", "offset": { "signal": "chartX" } },
"y": { "signal": "chartY + chartH + 25" },
"text": { "field": "time_label" },
"align": { "value": "center" }, "baseline": { "value": "middle" },
"fontSize": { "value": 14 }, "fontWeight": { "value": "bold" }, "fill": { "value": "#d8e6f5" }
}
}
},

// 5. ĐƯỜNG DỮ LIỆU (LINE MARKS)
{
"type": "line", "from": { "data": "line1_data" },
"encode": {
"enter": {
"x": { "scale": "xscale", "field": "ts", "offset": { "signal": "chartX" } },
"y": { "scale": "yscale", "field": "val1", "offset": { "signal": "chartY" } },
"stroke": { "value": "#59e03f" }, "strokeWidth": { "value": 2 }, "strokeOpacity": { "value": 0.95 }
}
}
},
{
"type": "line", "from": { "data": "line2_data" },
"encode": {
"enter": {
"x": { "scale": "xscale", "field": "ts", "offset": { "signal": "chartX" } },
"y": { "scale": "yscale", "field": "val2", "offset": { "signal": "chartY" } },
"stroke": { "value": "#29a8ff" }, "strokeWidth": { "value": 2 }, "strokeOpacity": { "value": 0.95 }
}
}
},

// 6. BẢNG KPI LỀ PHẢI (SIDE PANEL)
{
"type": "text",
"encode": {
"enter": {
"x": { "signal": "width - 120" }, "y": { "value": 125 },
"text": { "value": "Latest Value" },
"fontSize": { "value": 20 }, "fontWeight": { "value": "bold" }, "fill": { "value": "#8ea0b5" }
}
}
},
{
"type": "text",
"encode": {
"enter": {
"x": { "signal": "width - 120" }, "y": { "value": 185 },
"fontSize": { "value": 28 }, "fontWeight": { "value": "bold" }, "fill": { "value": "#59e03f" }
},
"update": { "text": { "signal": "format(lastMetric1, '.0f')" } }
}
},
{
"type": "text",
"encode": {
"enter": {
"x": { "signal": "width - 120" }, "y": { "value": 245 },
"fontSize": { "value": 28 }, "fontWeight": { "value": "bold" }, "fill": { "value": "#29a8ff" }
},
"update": { "text": { "signal": "format(lastMetric2, '.0f')" } }
}
},

// 7. CHÚ GIẢI (LEGEND) Ở DƯỚI CÙNG
{
"type": "rule",
"encode": {
"enter": {
"x": { "value": 42 }, "x2": { "value": 88 },
"y": { "signal": "height - 65" },
"stroke": { "value": "#59e03f" }, "strokeWidth": { "value": 3 }
}
}
},
{
"type": "text",
"encode": {
"enter": {
"x": { "value": 105 }, "y": { "signal": "height - 58" },
"text": { "value": "Metric 1 Name" },
"fontSize": { "value": 18 }, "fontWeight": { "value": "bold" }, "fill": { "value": "#f3f7ff" }
}
}
},
{
"type": "rule",
"encode": {
"enter": {
"x": { "value": 280 }, "x2": { "value": 326 },
"y": { "signal": "height - 65" },
"stroke": { "value": "#29a8ff" }, "strokeWidth": { "value": 3 }
}
}
},
{
"type": "text",
"encode": {
"enter": {
"x": { "value": 343 }, "y": { "signal": "height - 58" },
"text": { "value": "Metric 2 Name" },
"fontSize": { "value": 18 }, "fontWeight": { "value": "bold" }, "fill": { "value": "#f3f7ff" }
}
}
}
]
}
1781705759126.png
 
Sửa lần cuối:
Back
Top