必须用 zap 输出结构化 json 日志并注入上下文,经 filebeat 采集、logstash 轻量处理后写入 elasticsearch,才能打通 gin→elk 链路;否则默认文本日志会导致解析失败、字段类型错误和链路追踪缺失。

直接用 Gin 原生日志或 log 包写文件,进不了 ELK。必须让日志以结构化、可传输的格式落地,再由 Filebeat 采集——这是打通 Gin → ELK 链路的硬性前提。
为什么 Gin 默认日志不能直连 ELK
Gin 的 gin.DefaultWriter 或标准库 log 输出的是纯文本行,无固定字段、无时间戳 ISO 格式、无 JSON 封装,Logstash 的 json codec 会解析失败,grok 则需反复调优正则,极易漏字段或错类型。更关键的是:它不带 trace_id、request_id 等上下文,无法关联请求链路。
- 现象:
Logstash日志里出现大量_jsonparsefailuretag 或message字段原样堆积 - 后果:Elasticsearch 中
status被识别为text,导致无法聚合;latency是字符串,排序/统计全错 - 正确路径:Gin 输出结构化日志 → Filebeat 收集 → Logstash(可选轻量处理)→ Elasticsearch
用 zap + middleware 输出 JSON 日志并注入上下文
zap 是目前 Go 生态性能最好、结构化最自然的日志库,配合 Gin 中间件可自动注入 HTTP 方法、路径、状态码、耗时、trace_id(需结合 OpenTelemetry 或自定义 header)。
- 安装:
go get go.uber.org/zap - 在中间件中初始化 logger 并写入结构体:
func ZapLogger(logger *zap.Logger) gin.HandlerFunc { return func(c *gin.Context) { start := time.Now() c.Next() <pre class="brush:php;toolbar:false;"> logger.Info("http_request", zap.String("method", c.Request.Method), zap.String("path", c.Request.URL.Path), zap.Int("status", c.Writer.Status()), zap.Float64("latency_ms", float64(time.Since(start).Microseconds())/1000), zap.String("ip", c.ClientIP()), zap.String("user_agent", c.Request.UserAgent()), zap.String("trace_id", c.GetHeader("X-Trace-ID")), // 若有透传 ) }}
- 确保输出到文件(非 stdout),例如:
l, _ := zap.NewProduction(zap.Output(zapcore.AddSync(&os.File{Fd: int64(123)}))),或用os.OpenFile指定日志路径 - 关键:关闭
zap.Development(),启用zap.NewProduction(),否则时间戳不是@timestamp格式,datefilter 会失败
Filebeat 配置要点:权限、codec、字段补全
Filebeat 不是“配了就能用”,常见卡点在权限和字段缺失。它默认以 filebeat 用户运行,但 Gin 日志文件往往属主是 app 或 root,需显式授权。
- 日志路径权限:执行
sudo setfacl -m u:filebeat:r /var/log/myapp/access.log(推荐 ACL,比改属组更安全) - 配置
filebeat.inputs必须指定codec: json,否则仍当纯文本读:filebeat.inputs: - type: filestream enabled: true paths: - /var/log/myapp/access.log codec: json - 补
@timestamp:若zap输出没带ts字段(或格式非 ISO),加processors自动注入:processors: - add_fields: target: '' fields: service.name: "my-gin-api" - timestamp: field: ts layouts: - 'Jan 02 15:04:05.000' test: - '2026-09-21T00:05:33.123Z' - 避免重发:设
close_inactive: 5m和clean_inactive: 72h,防止日志轮转后重复采集
Logstash 只做必要过滤,别在 pipeline 里做字段计算
Logstash 是瓶颈点,尤其高并发时 CPU 占用飙升。Gin 已用 zap 输出完整结构,Logstash 应只做三件事:校验时间、补索引名、丢弃脏数据。所有字段加工(如提取 URL 参数、计算 PV/UV)应交给 Kibana Lens 或 Elasticsearch Painless script。
- input 插件用
beats(不是file),因 Filebeat 已完成采集与解析:input { beats { port => 5044 } } - filter 只保留 date + mutate:
filter { date { match => ["@timestamp", "ISO8601"] timezone => "UTC" } mutate { add_field => { "[@metadata][index]" => "gin-app-%{+YYYY.MM.dd}" } } } - output 直写 ES,禁用
document_id(除非真需要去重),否则写入性能下降 30%+:output { elasticsearch { hosts => ["http://es-host:9200"] index => "%{[@metadata][index]}" } } - 验证:用
curl -s http://localhost:9600/_node/stats/pipeline?pretty | grep -A5 'events\|filter_time_in_millis'查看 pipeline 实际耗时
真正难的不是配置,而是字段一致性——zap 输出的 latency_ms 类型必须和 Elasticsearch 模板里定义的 float 完全匹配,差一个字母(比如写成 latency_ms vs latency_ms_)就会触发 dynamic mapping,后续再改要 reindex。上线前务必用 curl -XGET 'http://es:9200/gin-app-*/_mapping?pretty' 确认字段类型。











