在Golang项目开发中,日志的收集与聚合是保障系统可观测性的核心环节,合理的日志体系能够帮助开发者快速定位问题、分析系统运行状态。本文汇总多种Golang日志收集聚合的实用方法,覆盖从基础日志生成到分布式场景下的日志汇聚全链路。
基于标准库log的基础日志收集
Golang标准库自带的log模块可以满足基础的日志输出需求,我们可以通过自定义输出目标实现简单的日志收集。下面的示例将日志同时输出到控制台和本地文件,方便后续做基础的聚合处理。
package main
import (
"log"
"os"
)
func main() {
// 创建日志文件
logFile, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
log.Fatal("打开日志文件失败:", err)
}
defer logFile.Close()
// 设置日志输出到文件和控制台
log.SetOutput(io.MultiWriter(os.Stdout, logFile))
// 设置日志前缀和标志
log.SetPrefix("【APP_LOG】")
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
// 输出测试日志
log.Println("这是一条普通日志")
log.Println("这是一条包含业务信息的日志,用户ID:1001,操作:登录")
}
使用logrus实现结构化日志收集
logrus是Golang中常用的第三方日志库,支持结构化日志输出,更适合后续的日志聚合处理。我们可以通过配置logrus的输出格式和钩子,将日志发送到指定的存储系统。
package main
import (
"os"
"github.com/sirupsen/logrus"
)
func main() {
// 创建logrus实例
logger := logrus.New()
// 设置日志级别
logger.SetLevel(logrus.InfoLevel)
// 设置为JSON结构化格式,方便后续聚合解析
logger.SetFormatter(&logrus.JSONFormatter{
TimestampFormat: "2006-01-02 15:04:05",
})
// 添加文件输出钩子
logFile, err := os.OpenFile("app_struct.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
logger.Fatal("打开日志文件失败:", err)
}
logger.SetOutput(logFile)
// 输出结构化日志,附带自定义字段
logger.WithFields(logrus.Fields{
"user_id": 1001,
"action": "login",
"status": "success",
}).Info("用户登录成功")
logger.WithFields(logrus.Fields{
"order_id": 2001,
"amount": 99.9,
}).Info("订单创建完成")
}
使用zap实现高性能日志收集
zap是Uber开源的高性能日志库,性能优于logrus,适合对性能要求较高的生产场景。下面的示例展示如何配置zap实现日志的分级输出和文件轮转,为聚合做准备。
package main
import (
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"os"
"time"
)
func main() {
// 配置编码器,设置为JSON格式
encoderConfig := zapcore.EncoderConfig{
TimeKey: "time",
LevelKey: "level",
NameKey: "logger",
CallerKey: "caller",
MessageKey: "msg",
StacktraceKey: "stacktrace",
LineEnding: zapcore.DefaultLineEnding,
EncodeLevel: zapcore.LowercaseLevelEncoder,
EncodeTime: zapcore.ISO8601TimeEncoder,
EncodeDuration: zapcore.SecondsDurationEncoder,
EncodeCaller: zapcore.ShortCallerEncoder,
}
jsonEncoder := zapcore.NewJSONEncoder(encoderConfig)
// 配置日志输出目标,同时输出到控制台和文件
fileWriter := zapcore.AddSync(&lumberjack.Logger{
Filename: "app_zap.log",
MaxSize: 10, // 单个日志文件最大10MB
MaxBackups: 3, // 最多保留3个备份
MaxAge: 7, // 日志保留7天
Compress: true,
})
consoleWriter := zapcore.AddSync(os.Stdout)
// 设置日志级别
highPriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
return lvl >= zapcore.InfoLevel
})
// 创建核心
core := zapcore.NewTee(
zapcore.NewCore(jsonEncoder, consoleWriter, highPriority),
zapcore.NewCore(jsonEncoder, fileWriter, highPriority),
)
// 创建logger实例
logger := zap.New(core, zap.AddCaller())
defer logger.Sync()
// 输出日志
logger.Info("zap日志测试",
zap.String("module", "user"),
zap.Int("user_id", 1002),
zap.String("action", "logout"),
)
logger.Error("接口调用失败",
zap.String("api", "/api/order"),
zap.Int("code", 500),
zap.String("err_msg", "数据库连接超时"),
)
}
日志聚合到Elasticsearch实践
当日志量较大时,我们需要将分散的日志聚合到统一的存储系统中,Elasticsearch是常用的日志存储方案。下面的示例展示如何将Golang生成的日志发送到Elasticsearch中。
package main
import (
"context"
"fmt"
"github.com/elastic/go-elasticsearch/v8"
"github.com/elastic/go-elasticsearch/v8/esapi"
"log"
"strings"
"time"
)
func main() {
// 配置Elasticsearch客户端
cfg := elasticsearch.Config{
Addresses: []string{
"http://192.168.0.1:9200",
},
}
client, err := elasticsearch.NewClient(cfg)
if err != nil {
log.Fatal("创建ES客户端失败:", err)
}
// 构造日志数据
logData := strings.NewReader(fmt.Sprintf(`{
"time": "%s",
"level": "info",
"module": "order",
"msg": "订单支付完成",
"order_id": 3001,
"amount": 199.9
}`, time.Now().Format("2006-01-02 15:04:05")))
// 发送日志到ES
req := esapi.IndexRequest{
Index: "app-logs",
DocumentID: "",
Body: logData,
Refresh: "true",
}
res, err := req.Do(context.Background(), client)
if err != nil {
log.Fatal("发送日志到ES失败:", err)
}
defer res.Body.Close()
if res.IsError() {
log.Printf("ES返回错误: %s", res.Status())
} else {
log.Println("日志成功写入Elasticsearch")
}
}
日志聚合到Loki实践
Loki是Grafana开源的轻量级日志聚合系统,适合云原生场景下的日志收集。我们可以通过发送HTTP请求将Golang日志推送到Loki中。
package main
import (
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"time"
)
// Loki日志推送请求结构
type LokiStream struct {
Stream map[string]string `json:"stream"`
Values [][]string `json:"values"`
}
type LokiPushRequest struct {
Streams []LokiStream `json:"streams"`
}
func main() {
// 构造日志内容
logTime := time.Now().UnixNano()
logContent := fmt.Sprintf(`{"time":"%s","level":"warn","module":"system","msg":"内存使用率超过阈值","usage":0.85}`,
time.Now().Format("2006-01-02 15:04:05"))
// 构造Loki推送请求
pushReq := LokiPushRequest{
Streams: []LokiStream{
{
Stream: map[string]string{
"app": "golang-demo",
"level": "warn",
"module": "system",
"job": "log-collect",
},
Values: [][]string{
{fmt.Sprintf("%d", logTime), logContent},
},
},
},
}
// 序列化请求数据
reqData, err := json.Marshal(pushReq)
if err != nil {
log.Fatal("序列化请求数据失败:", err)
}
// 发送请求到Loki
resp, err := http.Post("http://127.0.0.1:3100/loki/api/v1/push", "application/json", bytes.NewBuffer(reqData))
if err != nil {
log.Fatal("推送日志到Loki失败:", err)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNoContent {
log.Println("日志成功推送到Loki")
} else {
log.Printf("Loki返回异常状态码: %d", resp.StatusCode)
}
}
不同方案对比
我们可以根据实际场景选择合适的日志收集聚合方案,下面的表格对比了不同方案的特点:
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 标准库log | 小型项目、简单调试场景 | 无需引入第三方依赖,使用简单 | 功能单一,不支持结构化输出,性能一般 |
| logrus | 中小型项目,需要结构化日志 | 生态完善,支持钩子扩展,使用灵活 | 性能不如zap,大流量场景下有瓶颈 |
| zap | 高性能要求、大流量生产场景 | 性能优异,内存占用低,支持结构化输出 | 配置相对复杂,API使用门槛稍高 |
| Elasticsearch聚合 | 需要复杂日志查询、分析的场景 | 查询能力强,支持全文检索,生态完善 | 部署维护成本高,资源占用较多 |
| Loki聚合 | 云原生场景、轻量级日志聚合 | 部署简单,资源占用低,与Grafana集成方便 | 查询能力弱于Elasticsearch,不支持全文检索 |
注意事项
- 生产环境中建议对日志进行分级,不同级别日志输出到不同目标,避免无用日志占用存储
- 日志文件需要做轮转处理,避免单个文件过大影响读写性能
- 结构化日志的字段尽量统一规范,方便后续聚合查询
- 敏感信息如用户密码、身份证号等不要输出到日志中,避免信息泄露
- 日志聚合系统的地址不要硬编码在代码中,建议通过配置文件读取,方便环境切换