【Go】基于 Gin 从0到1搭建 Web 管理后台系统后端服务(一)项目初始化、配置和日志(下)

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 【Go】基于 Gin 从0到1搭建 Web 管理后台系统后端服务(一)项目初始化、配置和日志(下)

3、日志初始化

为什么需要记录日志?

在Go语言中记录日志是一个非常常见的行为。以下是几个原因:

  1. 问题排查:在应用程序出现故障或错误时,日志记录可以帮助我们定位问题所在。我们可以通过查看日志记录,了解应用程序在何时、何处出现问题,从而更快地进行故障排查。
  2. 性能分析:日志记录也可以用于分析应用程序的性能。我们可以在代码中记录应用程序执行过程中的关键事件和时间戳,然后使用这些信息来分析和优化应用程序的性能。
  3. 安全监控:日志记录还可以用于监控应用程序的安全性。我们可以记录所有的访问请求和响应,以便在应用程序受到攻击时能够快速响应。
  4. 后续分析:日志记录还可以用于后续分析和审计。我们可以将日志记录存储在外部数据库中,以便在未来需要时能够查询和分析。

值得注意的是,日志记录是一个非常重要的行为,可以帮助我们快速排查问题、优化性能、监控安全性以及进行后续分析和审计。因此,在Go语言中记录日志是一个必要的步骤。

这里将使用 zap 作为日志库,一般来说,日志都是需要写入到文件保存的,这也是 zap 唯一缺少的部分,所以我将结合 lumberjack 来使用,实现日志切割归档的功能。使用Zap作为日志记录器,有以下几个原因:

  1. 高性能:Zap是一个非常高性能的日志库,其性能比其他日志库(如logrus和zap)更高。这主要是由于Zap使用了零内存分配的技术,可以在不影响性能的情况下减少垃圾回收。
  2. 丰富的功能:Zap支持多种日志级别、多种输出格式(如JSON、Console、Logfmt等)、多种输出位置(如控制台、文件、网络等),还支持自定义日志字段和Hook。
  3. 安全性:Zap采用了严格的日志记录策略,确保日志安全性。它可以避免由于多协程写入日志导致的竞态条件,还可以自动切分日志文件,避免单个日志文件过大导致的性能问题和磁盘空间浪费。
  4. 社区支持:Zap是由Uber开发并维护的开源日志库,其开发和维护的活跃度和社区支持度都非常高。因此,Zap可以得到广泛的支持和使用,并有大量的第三方库和框架与之兼容。

而Lumberjack是一个用于高性能、轮转和滚动日志文件的库。在Zap中使用Lumberjack有以下几个好处:

  1. 高性能:Lumberjack使用了缓冲区和异步写入等技术,可以减少IO的次数,提高写入日志的性能。
  2. 轮转和滚动:Lumberjack可以自动进行日志文件的轮转和滚动,可以防止单个日志文件变得过大而影响性能,也可以方便地管理和备份历史日志。
  3. 稳定性和可靠性:Lumberjack可以确保即使在写入日志的同时发生了故障,也不会丢失日志数据。此外,Lumberjack还可以处理文件系统的错误和日志文件权限等问题。

在Zap中使用Lumberjack可以提高日志的性能、稳定性和可靠性,并且方便管理和备份历史日志。

安装zap:

go get -u go.uber.org/zap
go get -u gopkg.in/natefinch/lumberjack.v2

3.1 定义配置项

config.yarml 中新增日志配置:

# ...
zap: # 日志配置
  level: info # 日志级别
  prefix: '[east_white_common_admin/server]' # 日志前缀
  format: console # 输出
  director: log # 日志存放的文件
  encode_level: LowercaseColorLevelEncoder # 编码级别
  stacktrace_key: stacktrace # 栈名
  max_age: 0 # 日志留存时间
  show_line: true # 显示行
  log_in_console: true # 输出控制台

然后新建 config/zap.go,在文件中增加对应的结构体和日志级别转换方法:

