Skip to content

从 Loki 到 GreptimeDB:双写迁移实战

实战教程:配置 Grafana Alloy 把日志同时写入 Loki 和 GreptimeDB,用 Pipeline 把 syslog 消息解析成结构化字段,并在内置 Dashboard 中查询。
从 Loki 到 GreptimeDB:双写迁移实战
本页内容

GreptimeDB 为可观测性数据的长期存储和查询而设计,日志是其中的核心场景。关于与 Grafana Loki 的详细性能对比,可参考《超越 Loki!GreptimeDB 日志场景性能报告》;这条迁移路线在生产环境的大规模实践,可参考《从 Loki 到 GreptimeDB:OceanBase Cloud 300TB 多云日志实践》

GreptimeDB 提供 Loki 兼容的写入端点,现有的 Loki 写入端只需少量配置改动,就能把新日志同时发送到 GreptimeDB。本文将完成以下四步:

  1. 运行一个 Docker Compose 场景,向 Loki 发送模拟的 syslog 消息;
  2. 配置 Grafana Alloy,把每条日志同时写入 Loki 和 GreptimeDB;
  3. 添加一个 GreptimeDB Pipeline,把每条消息解析成结构化字段;
  4. 在 GreptimeDB 内置的 Dashboard 中查询结构化日志。

本文只覆盖双写和新日志写入链路的切换,不会复制已经存储在 Loki 中的数据。如需迁移历史日志,请从原始数据源、归档或导出记录中回放。完整的迁移方案参见官方迁移指南

前置条件

需要安装 Docker 和 Docker Compose,并确保主机上的 514300031004000123455189351898 端口可用。

1. 启动 Loki 场景

我们从 Grafana Alloy syslog 场景开始:

bash
git clone https://github.com/grafana/alloy-scenarios.git
cd alloy-scenarios/syslog
docker compose up -d

