从 Loki 迁移
本指南介绍如何将 Loki 日志写入迁移到 GreptimeDB。 GreptimeDB 支持 Loki Push API 写入日志,因此现有的 Loki 兼容写入端通常只需少量配置变更即可将日志发送到 GreptimeDB。
GreptimeDB 的 Loki 兼容端点用于日志写入。 对于查询和仪表盘,请使用 GreptimeDB SQL、全文检索、GreptimeDB Dashboard 以及 Grafana 集成,而不是 LogQL。
迁移前须知
请先检查当前 Loki 部署,并决定日志在 GreptimeDB 中的存储方式:
- 梳理所有向 Loki 写入日志的组件,例如 Grafana Alloy、OpenTelemetry Collector、Promtail、Fluent Bit、Vector 或自定义客户端。
- 规划目标 GreptimeDB 数据库和表名。如果没有提供表名 Header,GreptimeDB 会将 Loki 日志写入
loki_logs。 - 检查 Loki stream labels。GreptimeDB 会将 labels 存储为标签列,因此应避免使用 request ID、user ID、trace ID 等高基数字段作为 label。
- 确认是否只需要保存原始日志行,还是需要使用 GreptimeDB Pipeline 将日志行解析为结构化列。
- 规划历史数据迁移。GreptimeDB 不会直接导入 Loki chunk 或 index 文件;请通过原始日志源、归档文件或导出的记录,将历史日志重放到 GreptimeDB 的日志写入 API。
对于新的采集器配置,推荐使用 Grafana Alloy。 如果仍在使用已有的 Loki 兼容客户端,可以通过修改 Loki Push URL 并添加 GreptimeDB Header,将其重定向到 GreptimeDB。
迁移步骤
配置 GreptimeDB Loki 端点
将 Loki Push 请求发送到:
http{s}://<host>:4000/v1/loki/api/v1/push
使用以下 GreptimeDB 专用 Header:
| Header | 是否必需 | 说明 |
|---|---|---|
X-Greptime-DB-Name | 否 | 目标数据库名,默认值为 public。 |
X-Greptime-Log-Table-Name | 否 | 目标日志表名,默认值为 loki_logs。 |
Authorization | 取决于部署 | 使用 Base64 编码的 <username>:<password> 进行 Basic 认证。 |
X-Greptime-Pipeline-Name 或 X-Greptime-Log-Pipeline-Name | 否 | 在写入前用于解析 Loki 条目的 Pipeline 名称。 |
GreptimeDB 接受与 Loki 相同的 Push 请求体格式:
Content-Type: application/x-protobuf:Snappy 压缩的 LokiPushRequest。Content-Type: application/json:包含顶层streams数组的 JSON 请求体。
以下 JSON 请求可用于快速检查连通性:
curl -X POST "http://localhost:4000/v1/loki/api/v1/push" \
-H "Content-Type: application/json" \
-H "X-Greptime-DB-Name: public" \
-H "X-Greptime-Log-Table-Name: loki_demo_logs" \
--data-raw '{
"streams": [
{
"stream": {
"job": "api",
"env": "prod"
},
"values": [
["1731748568804293888", "request completed", {"trace_id": "abc"}]
]
}
]
}'
双写 Loki 和 GreptimeDB
迁移期间,请同时写入 Loki 和 GreptimeDB,直到完成写入、保留策略、仪表盘和告警验证。
以下 Alloy 示例保留现有 Loki sink,并新增一个 GreptimeDB Loki 兼容 sink:
loki.source.file "app" {
targets = [
{__path__ = "/var/log/app/*.log"},
]
forward_to = [loki.process.app.receiver]
file_match {
enabled = true
}
}
loki.process "app" {
forward_to = [
loki.write.existing_loki.receiver,
loki.write.greptimedb.receiver,
]
stage.static_labels {
values = {
job = "app",
env = "prod",
}
}
}
loki.write "existing_loki" {
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_app_logs",
}
basic_auth {
username = "<greptime_user>"
password = "<greptimedb_password>"
}
}
}
如果你的采集器已经配置了 Loki 输出,迁移初期请先保持 labels 和处理阶段不变。
只修改 GreptimeDB sink 的 URL、数据库 Header、表名 Header 和认证配置。
该示例遵循 Grafana Alloy 指南中的 Loki 组件模式:loki.source.file 读取文件,loki.process 在 Loki pipeline 中保留 label 处理,loki.write.endpoint 则承载 GreptimeDB URL、自定义 Header 以及可选的 Basic 认证配置。
由于该示例在 __path__ 中使用了 glob 匹配模式,file_match 会启用 Alloy 内置的文件发现能力,将该模式展开为实际匹配的文件。如果你使用的是明确的文件路径,可以省略 file_match。
验证直接写入的数据模型
不使用 Pipeline 时,GreptimeDB 会将 Loki 条目存储在原始日志表中:
| Loki 数据 | GreptimeDB 列 |
|---|---|
| Entry timestamp | greptime_timestamp 时间索引 |
| Log line | line 字段列 |
| Structured metadata | structured_metadata JSON 字段列 |
| Stream labels | 字符串标签列 |
直接写入时,请让 GreptimeDB 在首次写入时自动创建表。 不要通过 SQL 预先创建直接写入表来指定 label 列。 Labels 是动态的,会成为自动生成表结构中的标签列。 如果需要自定义表结构,请使用 Pipeline,并根据 Pipeline 配置创建表。
使用 SQL 验证写入结果:
DESC loki_app_logs;
SELECT greptime_timestamp, line, job, env, structured_metadata
FROM loki_app_logs
ORDER BY greptime_timestamp DESC
LIMIT 10;
也可以检查该表是否被识别为 Loki 日志数据:
SELECT table_schema, table_name, signal_type, source
FROM information_schema.table_semantics
WHERE table_name = 'loki_app_logs';
也可以打开 http://<host>:4000/dashboard 访问 GreptimeDB Dashboard,并使用 Log View 查询写入的日志。
使用 Pipeline 解析 Loki 日志行
如果 Loki 日志行包含 JSON、logfmt、Nginx access logs 或其他需要展开为可查询列的结构化格式,请使用 GreptimeDB Pipeline。
如果使用 AI 编码代理来创建 Pipeline,可以为它提供 greptimedb-pipeline Skill,帮助它生成、dry-run 并迭代 Pipeline 配置。
当请求中包含 X-Greptime-Pipeline-Name 或 X-Greptime-Log-Pipeline-Name 时,GreptimeDB 会将每条 Loki 条目按以下输入字段送入 Pipeline:
| Pipeline 输入字段 | 说明 |
|---|---|
greptime_timestamp | Loki 条目的时间戳。 |
loki_line | 原始 Loki 日志行。 |
loki_label_<name> | Loki stream label 值。 |
loki_metadata_<name> | Loki structured metadata 值。 |
例如,假设 Alloy 读取以下 ZooKeeper 日志文件:
2015-08-25 11:23:58,959 - WARN [LearnerHandler-/10.10.34.11:45441:Leader@574] - Committing zxid 0xf00000000 from /10.10.34.13:2888 not first!
2015-08-25 11:23:58,960 - WARN [LearnerHandler-/10.10.34.11:45441:Leader@576] - First is 0x0
2015-08-25 11:23:58,960 - INFO [LearnerHandler-/10.10.34.11:45441:Leader@598] - Have quorum of supporters; starting up and setting last processed zxid: 0xf00000000
2015-08-25 11:26:27,891 - INFO [/10.10.34.13:3888:QuorumCnxManager$Listener@493] - Received connection request /10.10.34.12:57513
2015-08-25 11:26:27,897 - INFO [WorkerReceiver[myid=3]:FastLeaderElection@542] - Notification: 2 (n.leader), 0xd0000001b (n.zxid), 0x1 (n.round), LOOKING (n.state), 2 (n.sid), 0xd (n.peerEPoch), LEADING (my state)
2015-08-25 11:26:27,898 - INFO [WorkerReceiver[myid=3]:FastLeaderElection@542] - Notification: 3 (n.leader), 0xd0000001b (n.zxid), 0x3 (n.round), LOOKING (n.state), 2 (n.sid), 0xe (n.peerEPoch), LEADING (my state)
2015-08-25 11:26:28,138 - INFO [LearnerHandler-/10.10.34.12:38330:LearnerHandler@263] - Follower sid: 2 : info : org.apache.zookeeper.server.quorum.QuorumPeer$QuorumServer@7761c32f
2015-08-25 11:26:28,159 - INFO [LearnerHandler-/10.10.34.12:38330:LearnerHandler@318] - Synchronizing with Follower sid: 2 maxCommittedLog=0x0 minCommittedLog=0x0 peerLastZxid=0xd0000001b
2015-08-25 11:26:28,159 - INFO [LearnerHandler-/10.10.34.12:38330:LearnerHandler@395] - Sending SNAP
2015-08-25 11:26:28,159 - INFO [LearnerHandler-/10.10.34.12:38330:LearnerHandler@419] - Sending snapshot last zxid of peer is 0xd0000001b zxid of leader is 0xf00000000sent zxid of db as 0xf00000000
创建一个 Pipeline,提取 ZooKeeper 时间戳、日志级别、线程、源码行号和消息:
# zk_pipeline.yaml
version: 2
processors:
- regex:
fields:
- loki_line, zk
patterns:
- '^(?<timestamp>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3}) - (?<level>[A-Z]+)\s+\[(?<thread>.*)@(?<source_line>\d+)\] - (?<message>.*)$'
ignore_missing: true
- date:
fields:
- zk_timestamp
formats:
- "%Y-%m-%d %H:%M:%S,%3f"
timezone: "UTC"
- select:
fields:
- zk_timestamp
- zk_level
- zk_thread
- zk_source_line
- zk_message
- loki_label_job
- loki_label_env
transform:
- field: zk_timestamp
type: time
index: timestamp
- fields:
- zk_level
- loki_label_job
- loki_label_env
type: string
tag: true
- field: zk_thread
type: string
index: inverted
- field: zk_source_line
type: int32
- field: zk_message
type: string
index: fulltext
示例日志中的时间戳没有包含时区。请将 timezone 设置为日志生产端使用的时区。
上传 Pipeline:
curl -X POST "http://localhost:4000/v1/pipelines/zk_logs" \
-F "file=@zk_pipeline.yaml"
然后在 GreptimeDB Loki sink 中添加 Pipeline Header:
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_zookeeper_logs",
"X-Greptime-Pipeline-Name" = "zk_logs",
}
}
}
Alloy 通过该 sink 发送 ZooKeeper 日志文件后,GreptimeDB 会先应用该 Pipeline,再将处理后的行写入 loki_zookeeper_logs。
通过 Pipeline 写入后,可以查询结构化列:
SELECT
zk_timestamp,
loki_label_job AS job,
loki_label_env AS env,
zk_level,
zk_thread,
zk_source_line,
zk_message
FROM loki_zookeeper_logs
WHERE zk_level = 'WARN'
ORDER BY zk_timestamp DESC
LIMIT 10;
如需在解析后的 message 字段上使用全文检索,请参阅全文检索。
迁移历史日志
完整迁移历史日志时,应将数据重放到 GreptimeDB,而不是复制 Loki 存储文件。 常见做法包括:
- 使用 Alloy、Vector、Fluent Bit 或自定义脚本,从原始日志文件或对象存储归档中重放日志。
- 将选定的 Loki 查询结果导出为按行分隔的记录,转换为 Loki Push JSON 后写入 GreptimeDB。
- 仅回填切换后必须可查询的保留窗口,并通过双写覆盖新增数据。
回填时,请保留原始纳秒时间戳,以便 GreptimeDB 保留事件发生时间。 建议按有限时间范围分批导入,便于验证和重试。
切换读写流量
在停止写入 Loki 之前,请验证:
- 每个重要 service、namespace 和 environment 的最新日志都已写入 GreptimeDB。
- 同一时间窗口内的行数或抽样记录与 Loki 匹配。
- GreptimeDB 保留策略满足日志保留要求。
- 仪表盘和告警已从 LogQL 改写为 SQL 或 GreptimeDB 日志查询。
- 常用检索列已经具备全文索引或跳数索引。
验证完成后,从采集器配置中移除 Loki sink,仅保留 GreptimeDB sink 作为日志目的端。