前言

在微服务架构中,服务日志的采集与处理是运维监控体系的基础。本教程将手把手教你构建一个基础的日志采集系统,包含:

  • 使用 Golang Gin 构建 Web 服务
  • 使用 Zap 实现 JSON 格式日志记录到文件
  • 支持优雅退出,确保日志完整写入
  • 使用 Docker 实现服务容器化
  • 使用 Filebeat 转发日志到文件(后续可扩展为转发至 ELK/Kafka)

架构分析

项目结构

为更好地展示本章节内容,下面列出项目结构。

demo
│  docker-compose.yml
│  Dockerfile
│  Dockerfile-filebeat
│  go.mod
│  go.sum
│
├─app
│  │  main.exe
│  │  main.go
│  │
│  └─service
│          service.go
│
├─filebeat
│      filebeat.yml
│
├─filebeat_output
│      filebeat.log
│
└─logs
        app.log

Gin + Zap 实现日志记录

Gin 服务启动代码主要位于 app/main.go,负责:

  • 初始化 Zap 日志组件
  • 启动 Gin 引擎与路由
  • 设置日志中间件与优雅退出逻辑
package main

import (
	"context"
	"errors"
	"github.com/gin-gonic/gin"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"
)

func main() {
	logger := NewLogger()
	defer logger.Sync()
	server := NewMyServer(logger)

	r := gin.New()
	r.Use(GinZapLogger(logger), gin.Recovery())
	r.GET("/service1", server.Service1)
	srv := &http.Server{
		Addr:    ":8080",
		Handler: r,
	}
	go func() {
		if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
			logger.Fatal("server error", zap.Error(err))
		}
	}()
	// 优雅退出监听
	quit := make(chan os.Signal)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
	<-quit
	logger.Info("Shutting down server...")

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	if err := srv.Shutdown(ctx); err != nil {
		logger.Fatal("Server forced to shutdown:", zap.Error(err))
	}

	logger.Info("Server exiting")
}

func GinZapLogger(logger *zap.Logger) gin.HandlerFunc {
	return func(c *gin.Context) {
		start := time.Now()
		path := c.Request.URL.Path

		c.Next()

		latency := time.Since(start)
		status := c.Writer.Status()
		clientIP := c.ClientIP()
		method := c.Request.Method

		logger.Info("GIN_LOG",
			zap.Int("status", status),
			zap.String("method", method),
			zap.String("path", path),
			zap.String("ip", clientIP),
			zap.Duration("latency", latency),
		)
	}
}

type MyServer struct {
	logger *zap.Logger
}

func NewMyServer(logger *zap.Logger) *MyServer {
	return &MyServer{
		logger: logger,
	}
}

func (s *MyServer) Service1(ctx *gin.Context) {
	param := ctx.Query("param")
	s.logger.Info("Service1: param", zap.String("param", param))
	ctx.JSON(http.StatusOK, gin.H{
		"code": 200,
		"msg":  "success",
		"data": param,
	})
}

func NewLogger() *zap.Logger {
	logFile := "logs/app.log"
	_ = os.MkdirAll("logs", os.ModePerm)

	// 文件输出
	fileWriter, _ := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	fileSyncer := zapcore.AddSync(fileWriter)

	// 控制台输出
	consoleSyncer := zapcore.AddSync(os.Stdout)

	// 编码器
	encoderCfg := zap.NewProductionEncoderConfig()
	encoderCfg.EncodeTime = zapcore.ISO8601TimeEncoder
	encoder := zapcore.NewJSONEncoder(encoderCfg)

	// 多输出 core
	core := zapcore.NewTee(
		zapcore.NewCore(encoder, fileSyncer, zapcore.InfoLevel),
		zapcore.NewCore(encoder, consoleSyncer, zapcore.DebugLevel),
	)

	logger := zap.New(core, zap.AddCaller())
	logger.Info("Hello Zap!", zap.String("env", "docker"))

	return logger
}

功能简述

1. Gin 服务启动与路由
  • 创建 Gin 引擎:gin.New()

  • 注册中间件:

    • GinZapLogger(logger):记录每个请求日志
    • gin.Recovery():防止服务崩溃
  • 注册路由:

    • 示例接口:GET /service1

