Go日志分析系统-ELK实时日志监控

1: 概述与适用范围

  • 本文搭建一套可在单台开发机运行的日志采集与检索环境,完整链路为:Go 应用 -> JSON 日志文件 -> Filebeat -> Logstash -> Elasticsearch -> Kibana
  • Go 应用负责逐行写入结构化 JSON,Filebeat 负责增量读取文件,Logstash 负责接收和转发,Elasticsearch 负责存储与检索,Kibana 负责可视化查询;
  • 教程面向 Go 开发者和初级 DevOps 工程师,重点是先跑通一个可观察、可排障的最小闭环;
  • 本文的 Compose 配置关闭了 Elastic 安全认证,仅允许用于本机学习,禁止直接暴露到公网或照搬到生产环境
  • 原文参考:Go日志分析系统-ELK实时日志监控

2: ELK/Filebeat 组件职责

组件职责本文端口
Go 应用生成逐行 JSON 业务日志
Filebeat追踪文件读取位置,将新增日志发送给 Logstash
Logstash通过 Beats input 接收事件,并写入指定索引5044,监控 API 9600
Elasticsearch存储、索引和查询日志9200
Kibana创建 Data View,使用 KQL 检索日志5601
  • Filebeat 比直接让 Go 应用调用 Elasticsearch 更容易处理断线重试、读取进度和日志轮转;
  • Logstash 在这个入门示例中只做轻量转发。后续需要字段清洗、脱敏或路由时,可以在 pipeline 中增加 filter;
  • Elasticsearch 和 Kibana 比较消耗内存,启动前应确认 Docker 可用资源充足。

3: 数据流与示例目录

数据流如下:

1
2
3
4
5
6
main.go
  -> ./logs/app.log
  -> Filebeat filestream input
  -> Logstash beats input :5044
  -> Elasticsearch go-app-yyyy.MM.dd
  -> Kibana Data View: go-app-*

新建一个空目录,并准备以下文件:

1
2
3
4
5
6
7
8
9
go-elk-demo/
├── docker-compose.yml
├── main.go
├── logs/
├── filebeat/
│   └── filebeat.yml
└── logstash/
    └── pipeline/
        └── logstash.conf
  • logs/ 可以暂时不手动创建,Go 程序中的 os.MkdirAll 会负责创建;
  • Filebeat 容器将宿主机 ./logs 只读挂载为 /var/log/go-app
  • Filebeat 的读取进度保存在命名卷 filebeat-data 中,容器重建后不会从头重复读取所有日志。

4: 环境要求

  • Docker Engine 24+,并包含 Docker Compose v2;
  • Go 1.21+;
  • curl,用于检查 Elasticsearch;
  • 建议为 Docker 分配至少 4 GB 内存和 2 个 CPU;
  • Linux 主机如果 Elasticsearch 因虚拟内存限制退出,可临时设置:
1
sudo sysctl -w vm.max_map_count=262144
  • 上述 sysctl -w 只在当前系统运行期间生效,重启后会恢复;
  • 需要持久化时,写入独立配置文件并重新加载系统参数:
1
2
echo 'vm.max_map_count=262144' | sudo tee /etc/sysctl.d/99-elasticsearch.conf
sudo sysctl --system

先确认工具版本:

1
2
3
4
docker version
docker compose version
go version
curl --version

5: Go JSON 日志程序

在项目根目录创建 main.go。示例只使用 Go 标准库,输出的每一行都是一个独立 JSON 对象。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
package main

import (
    "encoding/json"
    "fmt"
    "os"
    "time"
)

type LogEntry struct {
    Timestamp string `json:"@timestamp"`
    Level     string `json:"level"`
    Service   string `json:"service"`
    TraceID   string `json:"trace_id"`
    Message   string `json:"message"`
}

