要实现基于 ClickStack(含 OpenTelemetry)、ClickHouse、HyperDX 整套工具链的日志采集方案,核心是通过 OpenTelemetry(ClickStack 的采集层) 标准化收集日志,写入 ClickHouse 存储,并确保数据格式兼容 HyperDX(UI 层)的可视化需求。以下是完整实现:

1. 工具链角色与依赖说明

工具角色依赖包作用
OpenTelemetry 数据采集层(ClickStack 核心) opentelemetry-sdk 等 标准化收集 ClickHouse 的 INFO 日志
ClickHouse 存储层 clickhouse-driver 存储采集的日志数据
HyperDX UI 可视化层 无需额外包(通过 API 连接 ClickHouse) 从 ClickHouse 读取数据并展示

2. 环境准备与依赖安装

2.1 安装组件

# 核心依赖:OpenTelemetry SDK及日志处理器
pip install opentelemetry-sdk opentelemetry-exporter-otlp-proto-http
# ClickHouse驱动(用于直接写入或验证数据)
pip install clickhouse-driver
# 配置文件处理
pip install configparser
# 日志文件监控(实时读取新增日志)
pip install watchdog

2.2 CK新建监控表

-- 在ClickHouse中手动执行此SQL(需提前创建数据库hyperdx_logs)
CREATE TABLE IF NOT EXISTS hyperdx_logs.clickhouse_info_logs (
    timestamp DateTime64(3) CODEC(Delta, ZSTD),  -- 毫秒级时间戳
    severity_text String CODEC(ZSTD),           -- 日志级别(INFO/ERROR等)
    body String CODEC(ZSTD),                    -- 日志内容
    service_name String CODEC(ZSTD),            -- 服务名(HyperDX用于筛选)
    thread String CODEC(ZSTD),                  -- 线程信息
    attributes Map(String, String) CODEC(ZSTD)  -- 扩展属性(兼容OTLP格式)
) ENGINE = MergeTree()
ORDER BY (timestamp, service_name)  -- 按时间和服务名排序,优化HyperDX查询

3. 配置文件(config.ini

[log_source]
# ClickHouse的日志文件路径(需替换为实际路径)
log_file_path = /var/log/clickhouse-server/clickhouse-server.log
# 目标日志级别(仅收集INFO)
target_level = INFO

[opentelemetry]
# OpenTelemetry服务地址(若使用HyperDX托管的OTel Collector,填HyperDX的Endpoint)
# 本地测试可省略,直接通过Exporter写入ClickHouse
otlp_endpoint = http://localhost:4318  # HyperDX默认OTLP端点

[clickhouse]
# ClickHouse连接信息(HyperDX会从这里读取数据)
host = localhost
port = 9000
user = default
password = 
database = hyperdx_logs  # 需提前创建的数据库
table = clickhouse_info_logs  # 存储日志的表

[hyperdx]
# HyperDX的数据源标识(确保与HyperDX配置一致)
service_name = clickhouse-monitor
service_version = 1.0

4. Python 脚本实现(clickhouse_log_collector.py

import os
import re
import time
import configparser
from datetime import datetime
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
from opentelemetry import trace, logs
from opentelemetry.sdk.logs import LoggerProvider, LogRecordProcessor
from opentelemetry.sdk.logs.export import BatchLogRecordProcessor
from opentelemetry.sdk.resources import Resource
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
from clickhouse_driver import Client  # 仅用于验证连接,不包含建表逻辑


class ClickHouseLogCollector:
    def __init__(self, config_path):
        # 加载配置
        self.config = self._load_config(config_path)
        # 初始化OpenTelemetry资源
        self.resource = Resource(attributes={
            "service.name": self.config['hyperdx']['service_name'],
            "service.version": self.config['hyperdx']['service_version'],
            "log.source": "clickhouse-server"
        })
        # 初始化OpenTelemetry日志处理器
        self.logger_provider = self._init_otlp_logger()
        self.logger = self.logger_provider.get_logger(__name__)
        # 初始化ClickHouse客户端(仅验证连接,不涉及建表)
        self.ch_client = self._init_clickhouse_client()
        # 日志解析正则
        self.log_pattern = re.compile(
            r'(?P<timestamp>\d{4}\.\d{2}\.\d{2} \d{2}:\d{2}:\d{2}\.\d{3}) \[(?P<thread>.*?)\] (?P<level>\w+) (?P<message>.*)'
        )

    def _load_config(self, config_path):
        """加载配置文件"""
        if not os.path.exists(config_path):
            raise FileNotFoundError(f"配置文件 {config_path} 不存在")
        config = configparser.ConfigParser()
        config.read(config_path, encoding='utf-8')
        return config

    def _init_otlp_logger(self):
        """初始化OpenTelemetry日志器"""
        otlp_exporter = OTLPLogExporter(
            endpoint=self.config['opentelemetry']['otlp_endpoint'],
            headers={"x-hyperdx-api-key": "YOUR_HYPERDX_API_KEY"}  # 若使用HyperDX云服务,需填写APIKey
        )
        logger_provider = LoggerProvider(resource=self.resource)
        logger_provider.add_log_record_processor(
            BatchLogRecordProcessor(otlp_exporter)
        )
        logs.set_logger_provider(logger_provider)
        return logger_provider

    def _init_clickhouse_client(self):
        """初始化ClickHouse客户端(仅验证连接)"""
        client = Client(
            host=self.config['clickhouse']['host'],
            port=int(self.config['clickhouse']['port']),
            user=self.config['clickhouse']['user'],
            password=self.config['clickhouse']['password'],
            database=self.config['clickhouse']['database']
        )
        # 验证连接
        try:
            client.execute("SELECT 1")
            print(f"成功连接到ClickHouse: {self.config['clickhouse']['host']}:{self.config['clickhouse']['port']}")
            print(f"请确保已手动创建表: {self.config['clickhouse']['database']}.{self.config['clickhouse']['table']}")
        except Exception as e:
            raise ConnectionError(f"ClickHouse连接失败: {str(e)}")
        return client

    def _parse_log_line(self, line):
        """解析ClickHouse日志行"""
        match = self.log_pattern.match(line.strip())
        if not match:
            return None
        level = match.group('level').upper()
        if level != self.config['log_source']['target_level']:
            return None
        timestamp_str = match.group('timestamp').replace('.', '-', 3).replace('.', ':')
        try:
            timestamp = datetime.strptime(timestamp_str, "%Y-%m-%d %H:%M:%S:%f")
        except Exception:
            return None
        return {
            "timestamp": timestamp,
            "severity_text": level,
            "body": match.group('message'),
            "thread": match.group('thread'),
            "service_name": self.config['hyperdx']['service_name']
        }

    def start_monitoring(self):
        """监控日志文件,实时采集并通过OTLP发送"""
        class LogHandler(FileSystemEventHandler):
            def __init__(self, collector):
                self.collector = collector
                self.log_file = open(collector.config['log_source']['log_file_path'], 'r', encoding='utf-8')
                self.log_file.seek(0, os.SEEK_END)  # 定位到文件末尾,只处理新增日志

            def on_modified(self, event):
                """文件更新时触发"""
                if event.src_path == self.collector.config['log_source']['log_file_path']:
                    for line in self.log_file.readlines():
                        parsed = self.collector._parse_log_line(line)
                        if parsed:
                            # 通过OpenTelemetry记录日志
                            self.collector.logger.log(
                                severity=logs.Severity.INFO,
                                message=parsed['body'],
                                timestamp=parsed['timestamp'],
                                attributes={
                                    "thread": parsed['thread'],
                                    "service_name": parsed['service_name']
                                }
                            )

            def close(self):
                self.log_file.close()

        # 启动监控
        log_path = self.config['log_source']['log_file_path']
        print(f"开始监控ClickHouse日志:{log_path}(仅收集{self.config['log_source']['target_level']}级别)")
        event_handler = LogHandler(self)
        observer = Observer()
        observer.schedule(event_handler, path=os.path.dirname(log_path), recursive=False)
        observer.start()

        try:
            while True:
                time.sleep(5)
        except KeyboardInterrupt:
            observer.stop()
            event_handler.close()
            self.logger_provider.shutdown()  # 关闭OTLP处理器,确保数据flush
            self.ch_client.disconnect()
            print("程序已停止")
        observer.join()


if __name__ == "__main__":
    print("=== 请先手动执行以下SQL创建ClickHouse表 ===")
    print("""
CREATE TABLE IF NOT EXISTS hyperdx_logs.clickhouse_info_logs (
    timestamp DateTime64(3) CODEC(Delta, ZSTD),
    severity_text String CODEC(ZSTD),
    body String CODEC(ZSTD),
    service_name String CODEC(ZSTD),
    thread String CODEC(ZSTD),
    attributes Map(String, String) CODEC(ZSTD)
) ENGINE = MergeTree()
ORDER BY (timestamp, service_name)
    """)
    print("===========================================\n")
    
    input("按Enter继续(确保已创建表)...")
    
    # 加载配置并启动采集
    collector = ClickHouseLogCollector("config.ini")
    collector.start_monitoring()

5. HyperDX 下载与安装(Docker 方式,推荐)

HyperDX 官方推荐使用 Docker Compose 快速部署,适合本地测试或生产环境。

5.1. 环境准备

  • 安装 Docker 和 Docker Compose(确保 Docker 版本 ≥ 20.10,Compose 版本 ≥ 2.10):
    bash
    # 安装 Docker(Ubuntu 示例)
    sudo apt-get update && sudo apt-get install docker.io docker-compose-plugin
    sudo systemctl start docker && sudo systemctl enable docker

5.2. 下载 HyperDX 配置

# 创建工作目录
mkdir -p /opt/hyperdx && cd /opt/hyperdx

# 下载官方 Docker Compose 配置(国内可使用 Gitee 镜像)
curl -O https://raw.githubusercontent.com/hyperdxio/hyperdx/main/docker-compose.yml
# 国内镜像(可选):curl -O https://gitee.com/hyperdx/hyperdx/raw/main/docker-compose.yml

5.3. 启动 HyperDX

# 启动服务(首次启动会下载镜像,耗时较长)
sudo docker compose up -d

# 检查状态(确保所有容器正常运行)
sudo docker compose ps
 
  • 成功启动后,HyperDX 前端地址:http://服务器IP:8080
  • 默认账号密码:admin@hyperdx.io / hyperdx

5.4. 配置 ClickHouse 数据源(HyperDX 连接 ClickHouse)

HyperDX 需要连接 ClickHouse 读取日志数据,步骤如下:

1. 登录 HyperDX 控制台

  • 访问 http://服务器IP:8080,输入默认账号密码登录。
  • 首次登录会提示修改密码,按提示操作即可。

2. 添加 ClickHouse 数据源

  1. 进入数据源配置页面:
    点击左侧菜单 Settings → Data Sources → Add Data Source → 选择 ClickHouse。
  2. 填写 ClickHouse 连接信息(与 config.ini 保持一致):
    配置项示例值说明
    Name clickhouse-logs 数据源名称(自定义)
    Host localhost(或 ClickHouse 服务器 IP) 若 HyperDX 与 ClickHouse 同机,填 localhost
    Port 8123 ClickHouse HTTP 端口(默认 8123)
    Database hyperdx_logs 前面创建的数据库
    Username default ClickHouse 用户名
    Password (留空,若未设置密码) ClickHouse 密码
    TLS/SSL Disabled 本地测试关闭 TLS
  3. 测试连接:
    点击 Test Connection,显示 Connection Successful 表示连接成功。
  4. 保存数据源:点击 Save。

3. 配置日志查询规则(关联 ClickHouse 表)

  1. 进入日志配置页面:
    点击左侧菜单 Logs → Settings → Log Sources → Add Log Source。
  2. 配置日志表信息:
    • Data Source:选择前面创建的 clickhouse-logs
    • Table:选择 clickhouse_info_logs(前面创建的表)
    • Timestamp Column:选择 timestamp(日志时间字段)
    • Message Column:选择 body(日志内容字段)
    • Severity Column:选择 severity_text(日志级别字段)
  3. 保存配置:点击 Save。

5.5 验证 HyperDX 日志展示

  1. 确保前面的 Python 采集脚本已启动,且 ClickHouse 有新的 INFO 日志产生。
  2. 在 HyperDX 控制台点击左侧菜单 Logs,即可看到:
    • 实时刷新的 ClickHouse INFO 日志列表。
    • 支持按时间范围、日志级别、服务名(service_name:clickhouse-monitor)筛选。
    • 点击单条日志可查看详情(包括线程信息、原始消息等)。

5.6 HyperDX 常见问题解决

    1. HyperDX 连接 ClickHouse 失败:
      • 检查 ClickHouse 服务是否正常运行:sudo systemctl status clickhouse-server
      • 确认 ClickHouse HTTP 端口(8123)是否开放:telnet 服务器IP 8123
      • 查看 HyperDX 容器日志排查:sudo docker compose logs -f hyperdx-api
    2. 日志不显示:
      • 检查 Python 脚本是否正常采集日志(查看脚本输出)。
      • 确认 ClickHouse 表 clickhouse_info_logs 中有数据:SELECT * FROM hyperdx_logs.clickhouse_info_logs LIMIT 10
      • 检查 HyperDX 日志源配置的字段映射是否正确(timestampbody 等)。

6. 关键实现说明

6.1. 工具链集成逻辑

  • OpenTelemetry:作为 ClickStack 的采集层,通过OTLPLogExporter将日志转换为标准化的 OTLP 格式(HyperDX 和 ClickHouse 均支持)。
  • ClickHouse:存储日志的表结构包含service_nametimestamp等 HyperDX 必需的字段,确保 HyperDX 能直接查询。
  • HyperDX:无需在脚本中直接集成,只需确保 OpenTelemetry 发送的日志格式正确,HyperDX 会通过 OTLP 或直接连接 ClickHouse 读取数据并可视化。

6.2. 日志流程

ClickHouse日志文件 → watchdog监控新增行 → OpenTelemetry解析为OTLP格式 → 
OTLP Exporter → 写入ClickHouse → HyperDX查询ClickHouse并展示

7. 使用与验证步骤

  1. 配置 HyperDX:
    • 若使用 HyperDX 云服务,在其控制台添加 ClickHouse 数据源,填写 ClickHouse 连接信息。
    • 本地部署 HyperDX 时,确保其配置指向 ClickHouse 的hyperdx_logs数据库。
  2. 启动脚本:
    bash
    # 确保ClickHouse日志文件路径正确
    python3 clickhouse_log_collector.py
    
     
  3. 验证数据:
    • ClickHouse 验证:查询表确认数据写入:
      SELECT timestamp, severity_text, body FROM hyperdx_logs.clickhouse_info_logs LIMIT 10;
    • HyperDX 验证:在 HyperDX 界面的 “Logs” 页面,筛选service_name:clickhouse-monitor,可看到 INFO 级日志的可视化展示(包含时间线、详情等)。

8. 注意事项

  • HyperDX 配置:需确保service_name与 HyperDX 中的服务名一致,否则无法筛选数据。
  • OTLP 端点:若使用本地 ClickStack 的 OTel Collector,otlp_endpoint需指向 Collector 地址(通常为http://localhost:4318)。
  • 性能优化:BatchLogRecordProcessor会批量处理日志,减少 IO,适合高并发场景。

通过这套方案,实现了 “采集 - 存储 - 可视化” 全链路基于官方工具的集成,而非自定义实现,完全符合 ClickStack+ClickHouse+HyperDX 的技术栈要求。
 
 

 

 posted on 2025-07-29 09:53  xibuhaohao  阅读(105)  评论(0)    收藏  举报