package config
import (
   "strings"
   "go.uber.org/zap/zapcore"
)
type Zap struct {
   Level         string `mapstructure:"level" json:"level" yaml:"level"`                            // 级别
   Prefix        string `mapstructure:"prefix" json:"prefix" yaml:"prefix"`                         // 日志前缀
   Format        string `mapstructure:"format" json:"format" yaml:"format"`                         // 输出
   Director      string `mapstructure:"director" json:"director"  yaml:"director"`                  // 日志文件夹
   EncodeLevel   string `mapstructure:"encode_level" json:"encode_level" yaml:"encode_level"`       // 编码级
   StacktraceKey string `mapstructure:"stacktrace_key" json:"stacktrace_key" yaml:"stacktrace_key"` // 栈名
   MaxAge       int  `mapstructure:"max_age" json:"max_age" yaml:"max_age"`                      // 日志留存时间
   ShowLine     bool `mapstructure:"show_line" json:"show_line" yaml:"show_line"`                // 显示行
   LogInConsole bool `mapstructure:"log_in_console" json:"log_in_console" yaml:"log_in_console"` // 输出控制台
}
// ZapEncodeLevel 根据 EncodeLevel 返回 zapcore.LevelEncoder
func (z *Zap) ZapEncodeLevel() zapcore.LevelEncoder {
   switch {
   case z.EncodeLevel == "LowercaseLevelEncoder": // 小写编码器(默认)
      return zapcore.LowercaseLevelEncoder
   case z.EncodeLevel == "LowercaseColorLevelEncoder": // 小写编码器带颜色
      return zapcore.LowercaseColorLevelEncoder
   case z.EncodeLevel == "CapitalLevelEncoder": // 大写编码器
      return zapcore.CapitalLevelEncoder
   case z.EncodeLevel == "CapitalColorLevelEncoder": // 大写编码器带颜色
      return zapcore.CapitalColorLevelEncoder
   default:
      return zapcore.LowercaseLevelEncoder
   }
}
// TransportLevel 根据字符串转化为 zapcore.Level
func (z *Zap) TransportLevel() zapcore.Level {
   z.Level = strings.ToLower(z.Level)
   switch z.Level {
   case "debug":
      return zapcore.DebugLevel
   case "info":
      return zapcore.InfoLevel
   case "warn":
      return zapcore.WarnLevel
   case "error":
      return zapcore.WarnLevel
   case "dpanic":
      return zapcore.DPanicLevel
   case "panic":
      return zapcore.PanicLevel
   case "fatal":
      return zapcore.FatalLevel
   default:
      return zapcore.DebugLevel
   }
}

别忘了在 config.go 中加入 zap

package config
type Configuration struct {
   App App `mapstructure:"app" json:"app" yaml:"app"`
   Zap Zap `mapstructure:"zap" json:"zap" yaml:"zap"`
}

3.2 日志初始化方法

接下来就是写日志初始化方法了,在 core 中新建 zap.go

package core
import (
   "ewa_admin_server/core/internal"
   "ewa_admin_server/global"
   "ewa_admin_server/utils"
   "fmt"
   "os"
   "go.uber.org/zap"
   "go.uber.org/zap/zapcore"
)
// InitializeZap Zap 获取 zap.Logger
func InitializeZap() (logger *zap.Logger) {
   if ok, _ := utils.PathExists(global.EWA_CONFIG.Zap.Director); !ok { // 判断是否有Director文件夹
      fmt.Printf("create %v directory\n", global.EWA_CONFIG.Zap.Director)
      _ = os.Mkdir(global.EWA_CONFIG.Zap.Director, os.ModePerm)
   }
   cores := internal.Zap.GetZapCores()
   logger = zap.New(zapcore.NewTee(cores...))
   if global.EWA_CONFIG.Zap.ShowLine {
      logger = logger.WithOptions(zap.AddCaller())
   }
   fmt.Println("====2-zap====: zap log init success")
   return logger
}

core/internal 中新建 zap.go