func run() (err error) {
    if err := os.MkdirAll("logs", 0o755); err != nil {
        return fmt.Errorf("create logs directory: %w", err)
    }

    file, err := os.OpenFile("logs/app.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
    if err != nil {
        return fmt.Errorf("open log file: %w", err)
    }
    defer func() {
        if closeErr := file.Close(); closeErr != nil && err == nil {
            err = fmt.Errorf("close log file: %w", closeErr)
        }
    }()

    encoder := json.NewEncoder(file)
    entries := []LogEntry{
        {
            Timestamp: time.Now().UTC().Format(time.RFC3339Nano),
            Level:     "INFO",
            Service:   "demo-api",
            TraceID:   "trace-1000",
            Message:   "service started",
        },
        {
            Timestamp: time.Now().UTC().Format(time.RFC3339Nano),
            Level:     "ERROR",
            Service:   "demo-api",
            TraceID:   "trace-1001",
            Message:   "database request timed out",
        },
        {
            Timestamp: time.Now().UTC().Format(time.RFC3339Nano),
            Level:     "INFO",
            Service:   "demo-api",
            TraceID:   "trace-1002",
            Message:   "request completed",
        },
    }

    for _, entry := range entries {
        if err := encoder.Encode(entry); err != nil {
            return fmt.Errorf("encode log entry: %w", err)
        }
    }

    if err := file.Sync(); err != nil {
        return fmt.Errorf("sync log file: %w", err)
    }

    return nil
}

func main() {
    if err := run(); err != nil {
        fmt.Fprintf(os.Stderr, "write logs: %v\n", err)
        os.Exit(1)
    }
}
  • json.NewEncoder 会在每次 Encode 后追加换行,符合 Filebeat 的逐行采集方式;
  • @timestamp 使用 UTC RFC3339 格式,方便 Elasticsearch 和 Kibana 作为时间字段处理;
  • 程序采用追加模式,每运行一次会再生成 3 条日志。

6: Docker Compose

创建 docker-compose.yml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.14.3
    environment:
      discovery.type: single-node
      xpack.security.enabled: "false"
      ES_JAVA_OPTS: "-Xms1g -Xmx1g"
    ports:
      - "127.0.0.1:9200:9200"
    volumes:
      - elasticsearch-data:/usr/share/elasticsearch/data
    healthcheck:
      test: ["CMD-SHELL", "curl -fsS http://localhost:9200/_cluster/health >/dev/null || exit 1"]
      interval: 10s
      timeout: 5s
      retries: 20
      start_period: 30s
    restart: unless-stopped

  logstash:
    image: docker.elastic.co/logstash/logstash:8.14.3
    environment:
      LS_JAVA_OPTS: "-Xms512m -Xmx512m"
      XPACK_MONITORING_ENABLED: "false"
    ports:
      - "127.0.0.1:5044:5044"
      - "127.0.0.1:9600:9600"
    volumes:
      - ./logstash/pipeline:/usr/share/logstash/pipeline:ro
    depends_on:
      elasticsearch:
        condition: service_healthy
    healthcheck:
      test: ["CMD-SHELL", "curl -fsS http://localhost:9600/_node/pipelines >/dev/null || exit 1"]
      interval: 10s
      timeout: 5s
      retries: 20
      start_period: 30s
    restart: unless-stopped

  filebeat:
    image: docker.elastic.co/beats/filebeat:8.14.3
    user: root
    command: ["filebeat", "-e", "--strict.perms=false"]
    volumes:
      - ./filebeat/filebeat.yml:/usr/share/filebeat/filebeat.yml:ro
      - ./logs:/var/log/go-app:ro
      - filebeat-data:/usr/share/filebeat/data
    depends_on:
      logstash:
        condition: service_healthy
    healthcheck:
      test: ["CMD-SHELL", "filebeat test output >/dev/null 2>&1 || exit 1"]
      interval: 15s
      timeout: 10s
      retries: 10
      start_period: 20s
    restart: unless-stopped

  kibana:
    image: docker.elastic.co/kibana/kibana:8.14.3
    environment:
      ELASTICSEARCH_HOSTS: "http://elasticsearch:9200"
    ports:
      - "127.0.0.1:5601:5601"
    depends_on:
      elasticsearch:
        condition: service_healthy
    healthcheck:
      test: ["CMD-SHELL", "curl -fsS http://localhost:5601/api/status >/dev/null || exit 1"]
      interval: 10s
      timeout: 5s
      retries: 30
      start_period: 40s
    restart: unless-stopped

volumes:
  elasticsearch-data:
  filebeat-data:
  • 四个组件统一使用 8.14.3,避免不同大版本之间的协议和字段兼容问题;
  • 对外端口只绑定 127.0.0.1,降低学习环境被局域网或公网访问的风险;
  • Filebeat 使用 root--strict.perms=false 只是为了兼容本地 bind mount 的所有权差异;生产环境应设置正确的文件所有者和权限,并保留 Filebeat 的严格权限检查;
  • depends_on.condition 用于等待上游健康,但它不能替代应用自身的重试机制。

7: Logstash pipeline

创建 logstash/pipeline/logstash.conf

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
input {
  beats {
    port => 5044
  }
}

filter {
  if ![service] or [service] == "" {
    mutate {
      add_field => { "service" => "unknown" }
    }
  }
}

output {
  elasticsearch {
    hosts => ["http://elasticsearch:9200"]
    index => "go-app-%{+yyyy.MM.dd}"
  }

  stdout {
    codec => rubydebug
  }
}
  • Beats input 与 Filebeat output 都使用 5044
  • 索引名称固定为 go-app-日期,因此 Kibana Data View 使用 go-app-*
  • stdout 便于在学习阶段通过 docker compose logs logstash 查看 Logstash 实际收到的事件。

8: Filebeat 配置

创建 filebeat/filebeat.yml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
filebeat.inputs:
  - type: filestream
    id: go-app-json
    enabled: true
    paths:
      - /var/log/go-app/*.log
    prospector.scanner.fingerprint.length: 64
    parsers:
      - ndjson:
          target: ""
          overwrite_keys: true
          add_error_key: true
          message_key: message

output.logstash:
  hosts: ["logstash:5044"]

logging.level: info
logging.to_files: false
  • paths 必须写容器内路径 /var/log/go-app/*.log,它与 Compose 中 ./logs:/var/log/go-app:ro 完全对应;
  • ndjson.target: "" 将 JSON 字段放到事件根级,overwrite_keys: true 允许应用日志中的 @timestamp 覆盖 Filebeat 的采集时间,Logstash 也可以直接读取 serviceleveltrace_id
  • filestream 默认指纹长度较大,入门示例日志文件很小,因此将指纹长度调整为 64 字节,避免短文件迟迟不开始采集;该值只适用于短演示文件,生产环境应使用更长指纹,或保证不同日志文件的文件头足以区分;
  • registry 位于 /usr/share/filebeat/data,已经映射到 filebeat-data 命名卷。

9: 启动服务

先让 Go 程序创建 logs/ 和第一批日志,再校验并启动 Compose:

1
2
3
4
go run ./main.go
docker compose config
docker compose up -d
docker compose ps

docker compose config 应输出合并后的有效配置且不报错。docker compose ps 最终应看到四个服务均为 Up,健康检查完成后状态包含 healthy

检查 Elasticsearch 集群健康状态:

1
curl -sS http://localhost:9200/_cluster/health?pretty

单机模式下预期 statusyellowgreen。如果使用默认副本设置,单节点无法分配副本时显示 yellow 是正常现象,但不应为 red

10: 生成并验证日志

服务全部健康后,再生成一批日志:

1
go run ./main.go

查看本地文件和各采集组件日志:

1
2
3
tail -n 6 logs/app.log
docker compose logs --tail=100 filebeat
docker compose logs --tail=100 logstash

确认索引已经创建:

1
curl -sS 'http://localhost:9200/_cat/indices/go-app-*?v'

查询最新日志:

1
curl -sS 'http://localhost:9200/go-app-*/_search?pretty&size=10&sort=@timestamp:desc'

同时比对原始日志和 Elasticsearch 返回结果中的时间字段:原文件每行的 @timestamp 应与查询结果 _source.@timestamp 一致,而不是 Filebeat 读取该行的时间。

预期结果中包含:

1
2
3
4
5
6
{
  "level": "ERROR",
  "service": "demo-api",
  "trace_id": "trace-1001",
  "message": "database request timed out"
}
  • 第一次启动时,镜像下载、Elasticsearch 初始化和 Filebeat 建立连接需要一些时间;
  • 如果查询暂时为空,等待数秒后重试,并按第 14 节顺序检查各环节。

11: Kibana Data View 与 KQL

  1. 浏览器打开 http://localhost:5601
  2. 进入 Stack Management -> Data Views
  3. 点击 Create data view
  4. Name 可填写 Go App Logs,Index pattern 填写 go-app-*
  5. Timestamp field 选择 @timestamp,保存后进入 Discover
  6. 将右上角时间范围调整为包含刚才生成日志的时间。

在 Discover 的查询框中分别执行以下 KQL:

1
service : "demo-api"
1
level : "ERROR"
1
trace_id : "trace-1001"
  • service 用于按服务筛选;
  • level 用于快速定位错误日志;
  • trace_id 用于串联一次请求在不同处理阶段的日志。真实服务中应由入口生成 trace ID,并在调用链中持续传递。

12: 日志轮转和保留策略

  • 本文程序只负责追加文件,没有内置轮转。长期运行时必须限制单文件大小和保留时间,否则磁盘最终会写满;
  • 在 Linux 主机上可以使用 logrotate,按天或按大小轮转 logs/app.log,设置 rotatemaxagecompress 等策略;
  • Go 服务也可以使用成熟的轮转库,但应确认轮转方式与 Filebeat filestream 的文件识别机制兼容,优先采用重命名旧文件并创建新文件,而不是反复复制截断;
  • Filebeat registry 只记录读取进度,不负责删除源日志;源文件轮转与 Elasticsearch 索引保留是两个独立问题;
  • Elasticsearch 生产环境通常通过 ILM(Index Lifecycle Management)管理热数据保留、只读、迁移和删除。例如保留 7 天热数据、30 天后删除;具体周期应根据查询需求、合规要求和磁盘容量确定;
  • 本文为了保持入门配置简洁,没有创建 ILM policy。不要把“每日索引”误认为已经具备自动清理能力。

13: 监控与基础安全

建议至少关注以下状态:

对象基础检查
容器docker compose ps、重启次数、CPU、内存、磁盘
Filebeatharvester 是否启动、发送失败、registry 是否可写、积压量
Logstashpipeline 是否加载、事件吞吐、队列和 JVM 堆使用率
Elasticsearchcluster health、节点磁盘、JVM heap、索引增长和 rejected 请求
  • 学习环境可以用 docker compose logsdocker stats 和各组件 HTTP API 做基础检查;
  • 生产环境必须启用 Elasticsearch/Kibana 认证和 TLS,Filebeat 到 Logstash、Logstash 到 Elasticsearch 的链路也应使用 TLS;
  • 为采集端、写入端和查询端分别创建最小权限账号,不要让所有组件共用超级管理员凭据;
  • 日志网络应与业务公网隔离,通过防火墙、安全组或容器网络限制访问;
  • 日志中不得写入密码、Token、完整身份证号等敏感数据。必要时在应用输出前或 Logstash filter 中脱敏;
  • 密钥应通过 Secret 管理系统或受控环境变量注入,不能硬编码到 Compose 或提交到 Git。

14: 常见故障排查

排障时沿数据流从左到右检查,不要直接在 Kibana 末端反复刷新。

14.1 容器未启动

1
2
docker compose ps -a
docker compose logs --tail=200 elasticsearch logstash filebeat kibana
  • Elasticsearch 常见原因是内存不足或 vm.max_map_count 太小;
  • Logstash、Filebeat 常见原因是 YAML/配置文件语法错误或挂载路径不存在;
  • 先修复第一个明确错误,再重启对应服务:docker compose restart 服务名

14.2 Filebeat 无输入

1
2
3
ls -l logs/app.log
docker compose exec filebeat ls -l /var/log/go-app
docker compose logs --tail=200 filebeat
  • 确认宿主机文件存在,并且容器内 /var/log/go-app/app.log 可读;
  • 确认配置中的路径是 /var/log/go-app/*.log,而不是宿主机路径 ./logs/*.log
  • 如果为了重复实验删除了日志文件,但保留了 registry,Filebeat 可能认为旧内容已经读过。学习环境可按第 16 节完整清理卷后重试;生产环境不要随意删除 registry。

14.3 Logstash pipeline 错误

1
2
docker compose logs --tail=200 logstash
curl -sS http://localhost:9600/_node/pipelines?pretty
  • 重点检查 logstash.conf 的括号、插件参数和 Elasticsearch 地址;
  • hosts 应使用 Compose 服务名 http://elasticsearch:9200,不能在容器内写 localhost:9200
  • 修改 pipeline 后执行 docker compose restart logstash,再观察是否成功加载。

14.4 Elasticsearch 无索引

1
2
3
curl -sS http://localhost:9200/_cluster/health?pretty
curl -sS 'http://localhost:9200/_cat/indices?v'
docker compose logs --tail=200 logstash
  • 集群为 red 时先处理 Elasticsearch 本身;
  • 集群正常但没有 go-app-* 时,检查 Logstash 是否收到事件以及 output 是否连接成功;
  • 如果 Logstash 有事件但写入失败,日志通常会给出映射冲突、连接失败或磁盘水位等原因。

14.5 Kibana 无数据

  • 先用 _cat/indices_search 确认 Elasticsearch 中确实有数据;
  • 确认 Data View 是 go-app-*,时间字段是 @timestamp
  • 调大 Discover 时间范围,并清空可能残留的 KQL 筛选条件;
  • 如果 _search 有数据而 Discover 没有,检查日志时间是否位于当前查询时间窗内。

15: 生产环境演进

跑通单机链路后,可以按实际瓶颈逐步演进,而不是一次引入所有复杂组件:

  • 日志峰值导致 Logstash 或 Elasticsearch 短暂不可用时,可在 Filebeat 与消费端之间引入 Kafka,提供削峰和更长时间的缓冲;
  • 数据量和可用性要求提升后,将单节点 Elasticsearch 扩展为多节点集群,并规划分片、副本、冷热层和快照恢复;
  • 运行在 Kubernetes 时,可用 Filebeat DaemonSet 采集各节点容器日志,并补充 namespace、pod、container 等元数据;
  • 配置数量增加后,可使用 Fleet、配置仓库、模板化部署或其他集中配置管理方式,避免逐台修改;
  • 进一步接入告警、Dashboard、链路追踪和指标系统,形成日志、指标、Trace 的统一可观测体系。

Kafka、Elasticsearch 集群、Kubernetes DaemonSet 和集中配置管理都属于生产演进,不是本文核心教程的前置条件。

16: 清理环境

停止并删除容器、网络以及 Elasticsearch/Filebeat 数据卷:

1
docker compose down -v --remove-orphans

如需同时删除本地示例日志:

1
rm -f logs/app.log
  • -v 会删除 Elasticsearch 数据和 Filebeat registry,所有已索引日志及读取进度都会丢失;
  • 执行前确认目录中没有需要保留的数据;
  • 不加 -vdocker compose down 会保留命名卷,适合稍后继续实验。

17: 总结与参考

  • 结构化 JSON 是日志进入检索平台的基础,稳定字段比一大段难以解析的文本更容易筛选和聚合;
  • Filebeat 负责可靠采集,Logstash 负责处理与转发,Elasticsearch 负责存储检索,Kibana 负责交互式分析;
  • 本文所有路径、端口和索引保持统一:./logs/*.log504492005601go-app-*
  • 单机 Compose 适合学习和验证,不代表生产架构。生产环境必须补齐认证、TLS、网络隔离、最小权限、轮转、ILM、备份和监控。

参考资料: