SIEM/Log Management Giám sát máy chủ Linux bằng Node Exporter, Prometheus và OpenSearch

I. Thu thập chỉ số thiết bị Linux​

1. Node Exporter​

1.1. Giới thiệu​

Là bộ phận được cài trên mục tiêu thu thập các thông tin chỉ số phần cứng và hệ điều hành từ các hệ thống Linux, những thông tin này có thể là mức sử dụng CPU, RAM, đĩa và chỉ số mạng. [1]

1.2. Cấu hình​

Sau khi tải phiên bản mới nhất của Prometheus Node Exporter [2], chạy tệp với cấu hình xuất dữ liệu tại localhost ở cổng 9100. [3]
Sass:
./node_exporter --web.listen-address="127.0.0.1:9100" &

Kiểm tra bằng cách truy cập tại đường dẫn sau
Sass:
curl http://localhost:9100/metrics

Hình hiển thị kết quả xuất dữ liệu của Node Exporter

Hình 1. Kiểm tra trạng thái của Node​

2. Prometheus​

Prometheus là nơi lưu trữ các chỉ số này và thực hiện truy vấn, bằng cách:
- Cấu hình thêm đoạn lệnh sau trong /etc/prometheus/prometheus.yml, trong phần scrape_configs
- Cấu hình remote write để đẩy chỉ số đến cổng 8500 với tần suất lên đến 5000 sample mỗi lần gửi, 10 luồng và tối đa 10 shard trong bộ nhớ.

YAML:
scrape_configs:
  - job_name: "linux-node"
    static_configs:
    - targets: ['localhost:9100']
remote_write:
  - url: "http://localhost:8500/write"
    queue_config:
      max_samples_per_send: 5000
      max_shards: 10
      capacity: 10000

Sau đó có thể truy vấn sử dụng promql để truy vấn
Truy vấn thử nghiệm Promql

Hình 2. Truy vấn thử Promql​

II. Vận chuyển và trực quan hóa chỉ số thiết bị Linux​

1. Bộ vận chuyển Vector​

Là công cụ nhẹ, tốc độ cao có chức năng thu thập, chuẩn hóa và vận chuyển log và chỉ số.
Trong ngữ cảnh này, Vector sẽ là thành phần vận chuyển các chỉ số thô đến OpenSearch và cấu hình trên tệp /etc/vector/vector.yaml.
Phần source sẽ thu thập dữ liệu từ endpoint được cấu hình của Prometheus đã nêu trên.

YAML:
sources:
 prom_receiver:
    type: "prometheus_remote_write"
    address: "0.0.0.0:8500"
    path: "/write"

Đối với phần transform, trong trường hợp Prometheus nhận dữ liệu từ nhiều nguồn khác nhau mà Vector nhận cùng một nguồn, bằng các phân chia luồng dựa trên trường "name". [4]
YAML:
transforms:
  route_by_source_prometheus:
    type: "route"
    inputs:
      - prom_receiver
    route:
      linux_metrics: 'starts_with(string!(.name), "node_")'
      vcenter_metrics: 'starts_with(string!(.name), "vsphere_")'

Phần sinks sẽ chuyển chỉ số về OpenSearch với tên index là "linux_node_exporter-metrics-"
YAML:
sinks:
  opensearch_linux_node_metric:
    type: "elasticsearch"
    inputs:
      - route_by_source_prometheus.linux_metrics
    endpoints:
      - "https://127.0.0.1:9200"
    tls:
      verify_certificate: false
    auth:
      strategy: "basic"
      user: "admin"
      password: "Th3adm1n@"
    api_version: "v7"
    bulk:
      index: "linux_node_exporter-metrics-%Y.%m.%d"

2. Thành phần trực quan OpenSearch ​

2.1. Định nghĩa ​

Là một công cụ tìm kiếm và phân tích phân tán, hỗ trợ nhiều trường hợp sử dụng khác nhau, từ việc triển khai tìm kiếm trên trang web cho đến phân tích dữ liệu bảo mật nhằm phát hiện các mối đe dọa.

2.2. Tạo index chỉ số​

Để kiểm tra và khám phá dữ liệu đã đến OpenSearch, cần tạo một index pattern.
Kết quả sau khi tạo trong Discover

Tab Discover cho chỉ số Linux