2. Zap 日志系统初始化
  • 使用 NewLogger 创建 Zap 日志器,支持:

    • 日志输出到文件(如:logs/app.log
    • 同时输出到控制台(os.Stdout
    • 日志格式:JSON,时间戳格式为 ISO8601
    • 日志等级控制:文件为 Info,控制台为 Debug
  • 使用 zapcore.NewTee 实现多路输出

  • 日志包含调用代码位置(启用 zap.AddCaller()


3. 日志中间件
  • 自定义 GinZapLogger 中间件,记录请求详情:

    • 请求方法、路径、状态码、客户端 IP、处理耗时
    • 所有内容写入 Zap 日志

4. 业务逻辑处理
  • 示例函数:MyServer.Service1

    • 获取 URL 参数 param
    • 将参数写入日志
    • 返回标准 JSON 响应

Filebeat 配置详解(filebeat.yml 示例)

Filebeat是一个轻量级的日志采集工具,用于转发和汇总服务器、虚拟机和容器的日志。它从输入源读取日志,通过Harvester逐行读取,然后输出到目标如ElasticSearch。
Filebeat组件解析

如图所示,Filebeat 的核心是两个组件:InputHarvester


1. Harvester 说明

  • 每个文件由一个 Harvester 负责读取(逐行读取)
  • Harvester 负责打开并保持文件描述符
  • 即使文件被删除或重命名,Harvester 仍会继续读取内容
  • 文件删除后,只要进程持有 FD,内容仍可读取,直到 Harvester 关闭
  • 若长时间无数据更新(达到 close_inactive),Harvester 将停止

总结:Harvester 的职责是——逐行读取文件,并将日志传递给 Output。


2. Input 说明

  • 管理 Harvester 的组件
  • 负责发现哪些文件需要读取(基于路径)
  • 支持配置多个 Input,每个 Input 在单独 goroutine 中运行
  • Input 发现文件后,为其启动 Harvester 进行日志收集
filebeat.inputs:
  - type: log
    enabled: true
    paths:
      - /logs/app.log

output.file:
  path: "/output"
  filename: "filebeat.log"
  rotate_every_kb: 10000
  number_of_files: 7

💡 注:paths 要与 Gin 服务日志文件路径保持一致(挂载路径可在 Docker Compose 中设置)。

本章节中我们使用 File 输出作为演示,后续章节中将拓展为输出到 Elasticsearch/Kafka。


启动及编排

我们为 Gin 服务Filebeat 各自编写了独立的 Dockerfile,并通过 docker-compose 统一编排启动。


Gin 服务 Dockerfile

FROM golang:1.22-alpine

WORKDIR /app

# 复制依赖定义,利用缓存优化构建
COPY go.mod go.sum ./
RUN go mod download

# 复制源代码
COPY ./app ./app

RUN go build -o elk ./app/main.go

CMD ["./elk"]

Filebeat Dockerfile

FROM docker.elastic.co/beats/filebeat:7.17.9

USER root
COPY ./filebeat/filebeat.yml /usr/share/filebeat/filebeat.yml
RUN chown filebeat:filebeat /usr/share/filebeat/filebeat.yml && chmod 600 /usr/share/filebeat/filebeat.yml

USER filebeat

Docker Compose 配置

使用 docker-compose.yml 启动两个容器并进行网络/挂载配置:

version: '3'

services:
  gin-app:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "8080:8080"
    volumes:
      - ./logs:/app/logs                # 挂载日志目录
    container_name: gin-app

  filebeat:
    build:
      context: .
      dockerfile: Dockerfile-filebeat
    volumes:
      - ./logs:/logs
      - ./filebeat_output:/output
    depends_on:
      - gin-app
    container_name: filebeat

启动后系统整体结构:

当前系统架构


项目运行说明

1. 构建并启动项目

docker-compose up --build

2. 测试接口请求

curl http://localhost:8080/service1?param=test1

3. 查看 Filebeat 转发后的文件内容

cat ./filebeat_output/filebeat.log

日志示例输出

Gin zap 写入日志文件(app.log):

{
    "level": "info",
    "ts": "2025-06-24T23:00:56.069+0800",
    "caller": "app/main.go:82",
    "msg": "Service1: param",
    "param": "test1"
}

Filebeat 转发后的日志文件(filebeat.log):

{
    "@timestamp": "2025-06-24T13:55:13.408Z",
    "@metadata": {
        "beat": "filebeat",
        "type": "_doc",
        "version": "7.17.9"
    },
    "log": {
        "offset": 3637,
        "file": {
            "path": "/logs/app.log"
        }
    },
    "message": "{\"level\":\"info\",\"ts\":1750773310.5815911,\"msg\":\"Service1: param\",\"param\":\"test1\"}",
    "input": {
        "type": "log"
    },
    "ecs": {
        "version": "1.12.0"
    },
    "host": {
        "name": "2c889fcbb3a3"
    },
    "agent": {
        "type": "filebeat",
        "version": "7.17.9",
        "hostname": "2c889fcbb3a3",
        "ephemeral_id": "69e67121-a145-4f76-9554-465c198399c2",
        "id": "351dc454-38d4-446e-8666-6a665deac048",
        "name": "2c889fcbb3a3"
    }
}

总结

本章我们完整展示了 Golang + zap + Filebeat 的日志采集方案,并支持 Docker 容器化部署。当前我们已实现:

  • 接口日志记录
  • Zap 文件日志输出
  • Filebeat 读取日志并转发输出

后续我们在此基础上继续拓展:

  • 接入 Elasticsearch + Kibana,实现可视化日志分析

参考文档

【Filebeat工作原理分析】
【Go之Zap日志库集成Gin】
【Go 日志记录库:uber-go 的日志操作库 zap 使用】
【docker学习笔记18:Dockerfile 指令 VOLUME 介绍】
【菜鸟教程:docker-compose】

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