Golang | Web开发之Gin路由访问日志自定义输出实践

欢迎关注「全栈工程师修炼指南」公众号

点击 👇 下方卡片 即可关注我哟!

设为星标⭐每天带你 基础入门 到 进阶实践 再到 放弃学习

专注 企业运维实践、网络安全、系统运维、应用开发、物联网实战、全栈文章 等知识分享

  花开堪折直须折,莫待无花空折枝 


作者主页:[ https://www.weiyigeek.top ]  

博客:[ https://blog.weiyigeek.top ]

作者<开发安全运维>学习交流群,回复【学习交流群】即可加入


文章目录:

0x02 如何自定义 Gin 日志格式?

1.自定义定义路由日志的格式

2.自定义原生路由访问日志格式

3.使用 log 自定义日志并按天分隔保存到文件

0x02 如何自定义 Gin 日志格式?

1.自定义定义路由日志的格式

描述: 此处介绍如何定义路由日志的格式,而非使用默认的路由访问日志格式。
例如:默认的路由日志格式 GIN-debug] POST /foo --> main.main.func1 (3 handlers), 如果你想要以指定的格式(例如 JSON,key values 或其他格式)记录信息,则可以使用 gin.DebugPrintRouteFunc 指定格式。

package main

import (
	"github.com/gin-gonic/gin"
	"log"
	"net/http"
)

func main() {
  // gin 运行模式
  gin.SetMode(gin.DebugMode)

	r := gin.Default()
  
  // 关键点
	gin.DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string, nuHandlers int) {
		log.Printf("endpoint %v %v %v %v\n", httpMethod, absolutePath, handlerName, nuHandlers)
	}

	r.POST("/foo", func(c *gin.Context) {
		c.JSON(http.StatusOK, "foo")
	})

	r.GET("/bar", func(c *gin.Context) {
		c.JSON(http.StatusOK, "bar")
	})

	r.GET("/status", func(c *gin.Context) {
		c.JSON(http.StatusOK, "ok")
	})

	// 监听并在 0.0.0.0:8080 上启动服务
	r.Run()
}

执行效果:
ad9d62e5fd58909bd9567babccda0584.png


偷偷的告诉你哟?极客全栈修炼】微信小程序已经上线了,

可直接在微信里面直接浏览博主博客了哟,后续将上线更多有趣的小工具。


2.自定义原生路由访问日志格式

描述: 此处是使用 gin.LoggerWithFormatter & gin.LogFormatterParams 实现自定义路由访问日志。

代码示例:

package main

import (
  "fmt"
  "io"
  "os"
  "time"

  "github.com/gin-gonic/gin"
)

func main() {
  // 强制日志颜色化
  gin.ForceConsoleColor()
  // 禁用控制台颜色
  // gin.DisableConsoleColor()

  // 默认为 debug 模式,设置为发布模式
  gin.SetMode(gin.ReleaseMode)

  // 将日志同时写入文件和控制台,请使用以下代码 f 表示文件,os,os.Stdout 表示终端
  f, _ := os.Create("gin.log")
  // 如果需要同时将日志写入文件和控制台,请使用以下代码。
  gin.DefaultWriter = io.MultiWriter(f, os.Stdout)

  // 生成gin实例,即 WSGI 应用程序
  r := gin.New()

  // 自定义日志格式
  // LoggerWithFormatter 中间件会将日志写入 gin.DefaultWriter
  // By default =>  gin.DefaultWriter = os.Stdout
  r.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
    // 自定义格式
    return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n",
      param.ClientIP,
      param.TimeStamp.Format(time.RFC1123),
      param.Method,
      param.Path,
      param.Request.Proto,
      param.StatusCode,
      param.Latency,
      param.Request.UserAgent(),
      param.ErrorMessage,
    )
  }))
  r.Use(gin.Recovery())

  // 声明了一个路由及对应的处理函数 (匿名函数)
  r.GET("/log", func(c *gin.Context) {
    c.JSON(200, gin.H{
      "code": "200", "msg": "Test gin logs", "data": "",
    })
  })
  r.Run()
}

执行效果:
46191b2069aceb2a187016e4ccc5a3b2.png

3.使用 log 自定义日志并按天分隔保存到文件

描述: 此处使用原生的 log 模块实现自定义路由日志,并载入到Gin的日志中间件中,实现终端与文件同时输出,输入的日志的文件按照天进行分隔,好了,废话不多说直接上代码:

日志中间件: middleware\Logger.go

package middleware