Hình 3. Kết quả chỉ số Linux trên Discover ​

2.3. Tạo quy tắc tính toán​

Trước khi tạo một bộ quy tắc để tính toán trước khi trực quan, cần lựa chọn các phương pháp tạo quy tắc.
Tiêu chíVector.devIngest PipelinesIndex Transforms
Thời điểm xử lýKhi nhận được dữ liệu từ PrometheusKhi nhận được dữ liệu từ VectorChạy định kỳ trên những dữ liệu đã index trong OpenSearch
Xử lý dữ liệuChỉ xử lý stream dữ liệu thời gian thựcChỉ xử lý stream dữ liệu thời gian thựcQuét và tổng hợp trực tiếp trên các index hiện có
Phép toán nhiều bướcKhá phức tạpKhá phức tạpĐơn giản
Tác động hiệu suấtTốn tài nguyên ítGây chậm tốc độ ingestTốn thêm dung lượng bộ nhớ lưu trữ
Trực quan hóaPhải cấu hình biểu đồ dựa trên cấu trúc JSON mới.Phải cấu hình biểu đồ dựa trên cấu trúc JSON mới.Tạo ra một index tinh gọn, biểu đồ load nhanh
Kết quả trên cho thấy sử dụng OpenSearch Index Transforms, tính năng này sẽ tính toán theo chu kì trên những chỉ số của Linux. [5]

T
ạo một bộ rule tính toán, kết quả sẽ ghi vào một index mới "summary-linux-metric" với các thông tin chỉ số sau:
- CPU_Load_1m: sử dụng "node_load1" đo lường số lượng khối lượng công việc tính toán mà bộ xử lý đảm nhận, đơn vị %.
- CPU_Usage_Avg: sử dụng "node_cpu_seconds_total" đo lường mức sử dụng CPU thông qua số "idle" CPU, đơn vị %.
- Memory_Available_Bytes: sử dụng "node_memory_MemAvailable_bytes" để tính số RAM đang sẵn sàng trong máy chủ, đơn vị Byte.
- Memory_Usage_Avg: sử dụng "node_memory_MemTotal_bytes" chứa tổng số bộ nhớ RAM với giá trị RAM đang sẵn sàng để tính toán mức RAM sử dụng, đơn vị %.
- Disk_Usage_Avg: sử dụng "node_filesystem_size_bytes" chứa tổng số dung lượng bộ nhớ và "node_filesystem_avail_bytes" chứa dung lượng còn tại để tính toán mức dung lượng đĩa đã sử dụng, đơn vị %.
- Disk_Write_Bytes_Per_Sec và Disk_Read_Bytes_Per_Sec: sử dụng "node_disk_write_bytes_total" và "node_disk_read_bytes_total" để đo lường tổng số byte ghi và đọc trên đĩa, chuyển sang đơn vị GB.
- Disk_Read_Latency_Ms: sử dụng "node_disk_read_time_seconds_total" chứa tổng số thời gian mà tất cả các thao tác đọc đã tiêu tốn và "node_disk_reads_completed_total" chứa tổng số thao tác đọc đĩa thành công để tính toán độ trễ đọc, đơn vị ms.
- Disk_Write_Latency_Ms: Tương tự như Read như với "node_disk_write_time_seconds_total" và "node_disk_writes_completed_total" để tính toán độ trễ ghi, đơn vị ms.
- Network_In_Bytes_Per_Sec và Network_Out_Bytes_Per_Sec: "node_network_receive_bytes_total" và "node_network_transmit_bytes_total" lần lượt chứa tổng số Byte sử dụng cho thao tác nhận và truyền tải dữ liệu dưới đơn vị Byte/s. [6]


