必须用 logrus 或 zap 通过 TCP/HTTP 发结构化 JSON 日志至 Logstash,并带 service_name 和 trace_id 字段;Logstash 需配置 json filter 解析 message,k8s 中需验证网络连通性并避免 UDP 丢包。

Go 微服务里日志怎么发给 Logstash 才不丢、不乱、能关联
直接结论:别用 log.Printf 直连 Logstash,得走 TCP/UDP 或 HTTP 协议封装成结构化日志,且必须带 trace_id 和 service_name 字段。否则日志到了 Logstash 就是碎片,查问题时根本串不起来。
常见错误现象:logstash-plain 输入插件收不到日志;日志时间戳全是 Logstash 接收时间;多个服务日志混在一起分不清来源;高并发下大量 connection refused 或 broken pipe 错误。
- 用
logrus或zap替代标准库log,它们支持结构化字段和 Hook 机制 - 日志输出必须走网络协议(推荐
tcp或http),Logstash 配置对应输入插件(tcp{ port => 5000 }或http{ port => 8080 }) - 每条日志至少带上
service_name(如"order-service")、trace_id(从上下文提取,不能随机生成)、level、timestamp - 避免在日志写入路径做同步网络调用——用异步队列(如
logrus/hooks/airbrake类 Hook + channel 缓冲)或批量发送
Logstash 配置里 input/filter/output 怎么配才匹配 Go 日志格式
Go 发出的 JSON 日志,Logstash 默认会当字符串处理,导致 Kibana 里搜 level: error 没结果。关键不是“能不能收”,而是“收进来后字段是否可检索”。
典型错误配置:input { tcp { port => 5000 } } 后直接 output { elasticsearch { ... } },没做 json 解码,ES 里所有字段都在 message 字符串里。
立即学习“go语言免费学习笔记(深入)”;
-
filter块必须加json { source => "message" },否则字段不会被解析成顶层字段 - 如果 Go 日志用了嵌套结构(比如
{"meta": {"user_id": 123}}),加json { source => "message" target => "log" }再用mutate { rename => { "[log][meta][user_id]" => "user_id" } } - 确保
input的codec是json_lines(TCP/UDP)或默认(HTTP),否则多行日志会粘包 - 测试配置是否生效:用
curl -X POST http://localhost:8080 -H 'Content-Type: application/json' -d '{"level":"info","msg":"test"}',看 ES 是否出现level: "info"字段
Go 里用 logrus + logstash hook 发日志,为什么总连不上或超时
不是 Logstash 没开,而是 Go 客户端没设对重试和超时。logrus 的 logstash 官方 Hook(github.com/bshuster-repo/logrus-logstash-hook)默认用 UDP,但生产环境丢包率高,且不报错,看起来像“静默失败”。
常见现象:logrus 不报错,但 Logstash 的 tcp input 日志里看不到连接记录;本地跑通,上 k8s 就断;压测时日志丢失率突然飙升到 30%+。
- 禁用 UDP,改用
tcp并显式设置timeout(建议3s)和reconnect(true) - Hook 初始化时传入
logstash.NewHook("logstash-host:5000", &logstash.LogstashFormatter{}),别漏掉端口 - k8s 环境注意 Service DNS 名是否可达,用
nslookup logstash-headless或telnet logstash 5000实测连通性 - 别把 Hook 加在全局 logger 上就完事——每个微服务实例应有独立
service_name字段,通过WithField("service_name", os.Getenv("SERVICE_NAME"))注入
trace_id 怎么从 Gin / gRPC 上下文透传到日志里
没有 trace_id,分布式日志就是一堆散点。Go 微服务里最常踩的坑是:HTTP 请求头里的 X-Request-ID 或 traceparent 提取了,但没塞进 logger context,结果日志里还是空的。
错误做法:在 handler 开头 req.Header.Get("X-Request-ID"),然后手动拼进 log.WithField("trace_id", ...) —— 每个 handler 都要写一遍,漏一个就断链。
- Gin 场景:用中间件统一提取,写进
c.Set("trace_id", id),再配合logrus.WithContext(c.Request.Context())+ 自定义 Formatter 输出 - gRPC 场景:用
grpc_middleware+grpc_zap,注册UnaryServerInterceptor,自动把metadata.MD里的trace-id注入 zap logger - 关键点:logger 实例必须是 request-scoped 的(比如
log.WithFields(...)每次新建),不能复用全局 logger 直接Infof,否则字段会污染其他请求 - 验证方式:查一条错误日志,确认
trace_id字段值和上游调用方传入的一致,且能在 Jaeger/Kibana 里按该 ID 聚合出完整链路
真正难的不是发日志,是让每条日志在跨进程、跨网络、跨协议时,字段语义不丢失、时间不漂移、上下文不脱钩。尤其是 trace_id 和 service_name 这两个字段,一旦漏设或错设,后面所有聚合、检索、告警都白搭。