所有服务启动后,打开 Grafana Explore(http://localhost:3000/explore),选择 Loki 数据源,执行以下 LogQL 查询:

text
{component="loki.source.syslog"}

模拟器每 3~8 秒发送一条新消息,很快就能看到日志进来。

要停止该场景,执行:

bash
docker compose down

2. 把日志镜像写入 GreptimeDB

首先,在 docker-compose.yml 中添加一个 GreptimeDB standalone 服务:

yaml
greptimedb:
  image: greptime/greptimedb:${GREPTIMEDB_VERSION:-v1.1.2}
  ports:
    - "4000:4000"
  command:
    - standalone
    - start
    - --http-addr
    - 0.0.0.0:4000

该场景默认使用 GreptimeDB v1.1.2,可通过 GREPTIMEDB_VERSION 环境变量覆盖。端口 4000 暴露 HTTP API 和内置 Dashboard。

接下来,修改 config.alloy,让 syslog 数据源把每条日志转发到两个 loki.write 接收器:

text
livedebugging {
  enabled = true
}

loki.source.syslog "local" {
  listener {
    address = "0.0.0.0:51893"
    labels  = { component = "loki.source.syslog", protocol = "tcp" }
  }

  listener {
    address  = "0.0.0.0:51898"
    protocol = "udp"
    labels   = { component = "loki.source.syslog", protocol = "udp" }
  }

  forward_to = [
    loki.write.local.receiver,
    loki.write.greptimedb.receiver,
  ]
}

loki.write "local" {
  endpoint {
    url = "http://loki:3100/loki/api/v1/push"
  }
}

loki.write "greptimedb" {
  endpoint {
    url = "http://greptimedb:4000/v1/loki/api/v1/push"
    headers = {
      "X-Greptime-DB-Name"        = "public",
      "X-Greptime-Log-Table-Name" = "loki_syslog_raw",
      "X-Greptime-Pipeline-Name"  = "greptime_identity",
    }
  }
}

原有的 local 接收器继续写入 Loki,新增的 greptimedb 接收器把同样的日志发送到 GreptimeDB 的 Loki 兼容端点。

这几个 header 分别指定了 public 数据库、loki_syslog_raw 表和内置的 greptime_identity Pipeline。对于 Loki 写入请求,GreptimeDB 传给 Pipeline 的输入对象包含:原始时间戳 greptime_timestamp、原始日志行 loki_line,以及每个 stream 标签对应的 loki_label_<name>。这个内置 Pipeline 会根据这些字段自动推断表结构并直接写入,不解析原始日志行。

启动更新后的服务:

bash
docker compose up -d

打开 http://localhost:4000/dashboard,选择 Logs Query,再选择 public 数据库和 loki_syslog_raw 表。此时日志已同时存在于 Loki 和 GreptimeDB 中,但消息体仍以非结构化的 loki_line 形式存储。下一步,我们让日志级别等字段可以直接检索。

在 Logs Query 中查看 loki_syslog_raw 表

在 Logs Query 中查看 loki_syslog_raw 表

3. 用 Pipeline 解析 syslog 消息

模拟器发出的消息格式如下:

text
CRITICAL: Configuration loaded

创建 syslog_pipeline.yaml,写入以下 Pipeline 定义:

yaml
version: 2
processors:
  - regex:
      fields:
        - loki_line, log
      patterns:
        - '^\s*(?<level>[A-Z]+):\s*(?<message>.*)$'
      ignore_missing: true

transform:
  - field: greptime_timestamp
    type: time
    index: timestamp
  - field: log_level
    type: string
    index: inverted
    tag: true
  - field: log_message
    type: string
    index: fulltext
  - fields:
      - loki_label_component
      - loki_label_protocol
    type: string
    index: inverted
    tag: true

这个 Pipeline 做了四件事:

  • version: 2 表示先执行显式声明的 transform,再把 Pipeline 上下文中剩余的字段一并写入表中,因此原始的 loki_line 依然保留;
  • regex 处理器读取 loki_line,以 log 作为输出前缀,两个命名捕获组分别生成 log_levellog_message
  • greptime_timestamp 保留 Loki 日志条目的原始时间戳,作为表必需的时间索引;
  • 对用于等值过滤的字段添加倒排索引,并为 log_message 添加全文索引以支持文本检索。

关键列如下:

列名来源存储与索引角色
greptime_timestampLoki 日志条目时间戳时间索引
log_levellevel 正则捕获组带倒排索引的字符串 Tag
log_messagemessage 正则捕获组带全文索引的字符串字段
loki_label_componentAlloy stream 标签带倒排索引的字符串 Tag
loki_label_protocolAlloy stream 标签带倒排索引的字符串 Tag

4. 在 Alloy 启动前上传 Pipeline

添加一个一次性的 greptimedb-pipeline 服务负责上传 YAML 文件,并让 Alloy 等它成功结束后再启动。完整的 docker-compose.yml 如下:

yaml
services:
  rsyslog:
    image: rsyslog/syslog_appliance_alpine:latest@sha256:c0dd7cad9ff3234967ff59879590175b7590e8a5f5621ec49a85aff546b44a3b
    container_name: rsyslog
    ports:
      - "514:514/udp"
      - "514:514/tcp"
    volumes:
      - ./rsyslog.conf:/etc/rsyslog.conf
    depends_on:
      - alloy

  syslog-simulator:
    image: python:${PYTHON_VERSION:-3.11-slim}
    container_name: syslog-simulator
    volumes:
      - ./syslog_simulator.py:/syslog_simulator.py
    environment:
      - SYSLOG_HOST=rsyslog
      - SYSLOG_PORT=514
    depends_on:
      - rsyslog
    command: ["python3", "/syslog_simulator.py"]

  alloy:
    image: grafana/alloy:${GRAFANA_ALLOY_VERSION:-v1.17.1}
    ports:
      - "12345:12345"
      - "51893:51893"
      - "51898:51898"
    volumes:
      - ./config.alloy:/etc/alloy/config.alloy
      - ./logs:/tmp/app-logs/
    command: run --server.http.listen-addr=0.0.0.0:12345 --stability.level=experimental --storage.path=/var/lib/alloy/data /etc/alloy/config.alloy
    depends_on:
      loki:
        condition: service_started
      greptimedb-pipeline:
        condition: service_completed_successfully

  loki:
    image: grafana/loki:${GRAFANA_LOKI_VERSION:-3.7.3}
    ports:
      - "3100:3100"
    volumes:
      - ./loki-config.yaml:/etc/loki/local-config.yaml
    command: -config.file=/etc/loki/local-config.yaml

  greptimedb:
    image: greptime/greptimedb:${GREPTIMEDB_VERSION:-v1.1.2}
    ports:
      - "4000:4000"
    command:
      - standalone
      - start
      - --http-addr
      - 0.0.0.0:4000

  greptimedb-pipeline:
    image: curlimages/curl:${CURL_VERSION:-8.21.0}
    volumes:
      - ./syslog_pipeline.yaml:/syslog_pipeline.yaml
    depends_on:
      - greptimedb
    command:
      - --fail
      - --silent
      - --show-error
      - --retry
      - "60"
      - --retry-delay
      - "1"
      - --retry-all-errors
      - -X
      - POST
      - http://greptimedb:4000/v1/pipelines/syslog_logs
      - -F
      - file=@/syslog_pipeline.yaml

  grafana:
    image: grafana/grafana:${GRAFANA_VERSION:-13.1.0}
    environment:
      - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
      - GF_AUTH_ANONYMOUS_ENABLED=true
      - GF_AUTH_BASIC_ENABLED=false
    ports:
      - "3000:3000/tcp"
    entrypoint:
      - sh
      - -euc
      - |
        mkdir -p /etc/grafana/provisioning/datasources
        cat <<EOF > /etc/grafana/provisioning/datasources/ds.yaml
        apiVersion: 1
        datasources:
        - name: Loki
          type: loki
          access: proxy
          orgId: 1
          url: http://loki:3100
          basicAuth: false
          isDefault: false
          version: 1
          editable: false
        EOF
        /run.sh

这个辅助服务会在 GreptimeDB 启动期间不断重试上传;在你按下文把 Alloy 的 header 切换到自定义 Pipeline 后,Alloy 会等上传成功退出才启动,因此第一条日志就能用上自定义 Pipeline。

最后,把 config.alloy 中的表名和 Pipeline 两个 header 改为:

text
"X-Greptime-Log-Table-Name" = "loki_syslog_logs",
"X-Greptime-Pipeline-Name"  = "syslog_logs",

自定义 Pipeline 写入一张新表,这样 GreptimeDB 建表时就能应用 Tag 和索引定义。之前用于验证的原始数据仍保留在 loki_syslog_raw 表中。

再次启动:

bash
docker compose up -d

打开 http://localhost:4000/dashboard,进入 Logs Query,选择 public.loki_syslog_logs,可以看到 log_levellog_message 已经与原始 Loki 字段并列出现。

在 Logs Query 中查看 loki_syslog_logs 表的结构化列

在 Logs Query 中查看 loki_syslog_logs 表的结构化列

在查询构建器中把 log_level 过滤为 CRITICAL,或切换到 Code 模式执行以下 SQL:

sql
SELECT greptime_timestamp, log_level, log_message
FROM loki_syslog_logs
WHERE log_level = 'CRITICAL'
ORDER BY greptime_timestamp DESC
LIMIT 100;

log_level 上的等值过滤会命中倒排索引,消息体内的检索则使用 log_message 上的全文索引。

在查询构建器中把 log_level 过滤为 CRITICAL

在查询构建器中把 log_level 过滤为 CRITICAL

总结

这套迁移方案让 Loki 链路保持运行,同时用同样的实时流量验证 GreptimeDB。一个小小的自定义 Pipeline,就能把每条原始 syslog 日志变成带索引、可查询的列,而且不丢弃原始消息和 Loki 标签。

在生产环境移除 Loki 写入之前,建议对比相同时间窗口内的数据、确认数据保留要求,并迁移仍依赖 LogQL 的看板和告警。如果历史数据也需要可检索,请在最终切换前单独回放。

更多信息请参考官方文档:从 Loki 迁移Pipeline 配置GreptimeDB Dashboard

Stay in the loop

加入我们的社区