ElasticsearchNotes

第 20 章:日志检索实战

zjc 于 2026-01-20 发布

这是《Elasticsearch 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 日志检索是 Elasticsearch 另一个主流场景。它和商品搜索的关注点完全不同:商品搜索追求相关性,日志检索追求稳定接入、低成本存储、快速定位异常,以及清晰的生命周期治理。

本章从日志模型、采集管道、索引模板、查询排障到权限治理,搭建一套可落地的日志系统。

20.1 日志系统的需求

一个生产日志平台通常要回答这些问题:

问题 示例 依赖能力
服务是否报错 某服务近 15 分钟 ERROR 日志 时间过滤、级别过滤
请求在哪里失败 trace_id 串联多个服务 keyword 精确查找
影响多少用户 某错误影响的 UID 数 去重聚合
什么时候开始 错误按分钟分布 date_histogram
是否已恢复 错误率与成功率对比 指标日志、聚合
如何审计 谁查询了敏感日志 权限与审计

日志系统的常见挑战:

  1. 写入峰值高,故障时日志量可能放大 5 到 10 倍;
  2. 保存周期长,磁盘成本容易失控;
  3. 字段不规范,排障时会遇到同一个含义多个字段名;
  4. 日志包含敏感信息,需要脱敏和权限控制;
  5. 查询时间范围过大时,容易造成集群压力。

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 }
      }
    }
  }
}

设计要点:

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": {}
        }
      }
    }
  }
}

使用建议:

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 天”。

治理建议:

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"]
      }
    }
  ]
}

实际权限策略要遵循最小权限:

20.9 上线清单

本章小结

日志系统不是把日志写进 Elasticsearch 就结束了。稳定的数据模型、统一的时间字段、受控的采集链路、合理的生命周期、强制的时间范围和最小权限,共同决定了排障效率和长期成本。

思考题

  1. 为什么日志索引推荐用 Data Stream,而不是普通索引?
  2. trace.id 为什么必须是 keyword?如果误建成 text 会发生什么?
  3. Filebeat 直写和 Kafka 中转各适合什么规模?
  4. ILM 的 Hot/Warm/Cold/Delete 阶段分别解决什么问题?
  5. 如果用户反馈“日志查询很慢”,你会先检查哪五项?