import (
	"fmt"
	"io"
	"log"
	"os"
	"time"

	"github.com/gin-gonic/gin"
)

// Logger 是一个自定义的日志中间件
func Logger() gin.HandlerFunc {
	// 日志文件路径
	logFilePath := "./logs/"
	// 日志文件名前缀
	logFileName := "weiyigeek"
	// 日志文件后缀
	logFileExt := "log"
	// 日志文件最大大小,单位为 MB
	logFileMaxSize := 1000
	// 日志文件切割的时间间隔,单位为天
	logFileSplitDays := 1

	// 检查日志目录是否存在,不存在则创建
	err := os.MkdirAll(logFilePath, os.ModePerm)
	if err != nil {
		log.Fatalf("Failed to create log directory: %v", err)
	}

	// 获取当前时间的年月日
	now := time.Now()
	year, month, day := now.Date()

	// 构造日志文件名
	logFileName = fmt.Sprintf("%s-%d-%02d-%02d", logFileName, year, month, day)

	// 打开日志文件
	logFile, err := os.OpenFile(
		fmt.Sprintf("%s/%s.%s", logFilePath, logFileName, logFileExt),
		os.O_WRONLY|os.O_APPEND|os.O_CREATE,
		0666,
	)
	if err != nil {
		log.Fatalf("Failed to open log file: %v", err)
	}

	// 设置日志输出
	writers := []io.Writer{
		logFile,
		os.Stdout}
	log.SetOutput(io.MultiWriter(writers...))
	log.SetFlags(log.Ldate | log.Ltime | log.LUTC)

	return func(c *gin.Context) {
		// 处理请求前记录日志
		// 开始时间
		startTime := time.Now()
		// 调用该请求的剩余处理程序
		c.Next()
		// 结束时间
		endTime := time.Now()
		// 执行时间
		latencyTime := endTime.Sub(startTime)

		// 请求IP
		clientIP := c.ClientIP()
		// remoteIP := c.RemoteIP()

		// 请求方式
		reqMethod := c.Request.Method
		// 请求路由
		reqUri := c.Request.RequestURI
		// 请求协议
		reqProto := c.Request.Proto
		// 请求来源
		repReferer := c.Request.Referer()
		// 请求UA
		reqUA := c.Request.UserAgent()

		// 请求响应内容长度
		resLength := c.Writer.Size()
		if resLength < 0 {
			resLength = 0
		}
		// 响应状态码
		statusCode := c.Writer.Status()

		log.Printf(
			"%s | %3d | %s %10s | \033[44;37m%-6s\033[0m %s %s  | %10v | \"%s\" \"%s\"",
			colorForStatus(statusCode),
			statusCode,
			colorForStatus(0),
			clientIP,
			// remoteIP,
			reqMethod,
			reqUri,
			reqProto,
			latencyTime,
			reqUA,
			repReferer,
		)

		// 判断日志文件是否需要切割
		fileInfo, err := logFile.Stat()
		if err != nil {
			log.Fatalf("Failed to get log file info: %v", err)
		}
		_, _, lastDay := endTime.AddDate(0, 0, -1*logFileSplitDays).Date()
		if fileInfo.Size() > int64(logFileMaxSize*1024*1024) {
			// 关闭当前日志文件
			logFile.Close()
			// 构造新的日志文件名
			logFileName = fmt.Sprintf("%s-%s", logFileName, time.Now().Format("2006-01-02-15"))

			// 创建新的日志文件
			logFile, err = os.OpenFile(
				fmt.Sprintf("%s/%s.%s", logFilePath, logFileName, logFileExt),
				os.O_WRONLY|os.O_APPEND|os.O_CREATE,
				0666,
			)
			if err != nil {
				log.Fatalf("Failed to create log file: %v", err)
			}
			// 设置日志输出
			log.SetOutput(logFile)
			log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
		} else if fileInfo.ModTime().Day() == lastDay {
			// 关闭当前日志文件
			logFile.Close()

			// 构造新的日志文件名
			logFileName = fmt.Sprintf("%s-%s", logFileName, endTime.Format("2006-01-02"))

			// 创建新的日志文件
			logFile, err = os.OpenFile(
				fmt.Sprintf("%s/%s.%s", logFilePath, logFileName, logFileExt),
				os.O_WRONLY|os.O_APPEND|os.O_CREATE,
				0666,
			)
			if err != nil {
				log.Fatalf("Failed to create log file: %v", err)
			}
			// 设置日志输出
			log.SetOutput(logFile)
			log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
		}
	}
}