package internal
import (
   "ewa_admin_server/global"
   "fmt"
   "time"
   "go.uber.org/zap"
   "go.uber.org/zap/zapcore"
)
var Zap = new(_zap)
type _zap struct{}
// GetEncoder 获取 zapcore.Encoder
func (z *_zap) GetEncoder() zapcore.Encoder {
   if global.EWA_CONFIG.Zap.Format == "json" {
      return zapcore.NewJSONEncoder(z.GetEncoderConfig())
   }
   return zapcore.NewConsoleEncoder(z.GetEncoderConfig())
}
// GetEncoderConfig 获取zapcore.EncoderConfig
func (z *_zap) GetEncoderConfig() zapcore.EncoderConfig {
   return zapcore.EncoderConfig{
      MessageKey:     "message",
      LevelKey:       "level",
      TimeKey:        "time",
      NameKey:        "logger",
      CallerKey:      "caller",
      StacktraceKey:  global.EWA_CONFIG.Zap.StacktraceKey,
      LineEnding:     zapcore.DefaultLineEnding,
      EncodeLevel:    global.EWA_CONFIG.Zap.ZapEncodeLevel(),
      EncodeTime:     z.CustomTimeEncoder,
      EncodeDuration: zapcore.SecondsDurationEncoder,
      EncodeCaller:   zapcore.FullCallerEncoder,
   }
}
// GetEncoderCore 获取Encoder的 zapcore.Core
func (z *_zap) GetEncoderCore(l zapcore.Level, level zap.LevelEnablerFunc) zapcore.Core {
   writer, err := FileRotateLogs.GetWriteSyncer(l.String()) // 使用file-rotatelogs进行日志分割
   if err != nil {
      fmt.Printf("Get Write Syncer Failed err:%v", err.Error())
      return nil
   }
   return zapcore.NewCore(z.GetEncoder(), writer, level)
}
// CustomTimeEncoder 自定义日志输出时间格式
func (z *_zap) CustomTimeEncoder(t time.Time, encoder zapcore.PrimitiveArrayEncoder) {
   encoder.AppendString(global.EWA_CONFIG.Zap.Prefix + t.Format("2006/01/02 - 15:04:05.000"))
}
// GetZapCores 根据配置文件的Level获取 []zapcore.Core
func (z *_zap) GetZapCores() []zapcore.Core {
   cores := make([]zapcore.Core, 0, 7)
   for level := global.EWA_CONFIG.Zap.TransportLevel(); level <= zapcore.FatalLevel; level++ {
      cores = append(cores, z.GetEncoderCore(level, z.GetLevelPriority(level)))
   }
   return cores
}
// GetLevelPriority 根据 zapcore.Level 获取 zap.LevelEnablerFunc
func (z *_zap) GetLevelPriority(level zapcore.Level) zap.LevelEnablerFunc {
   switch level {
   case zapcore.DebugLevel:
      return func(level zapcore.Level) bool { // 调试级别
         return level == zap.DebugLevel
      }
   case zapcore.InfoLevel:
      return func(level zapcore.Level) bool { // 日志级别
         return level == zap.InfoLevel
      }
   case zapcore.WarnLevel:
      return func(level zapcore.Level) bool { // 警告级别
         return level == zap.WarnLevel
      }
   case zapcore.ErrorLevel:
      return func(level zapcore.Level) bool { // 错误级别
         return level == zap.ErrorLevel
      }
   case zapcore.DPanicLevel:
      return func(level zapcore.Level) bool { // dpanic级别
         return level == zap.DPanicLevel
      }
   case zapcore.PanicLevel:
      return func(level zapcore.Level) bool { // panic级别
         return level == zap.PanicLevel
      }
   case zapcore.FatalLevel:
      return func(level zapcore.Level) bool { // 终止级别
         return level == zap.FatalLevel
      }
   default:
      return func(level zapcore.Level) bool { // 调试级别
         return level == zap.DebugLevel
      }
   }
}

以及 file_rotate_logs.go

