这是《Elasticsearch 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 日志检索是 Elasticsearch 另一个主流场景。它和商品搜索的关注点完全不同:商品搜索追求相关性,日志检索追求稳定接入、低成本存储、快速定位异常,以及清晰的生命周期治理。
本章从日志模型、采集管道、索引模板、查询排障到权限治理,搭建一套可落地的日志系统。
20.1 日志系统的需求
一个生产日志平台通常要回答这些问题:
| 问题 | 示例 | 依赖能力 |
|---|---|---|
| 服务是否报错 | 某服务近 15 分钟 ERROR 日志 | 时间过滤、级别过滤 |
| 请求在哪里失败 | trace_id 串联多个服务 | keyword 精确查找 |
| 影响多少用户 | 某错误影响的 UID 数 | 去重聚合 |
| 什么时候开始 | 错误按分钟分布 | date_histogram |
| 是否已恢复 | 错误率与成功率对比 | 指标日志、聚合 |
| 如何审计 | 谁查询了敏感日志 | 权限与审计 |
日志系统的常见挑战:
- 写入峰值高,故障时日志量可能放大 5 到 10 倍;
- 保存周期长,磁盘成本容易失控;
- 字段不规范,排障时会遇到同一个含义多个字段名;
- 日志包含敏感信息,需要脱敏和权限控制;
- 查询时间范围过大时,容易造成集群压力。
20.2 日志数据模型
推荐统一使用 ECS(Elastic Common Schema)风格字段。字段规范越早统一,后期排障成本越低。
PUT _index_template/app-logs
{
"index_patterns": ["app-logs-*"],
"data_stream": {},
"template": {
"settings": {
"index.default_pipeline": "app-logs-pipeline",
"index.refresh_interval": "5s",
"index.number_of_shards": 3,
"index.number_of_replicas": 1,
"index.codec": "best_compression",
"index.sort.field": ["@timestamp", "service.name"],
"index.sort.order": ["desc", "asc"]
},
"mappings": {
"dynamic": false,
"properties": {
"@timestamp": { "type": "date" },
"data_stream.type": { "type": "constant_keyword", "value": "logs" },
"data_stream.dataset": { "type": "constant_keyword", "value": "app" },
"data_stream.namespace": { "type": "keyword" },
"service.name": { "type": "keyword" },
"service.environment": { "type": "keyword" },
"service.version": { "type": "keyword" },
"host.name": { "type": "keyword" },
"container.id": { "type": "keyword" },
"log.level": { "type": "keyword" },
"log.logger": { "type": "keyword" },
"message": { "type": "text" },
"trace.id": { "type": "keyword" },
"span.id": { "type": "keyword" },
"request.id": { "type": "keyword" },
"user.id": { "type": "keyword" },
"http.method": { "type": "keyword" },
"http.status_code": { "type": "short" },
"http.route": { "type": "keyword" },
"event.duration": { "type": "long" },
"error.type": { "type": "keyword" },
"error.message": { "type": "text" },
"geo.ip": { "type": "ip" },
"labels": { "type": "object", "enabled": false }
}
}
}
}
设计要点:
- 使用 Data Stream 管理只追加的日志数据,避免手工维护每日索引;
service.name、log.level、trace.id都是 keyword,适合精确过滤;message保留全文检索;labels关闭索引,避免业务标签无限膨胀破坏 Mapping;- 时间字段统一为
@timestamp,查询、告警和 ILM 都依赖它。
20.3 Ingest Pipeline
Ingest Pipeline 可以完成字段规整、类型转换、地理解析、失败处理和脱敏。
PUT _ingest/pipeline/app-logs-pipeline
{
"processors": [
{
"set": {
"field": "data_stream.type",
"value": "logs"
}
},
{
"set": {
"field": "data_stream.dataset",
"value": "app"
}
},
{
"date": {
"field": "time",
"target_field": "@timestamp",
"formats": ["ISO8601", "UNIX_MS"],
"timezone": "Asia/Shanghai",
"on_failure": [
{
"set": {
"field": "event.parse_error",
"value": "invalid_time"
}
},
{
"set": {
"field": "@timestamp",
"value": "{{{_ingest.timestamp}}}"
}
}
]
}
},
{
"lowercase": { "field": "log.level", "ignore_missing": true }
},
{
"script": {
"lang": "painless",
"source": """
if (ctx.message != null) {
ctx.message = ctx.message
.replaceAll(/\\b\\d{17}[0-9Xx]\\b/, '<id_card>')
.replaceAll(/\\b1[3-9]\\d{9}\\b/, '<mobile>')
.replaceAll(/(?i)authorization\\s*[:=]\\s*\\S+/, 'authorization=<redacted>');
}
"""
}
},
{
"remove": {
"field": ["password", "token", "secret"],
"ignore_missing": true
}
}
]
}
日志脱敏最好不要完全依赖 Elasticsearch。应用输出、采集器和 ES Pipeline 三层都可以做,越靠近源头做,风险越小;ES Pipeline 适合作为最后兜底。
20.4 采集管道
常见接入方式有三种。
20.4.1 Filebeat 直写
适合中小规模:
filebeat.inputs:
- type: filestream
id: app-log
paths:
- /var/log/app/*.log
parsers:
- ndjson:
add_error_key: true
message_key: message
output.elasticsearch:
hosts: ["https://es01:9200"]
username: "filebeat-writer"
password: "${ES_PASSWORD}"
index: "app-logs"
pipeline: "app-logs-pipeline"
setup.template.enabled: false
20.4.2 Kafka 缓冲
适合大规模和突发流量:
flowchart LR
A[应用日志] --> B[Filebeat/OTel Collector]
B --> K[Kafka]
K --> L[Logstash/消费者]
L --> E[Elasticsearch]
Kafka 的价值是削峰、解耦和可重放。ES 故障时日志先留在 Kafka,恢复后继续消费,避免日志直接丢失。
20.4.3 OpenTelemetry
如果团队同时需要日志、指标和链路,OpenTelemetry Collector 是更统一的选择。它能把 trace_id、span_id、service.name 自动带入日志,减少手工埋点不一致。
20.5 ILM 生命周期
日志最典型的生命周期:
PUT _ilm/policy/app-logs-policy
{
"policy": {
"phases": {
"hot": {
"min_age": "0ms",
"actions": {
"rollover": {
"max_primary_shard_size": "50gb",
"max_age": "1d"
},
"forcemerge": {
"max_num_segments": 1
}
}
},
"warm": {
"min_age": "2d",
"actions": {
"shrink": {
"number_of_shards": 1
},
"allocate": {
"require": { "data": "warm" }
}
}
},
"cold": {
"min_age": "15d",
"actions": {
"searchable_snapshot": {
"snapshot_repository": "backup-repo"
}
}
},
"delete": {
"min_age": "30d",
"actions": {
"delete": {}
}
}
}
}
}
使用建议:
- Hot 节点保留最近高频查询数据,优先内存和 SSD;
- Warm 节点承接较旧数据,可以减少副本、合并段;
- Cold 数据可用 searchable snapshot 或快照归档;
- Delete 阶段必须与合规要求确认,不要只按磁盘压力决定;
max_primary_shard_size通常比max_age更能保持分片均匀。
20.6 查询排障
20.6.1 查询某条链路
GET logs-app-default/_search
{
"size": 100,
"query": {
"bool": {
"filter": [
{ "term": { "trace.id": "4bf92f3577b34da6a3ce929d0e0e4736" } },
{ "range": { "@timestamp": { "gte": "now-2h", "lte": "now" } } }
]
}
},
"sort": [{ "@timestamp": "asc" }]
}
20.6.2 查询服务错误
GET logs-app-default/_search
{
"size": 50,
"query": {
"bool": {
"filter": [
{ "term": { "service.name": "order-service" } },
{ "terms": { "log.level": ["error", "fatal"] } },
{ "range": { "@timestamp": { "gte": "now-15m" } } }
]
}
},
"sort": [{ "@timestamp": "desc" }]
}
20.6.3 查看错误分布和影响用户
GET logs-app-default/_search
{
"size": 0,
"query": {
"bool": {
"filter": [
{ "term": { "service.name": "order-service" } },
{ "term": { "log.level": "error" } },
{ "range": { "@timestamp": { "gte": "now-1h" } } }
]
}
},
"aggs": {
"per_minute": {
"date_histogram": {
"field": "@timestamp",
"fixed_interval": "1m",
"min_doc_count": 0
},
"aggs": {
"users": {
"cardinality": { "field": "user.id" }
}
}
},
"top_errors": {
"terms": { "field": "error.type", "size": 10 }
}
}
}
排障时尽量使用 filter,不要无必要地把日志级别、服务名、时间范围放进 must。这些条件不需要评分,放进 filter 可以利用缓存,也让查询语义更清晰。
20.7 控制查询成本
日志查询最容易出问题的不是单条查询,而是“无时间范围的聚合”和“所有人都能查 30 天”。
治理建议:
- 所有查询强制带
@timestamp范围; - 默认查询最近 15 分钟,扩大范围需要用户主动选择;
- Dashboard 默认刷新间隔不要小于 1 分钟;
- 高基数字段不要做
terms大 size 聚合; - 避免对
message做大规模聚合或排序; - 保存常用 query template,减少临时复杂 DSL;
- 对 Kibana 用户设置并发、超时和索引权限;
- 对特别重的分析任务迁移到离线集群或数据仓库。
20.8 权限与审计
日志包含用户 ID、IP、请求参数甚至异常堆栈中的敏感数据。权限设计要同时控制“能查什么索引”和“能看到什么字段”。
POST /_security/role/logs_order_reader
{
"indices": [
{
"names": ["logs-app-order-*"],
"privileges": ["read"],
"field_security": {
"grant": [
"*",
"geo.ip",
"user.id"
],
"except": ["authorization", "request.headers"]
}
}
]
}
实际权限策略要遵循最小权限:
- 业务开发只能看本服务相关数据集;
- 生产日志查询需要审批或审计;
- 含用户手机号、身份证、token 的字段默认不可见;
- 客服、研发、安全团队使用不同角色;
- 审计日志单独保存,不能被普通用户删除;
- ES 节点间、客户端到 ES 全部启用 TLS。
20.9 上线清单
- 字段规范已统一,团队有新增字段评审流程;
- 时间解析失败时有兜底和错误标记;
- 索引模板使用 Data Stream 和 ILM;
- 写入端有限流、重试和背压;
- Kafka 消费有 lag 监控和死信队列;
- 高峰写入和高峰查询已经压测;
- 查询默认带时间范围;
- 敏感字段已脱敏,角色已收敛;
- 磁盘水位、写入拒绝、查询延迟、消费延迟有告警;
- 定期演练节点故障、磁盘告警和快照恢复。
本章小结
日志系统不是把日志写进 Elasticsearch 就结束了。稳定的数据模型、统一的时间字段、受控的采集链路、合理的生命周期、强制的时间范围和最小权限,共同决定了排障效率和长期成本。
思考题
- 为什么日志索引推荐用 Data Stream,而不是普通索引?
trace.id为什么必须是 keyword?如果误建成 text 会发生什么?- Filebeat 直写和 Kafka 中转各适合什么规模?
- ILM 的 Hot/Warm/Cold/Delete 阶段分别解决什么问题?
- 如果用户反馈“日志查询很慢”,你会先检查哪五项?