// colorForStatus 根据 HTTP 状态码返回 ANSI 颜色代码
func colorForStatus(code int) string {
	switch {
	case code >= 200 && code < 300:
		return "\033[42;1;37m" // green
	case code >= 300 && code < 400:
		return "\033[34m" // blue
	case code >= 400 && code < 500:
		return "\033[33m" // yellow
	case code == 0:
		return "\033[0m" // cancel
	default:
		return "\033[31m" // red
	}
}

亲,文章就要看完了,不关注一下【全栈工程师修炼指南】吗?

入口文件: main.go

package main

import (
	"devopsapi/middleware"
	router "devopsapi/routers"
	"fmt"
	"log"
	"net/http"
	"time"

	"github.com/gin-gonic/gin"
	"golang.org/x/sync/errgroup"
)

// 处理属于同一总体任务的子任务的goroutine的集合
var (
	g errgroup.Group
)

func main() {
	// 指定 gin 运行模式
	gin.SetMode(global.App.Mode)

	// 返回一个新的空白Engine实例
	r := gin.New()

	// 设置日志中间件
	r.Use(middleware.Logger())

	// 加载自定义路由
	router.Load(r)

	// Linux、Mac 环境下使用 fvbock/endless 艰辛平滑重启
	// err := endless.ListenAndServe(fmt.Sprintf("%s:%d", global.App.Host, global.App.Port), r)
	// if err != nil || err != http.ErrServerClosed {
	// 	log.Println("err:", err)
	// }

	// W通用:开放监听运行Gin服务
	server := &http.Server{
		// Gin运行的监听端口
		Addr: ":8080",
		// 要调用的处理程序,http.DefaultServeMux如果为nil
		Handler: r,
		// ReadTimeout是读取整个请求(包括正文)的最长持续时间。
		ReadTimeout: 5 * time.Second,
		// WriteTimeout是超时写入响应之前的最长持续时间
		WriteTimeout: 10 * time.Second,
		// MaxHeaderBytes控制服务器解析请求标头的键和值(包括请求行)时读取的最大字节数 (通常情况下不进行设置)
		MaxHeaderBytes: 1 << 20,
	}

	// 创建 goroutine 中调用给定的函数
	g.Go(func() error {
		return server.ListenAndServe()
	})
  
  // goroutine 所有函数调用都返回,然后从中返回第一个非零错误(如果有的话)。
	if err := g.Wait(); err != nil {
		log.Fatal(err)
	}
}

执行结果:
日志实现格式: 2023/06/09 09:59:04 [42;1;37m | 200 | [0m 10.20.172.103 | [44;37mGET [0m /app/version HTTP/1.1 | 144.4µs | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36 Edg/113.0.1774.57" ""
5c1339cc387a4d6ed48f95dfbe1fdbdc.png

本文至此完毕,更多技术文章,尽情等待下篇好文!

原文地址: https://blog.weiyigeek.top/2023/6-2-745.html

如果此篇文章对你有帮助,请你将它分享给更多的人! 

06c5016b3c7abe2f68e09f0e1c10e523.gif

b170e278a18bbef9ffc999df98b935ca.png 学习书籍推荐 往期发布文章 b7f0ff7b0cd2bda1dc6c7c0cb1dbdedf.png

公众号回复【0008】获取【Ubuntu22.04安装与加固脚本】

公众号回复【10001】获取【WinServer安全加固脚本】

公众号回复【10002】获取【KylinOS银河麒麟安全加固脚本】

公众号回复【0011】获取【k8S二进制安装部署教程】

公众号回复【0014】获取【Nginx学习之路汇总】

公众号回复【0015】获取【Jenkins学习之路汇总】

公众号回复【10005】获取【adb工具刷抖音赚米】

 热文推荐  

欢迎长按(扫描)二维码 取更多渠道哟!

79e45baccb4ad8627618b14f43de35a7.gif

欢迎关注 【全栈工程师修炼指南】(^U^)ノ~YO

添加作者微信【weiyigeeker 】 一起学习交流吧!

关注回复【学习交流群】即可加入【安全运维沟通交流小群

温馨提示: 由于作者水平有限,本章错漏缺点在所难免,希望读者批评指正,若有问题或建议请在文章末尾留下您宝贵的经验知识,或联系邮箱地址

master@weiyigeek.top 或 关注公众号 [全栈工程师修炼指南] 留言。

点个【赞 + 在看】吧!

点击【"阅读原文"】获取更多有趣的知识!   

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

全栈工程师修炼指南

原创不易,赞赏鼓励!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值