Sass:
PUT _plugins/_transform/linux_master_transform_rule
{
  "transform": {
    "enabled": true,
    "continuous": true,
    "description": "Linux metric master transform rule",
    "source_index": "linux_node_exporter-metrics-*",
    "target_index": "summary-linux-metric",
    "schedule": {
      "interval": {
        "period": 5,
        "unit": "Minutes"
      }
    },
    "page_size": 1000,
    "groups": [
      {
        "terms": {
          "source_field": "tags.instance.keyword",
          "target_field": "Host"
        }
      }
    ],
    "aggregations": {
      "@timestamp": {
        "scripted_metric": {
          "init_script": "state.max_time = 0L",
          "map_script": "if (doc['timestamp'].size() > 0) { long t = doc['timestamp'].value.toInstant().toEpochMilli(); if (t > state.max_time) { state.max_time = t; } }",
          "combine_script": "return state.max_time;",
          "reduce_script": """
            long max_t = 0L;
            for (t in states) {
              if (t > max_t) { max_t = t; }
            }
            if (max_t == 0L) return null;
            return Instant.ofEpochMilli(max_t).toString();
          """
        }
      },
      "CPU_Load_1m": {
        "scripted_metric": {
          "init_script": "state.total = 0.0; state.count = 0;",
          "map_script": """
            if (doc['name.keyword'].value == 'node_load1') {
              state.total += doc['gauge.value'].value;
              state.count++;
            }
          """,
          "combine_script": "return state;",
          "reduce_script": """
            double total = 0; long count = 0;
            for (s in states) {
              total += s.total;
              count += s.count;
            }
            return count == 0 ? 0.0 : total / count;
          """
        }
      },
      "CPU_Usage_Avg": {
        "scripted_metric": {
          "init_script": "state.total_idle = 0.0; state.total_cpu = 0.0;",
          "map_script": """
            if (doc['name.keyword'].value == 'node_cpu_seconds_total') {
              if (doc['tags.mode.keyword'].value == 'idle') {
                state.total_idle += doc['gauge.value'].value;
              }
              state.total_cpu += doc['gauge.value'].value;
            }
          """,
          "combine_script": "return state;",
          "reduce_script": """
            double total_idle = 0;
            double total_cpu = 0;
            for (s in states) {
              total_idle += s.total_idle;
              total_cpu += s.total_cpu;
            }
            if (total_cpu == 0) return 0.0;
            return ((total_cpu - total_idle) / total_cpu) * 100.0;
          """
        }
      },
      "Memory_Available_Bytes": {
        "scripted_metric": {
          "init_script": "state.total = 0.0; state.count = 0;",
          "map_script": """
            if (doc['name.keyword'].value == 'node_memory_MemAvailable_bytes') {
              state.total += doc['gauge.value'].value;
              state.count++;
            }
          """,
          "combine_script": "return state;",
          "reduce_script": """
            double total = 0; long count = 0;
            for (s in states) {
              total += s.total;
              count += s.count;
            }
            return count == 0 ? 0.0 : total / count;
          """
        }
      },
      "Memory_Usage_Avg": {
        "scripted_metric": {
          "init_script": """
            state.total = 0.0;
            state.avail = 0.0;
          """,
          "map_script": """
            if (doc['name.keyword'].value == 'node_memory_MemTotal_bytes') {
              state.total = doc['gauge.value'].value;
            } else if (doc['name.keyword'].value == 'node_memory_MemAvailable_bytes') {
              state.avail = doc['gauge.value'].value;
            }
          """,
          "combine_script": "return state;",
          "reduce_script": """
            double total = 0;
            double avail = 0;
            for (s in states) {
              if (s.total > 0) total = s.total;
              if (s.avail > 0) avail = s.avail;
            }
            if (total == 0) return 0;
            return ((total - avail) / total) * 100;
          """
        }
      },
      "Disk_Usage_Avg": {
        "scripted_metric": {
          "init_script": """
            state.total = 0.0;
            state.avail = 0.0;
          """,
          "map_script": """
            if (doc['name.keyword'].value == 'node_filesystem_size_bytes') {
              state.total = doc['gauge.value'].value;
            } else if (doc['name.keyword'].value == 'node_filesystem_avail_bytes') {
              state.avail = doc['gauge.value'].value;
            }
          """,
          "combine_script": "return state;",
          "reduce_script": """
            double total = 0;
            double avail = 0;
            for (s in states) {
              if (s.total > 0) total = s.total;
              if (s.avail > 0) avail = s.avail;
            }
            if (total == 0) return 0;
            return ((total - avail) / total) * 100;
          """
        }
      },
      "Disk_Read_Bytes_Per_Sec": {
        "scripted_metric": {
          "init_script": """
            state.min = null;
            state.max = null;
          """,
          "map_script": """
            if (doc['name.keyword'].value == 'node_disk_read_bytes_total') {
              double val = doc['gauge.value'].value;
              if (state.min == null || val < state.min) state.min = val;
              if (state.max == null || val > state.max) state.max = val;
            }
          """,
          "combine_script": "return state;",
          "reduce_script": """
            double min = -1;
            double max = -1;
            for (s in states) {
              if (s.min != null && (min == -1 || s.min < min)) min = s.min;
              if (s.max != null && (max == -1 || s.max > max)) max = s.max;
            }
            if (max == -1 || min == -1 || max == min) return 0;
            return (max - min) / 64424509440.0;
          """
        }
      },
      "Disk_Write_Bytes_Per_Sec": {
        "scripted_metric": {
          "init_script": """
            state.min = null;
            state.max = null;
          """,
          "map_script": """
            if (doc['name.keyword'].value == 'node_disk_written_bytes_total') {
              double val = doc['gauge.value'].value;
              if (state.min == null || val < state.min) state.min = val;
              if (state.max == null || val > state.max) state.max = val;
            }
          """,
          "combine_script": "return state;",
          "reduce_script": """
            double min = -1;
            double max = -1;
            for (s in states) {
              if (s.min != null && (min == -1 || s.min < min)) min = s.min;
              if (s.max != null && (max == -1 || s.max > max)) max = s.max;
            }
            if (max == -1 || min == -1 || max == min) return 0;
            return (max - min) / 64424509440.0;
          """
        }
      },
      "Disk_Read_Latency_Ms": {
        "scripted_metric": {
          "init_script": """
            state.time_min = null;
            state.time_max = null;
            state.ops_min = null;
            state.ops_max = null;
          """,
          "map_script": """
            double val = doc['gauge.value'].value;
            if (doc['name.keyword'].value == 'node_disk_read_time_seconds_total') {
              if (state.time_min == null || val < state.time_min) state.time_min = val;
              if (state.time_max == null || val > state.time_max) state.time_max = val;
            } else if (doc['name.keyword'].value == 'node_disk_reads_completed_total') {
              if (state.ops_min == null || val < state.ops_min) state.ops_min = val;
              if (state.ops_max == null || val > state.ops_max) state.ops_max = val;
            }
          """,
          "combine_script": "return state;",
          "reduce_script": """
            double time_min = -1; double time_max = -1;
            double ops_min = -1; double ops_max = -1;
            
            for (s in states) {
              if (s.time_min != null && (time_min == -1 || s.time_min < time_min)) time_min = s.time_min;
              if (s.time_max != null && (time_max == -1 || s.time_max > time_max)) time_max = s.time_max;
              if (s.ops_min != null && (ops_min == -1 || s.ops_min < ops_min)) ops_min = s.ops_min;
              if (s.ops_max != null && (s.ops_max > ops_max)) ops_max = s.ops_max;
            }
            
            double time_diff = (time_max > time_min) ? (time_max - time_min) : 0.0;
            double ops_diff = (ops_max > ops_min) ? (ops_max - ops_min) : 0.0;
            
            if (ops_diff == 0) return 0.0;
            return (time_diff / ops_diff) * 1000.0;
          """
        }
      },
      "Disk_Write_Latency_Ms": {
        "scripted_metric": {
          "init_script": """
            state.time_min = null;
            state.time_max = null;
            state.ops_min = null;
            state.ops_max = null;
          """,
          "map_script": """
            double val = doc['gauge.value'].value;
            if (doc['name.keyword'].value == 'node_disk_write_time_seconds_total') {
              if (state.time_min == null || val < state.time_min) state.time_min = val;
              if (state.time_max == null || val > state.time_max) state.time_max = val;
            } else if (doc['name.keyword'].value == 'node_disk_writes_completed_total') {
              if (state.ops_min == null || val < state.ops_min) state.ops_min = val;
              if (state.ops_max == null || val > state.ops_max) state.ops_max = val;
            }
          """,
          "combine_script": "return state;",
          "reduce_script": """
            double time_min = -1; double time_max = -1;
            double ops_min = -1; double ops_max = -1;
            
            for (s in states) {
              if (s.time_min != null && (time_min == -1 || s.time_min < time_min)) time_min = s.time_min;
              if (s.time_max != null && (time_max == -1 || s.time_max > time_max)) time_max = s.time_max;
              if (s.ops_min != null && (ops_min == -1 || s.ops_min < ops_min)) ops_min = s.ops_min;
              if (s.ops_max != null && (s.ops_max > ops_max)) ops_max = s.ops_max;
            }
            
            double time_diff = (time_max > time_min) ? (time_max - time_min) : 0.0;
            double ops_diff = (ops_max > ops_min) ? (ops_max - ops_min) : 0.0;
            
            if (ops_diff == 0) return 0.0;
            return (time_diff / ops_diff) * 1000.0;
          """
        }
      },
      "Network_In_Bytes_Per_Sec": {
        "scripted_metric": {
          "init_script": """
            state.min = null;
            state.max = null;
          """,
          "map_script": """
            if (doc['name.keyword'].value == 'node_network_receive_bytes_total') {
              double val = doc['gauge.value'].value;
              if (state.min == null || val < state.min) state.min = val;
              if (state.max == null || val > state.max) state.max = val;
            }
          """,
          "combine_script": "return state;",
          "reduce_script": """
            double min = -1;
            double max = -1;
            for (s in states) {
              if (s.min != null && (min == -1 || s.min < min)) min = s.min;
              if (s.max != null && (max == -1 || s.max > max)) max = s.max;
            }
            if (max == -1 || min == -1 || max == min) return 0;
            return (max - min) / 60.0;
          """
        }
      },
      "Network_Out_Bytes_Per_Sec": {
        "scripted_metric": {
          "init_script": """
            state.min = null;
            state.max = null;
          """,
          "map_script": """
            if (doc['name.keyword'].value == 'node_network_transmit_bytes_total') {
              double val = doc['gauge.value'].value;
              if (state.min == null || val < state.min) state.min = val;
              if (state.max == null || val > state.max) state.max = val;
            }
          """,
          "combine_script": "return state;",
          "reduce_script": """
            double min = -1;
            double max = -1;
            for (s in states) {
              if (s.min != null && (min == -1 || s.min < min)) min = s.min;
              if (s.max != null && (max == -1 || s.max > max)) max = s.max;
            }
            if (max == -1 || min == -1 || max == min) return 0;
            return (max - min) / 60.0;
          """
        }
      }
    }
  }
}