package internal
import (
   "ewa_admin_server/global"
   "os"
   "path"
   "time"
   rotatelogs "github.com/lestrrat-go/file-rotatelogs"
   "go.uber.org/zap/zapcore"
)
var FileRotateLogs = new(fileRotateLogs)
type fileRotateLogs struct{}
// GetWriteSyncer 获取 zapcore.WriteSyncer
func (r *fileRotateLogs) GetWriteSyncer(level string) (zapcore.WriteSyncer, error) {
   fileWriter, err := rotatelogs.New(
      path.Join(global.EWA_CONFIG.Zap.Director, "%Y-%m-%d", level+".log"),
      rotatelogs.WithClock(rotatelogs.Local),
      rotatelogs.WithMaxAge(time.Duration(global.EWA_CONFIG.Zap.MaxAge)*24*time.Hour), // 日志留存时间
      rotatelogs.WithRotationTime(time.Hour*24),
   )
   if global.EWA_CONFIG.Zap.LogInConsole {
      return zapcore.NewMultiWriteSyncer(zapcore.AddSync(os.Stdout), zapcore.AddSync(fileWriter)), err
   }
   return zapcore.AddSync(fileWriter), err
}

新建 utils/directory.go 文件,编写 PathExists 函数,用于判断路径是否存在:

package utils
import (
   "errors"
   "os"
)
//@function: PathExists
//@description: 文件目录是否存在
//@param: path string
//@return: bool, error
func PathExists(path string) (bool, error) {
   fi, err := os.Stat(path)
   if err == nil {
      if fi.IsDir() {
         return true, nil
      }
      return false, errors.New("存在同名文件")
   }
   if os.IsNotExist(err) {
      return false, nil
   }
   return false, err
}

3.3 定义全局变量

global/global.go 中,添加 Log 成员属性

package global
import (
   "ewa_admin_server/config"
   "go.uber.org/zap"
   "github.com/spf13/viper"
)
var (
   EWA_CONFIG config.Configuration
   EWA_VIPER  *viper.Viper
   EWA_LOG    *zap.Logger
)

3.4 测试

在 main.go 中调用日志初始化函数,并尝试写入日志:

package main
import (
   "ewa_admin_server/core"
   "ewa_admin_server/global"
   "go.uber.org/zap"
   "github.com/gin-gonic/gin"
)
const AppMode = "debug" // 运行环境,主要有三种:debug、test、release
func main() {
   gin.SetMode(AppMode)
   // TODO:1.配置初始化
   global.EWA_VIPER = core.InitializeViper()
   // TODO:2.日志
   global.EWA_LOG = core.InitializeZap()
   zap.ReplaceGlobals(global.EWA_LOG)
   global.EWA_LOG.Info("server run success on ", zap.String("zap_log", "zap_log"))
   //  TODO:3.数据库连接
   // TODO:4.其他初始化
   // TODO:5.启动服务
   core.RunServer()
}

重启项目,就会发现在根目录下生成了一个 log 文件夹,作为我们日后开发用的日志记录文件。
image.png

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
2月前
|
中间件 Go
Golang | Gin:net/http与Gin启动web服务的简单比较
总的来说,`net/http`和 `Gin`都是优秀的库,它们各有优缺点。你应该根据你的需求和经验来选择最适合你的工具。希望这个比较可以帮助你做出决策。
80 35
|
7月前
|
安全 Go
用 Zap 轻松搞定 Go 语言中的结构化日志
在现代应用程序开发中,日志记录至关重要。Go 语言中有许多日志库,而 Zap 因其高性能和灵活性脱颖而出。本文详细介绍如何在 Go 项目中使用 Zap 进行结构化日志记录,并展示如何定制日志输出,满足生产环境需求。通过基础示例、SugaredLogger 的便捷使用以及自定义日志配置,帮助你在实际开发中高效管理日志。
192 1
|
3月前
|
存储 JSON Go
PHP 日志系统的最佳搭档:一个 Go 写的远程日志收集服务
为了不再 SSH 上去翻日志,我写了个 Go 小脚本,用来接收远程日志。PHP 负责记录日志,Go 负责存储和展示,按天存储、支持 API 访问、可远程管理,终于能第一时间知道项目炸了。
67 10
|
7月前
|
Go API 数据库
Go 语言中常用的 ORM 框架,如 GORM、XORM 和 BeeORM,分析了它们的特点、优势及不足,并从功能特性、性能表现、易用性和社区活跃度等方面进行了比较,旨在帮助开发者根据项目需求选择合适的 ORM 框架。
本文介绍了 Go 语言中常用的 ORM 框架,如 GORM、XORM 和 BeeORM,分析了它们的特点、优势及不足,并从功能特性、性能表现、易用性和社区活跃度等方面进行了比较,旨在帮助开发者根据项目需求选择合适的 ORM 框架。
647 4
|
7月前
|
存储 JSON Go
如何在 Go 项目中隐藏敏感信息,比如避免暴露用户密码?
在Go语言开发中,用户信息管理常涉及敏感数据如密码的处理。为防止这些数据暴露给客户端,本文介绍了三种方法:使用JSON标签忽略字段、自定义序列化逻辑、使用数据传输对象(DTO),以确保用户数据的安全性。通过这些方法,可以有效控制数据输出,避免敏感信息泄露。
117 1
|
7月前
|
中间件 Go API
Go语言中几种流行的Web框架,如Beego、Gin和Echo,分析了它们的特点、性能及适用场景,并讨论了如何根据项目需求、性能要求、团队经验和社区支持等因素选择最合适的框架
本文概述了Go语言中几种流行的Web框架,如Beego、Gin和Echo,分析了它们的特点、性能及适用场景,并讨论了如何根据项目需求、性能要求、团队经验和社区支持等因素选择最合适的框架。
664 1
|
8月前
|
Go
使用go语言将A助手加入项目中
使用go语言将A助手加入项目中
46 2
|
20天前
|
存储 消息中间件 前端开发
PHP后端与uni-app前端协同的校园圈子系统:校园社交场景的跨端开发实践
校园圈子系统校园论坛小程序采用uni-app前端框架,支持多端运行,结合PHP后端(如ThinkPHP/Laravel),实现用户认证、社交关系管理、动态发布与实时聊天功能。前端通过组件化开发和uni.request与后端交互,后端提供RESTful API处理业务逻辑并存储数据于MySQL。同时引入Redis缓存热点数据,RabbitMQ处理异步任务,优化系统性能。核心功能包括JWT身份验证、好友系统、WebSocket实时聊天及活动管理,确保高效稳定的用户体验。
87 3
PHP后端与uni-app前端协同的校园圈子系统:校园社交场景的跨端开发实践
|
3月前
|
JSON 自然语言处理 前端开发
【01】对APP进行语言包功能开发-APP自动识别地区ip后分配对应的语言功能复杂吗?-成熟app项目语言包功能定制开发-前端以uniapp-基于vue.js后端以laravel基于php为例项目实战-优雅草卓伊凡
【01】对APP进行语言包功能开发-APP自动识别地区ip后分配对应的语言功能复杂吗?-成熟app项目语言包功能定制开发-前端以uniapp-基于vue.js后端以laravel基于php为例项目实战-优雅草卓伊凡
186 72
【01】对APP进行语言包功能开发-APP自动识别地区ip后分配对应的语言功能复杂吗?-成熟app项目语言包功能定制开发-前端以uniapp-基于vue.js后端以laravel基于php为例项目实战-优雅草卓伊凡
|
2月前
|
前端开发 JavaScript 关系型数据库
2025 年前端与后端开发方向的抉择与展望-优雅草卓伊凡
2025 年前端与后端开发方向的抉择与展望-优雅草卓伊凡
102 5
2025 年前端与后端开发方向的抉择与展望-优雅草卓伊凡
下一篇
oss创建bucket