Bật rule
Sass:
POST _plugins/_transform/linux_cpu_transform_rule/_start

Sau khi chạy thành công vào Dashboard Management > Index patterns để tạo pattern mới cho index mới tạo.
Tạo index chỉ số tính toán

Hình 4. Tạo index patterns
​

Sau khi tạo pattern này người dùng có thể vào Discover để xem
Cấu trúc một tài liệu trong summary-linux-metric

Hình 5. Tạo index patterns
​

2.4. Tạo Dashboard trực quan​

Sau đó tạo ra một dashboard giám sát:
- Tài nguyên sử dụng: CPU, RAM, dung lượng đĩa dưới đơn vị %
- Độ trễ đĩa dưới đơn vị ms
- Tốc độ đọc và ghi của đĩa dưới đơn vị Byte/s
- Tốc độ đường truyền mạng dưới đơn vị GB/s

Viz hiển thị Mức sử dụng tài nguyên và độ trễ đĩa của Linux

Viz hiển thị tốc độ đọc/ghi đĩa và tốc độ nhận/truyền tải mạng của Linux

Hình 6. Dashboard giám sát chỉ số của máy chủ Linux
​

Nguồn tham khảo​

[1] Soe Wai Lin, "Monitoring system resources using with node-exporter and prometheus", Medium. Avaliable: https://medium.com/@09soewailynn/mo...ith-node-exporter-and-prometheus-20e84941d180
[2] Prometheus, "Node Exporter 1.12.1 / 2026-07-14", Github. Avaliable: https://github.com/prometheus/node_exporter/releases
[3] Prometheus, "Monitoring Linux host metrics with the Node Exporter", Prometheus Documents. Avaliable: https://prometheus.io/docs/guides/node-exporter/
[4] Vector, "Transforms Route", Vector Documents Configuration. Avaliable: https://vector.dev/docs/reference/configuration/transforms/route/
[5] OpenSearch, "Index transforms", OpenSearch documents. Avaliable: https://docs.opensearch.org/latest/im-plugin/index-transforms/index/
[6] OpenSearch, "Scripted metric aggregation", OpenSearch documents. Avaliable: https://docs.opensearch.org/latest/aggregations/metric/scripted-metric/
 
Back
Top