Zap熟练使用一篇学会

Zap介绍

ZAP(Zed Attack Proxy)是一种开源的网络应用程序安全扫描工具,旨在帮助发现和解决Web应用程序中的安全漏洞。以下是关于ZAP的介绍:

一个好的项目日志系统是必不可少的。

Zap的日志功能

  1. 记录活动:ZAP日志记录了ZAP工具的各种活动,包括扫描过程中发现的漏洞、HTTP请求和响应、插件运行情况等。
  2. 调试信息:日志还可以用于记录调试信息,有助于了解ZAP的内部运行情况,识别问题和优化性能。
  3. 级别设置:ZAP日志通常支持多个日志级别,如调试、信息、警告和错误。用户可以根据需要设置不同级别的日志详细程度。
  4. 可配置性:用户通常可以配置日志的输出位置(如控制台、文件)、日志格式、日志滚动策略等,以满足不同的需求和偏好。
  5. 安全性:由于日志可能包含敏感信息,如用户凭证、敏感数据等,ZAP通常会提供安全措施,如日志加密、访问控制等来保护日志的安全性。

使用日志功能的好处:

  1. 故障排查:日志记录可帮助用户追踪和诊断问题,找出导致错误或异常行为的原因。
  2. 性能优化:通过分析日志,可以发现性能瓶颈,优化ZAP的配置和运行方式。
  3. 合规性:在一些情况下,需要记录ZAP的活动以符合合规性要求,如在安全审计中需要提供详细的操作日志。
  4. 学习和培训:ZAP的日志可以用于培训新用户,了解ZAP的功能和操作流程。
  5. 报告生成:日志记录的信息可以用于生成详细的报告,包括漏洞报告、扫描结果等。

Zap的基本使用

在使用Zap之前首先拉取一下Zap,同时配合gin和lumberjack使用,gin是go语言中 Web 框架。lumberjack 是一个 Go 语言的库,它提供了简单的轮转日志文件的功能,配合Zap以实现日志的轮转和压缩。

go get -u github.com/gin-gonic/gin
go get -u go.uber.org/zap
go get -u gopkg.in/natefinch/lumberjack.v2 

没有报错,然后go.mod文件里面有依赖说明拉取成功。

首先一个包,创建log.go把这个日志实现成一个接口,之后让我们的web程序都能调用它

zap分多个日志级别

Error级别的日志,说明日志是错误类型,在排障时,会首先查看错误级别的日志。Warn级别日志说明出现异常,但还不至于影响程序运行,如果程序执行的结果不符合预期,则可以参考Warn级别的日志,定位出异常所在。Info级别的日志,可以协助我们Debug,并记录一些有用的信息,供后期进行分析。

Fatal > Panic > Error > Warn > Info > Debug > Trace

启动应用程序时,期望哪些输出级别的日志被打印。例如,使用日志级别是info ,说明了只有日志级别高于info的日志才会被打印。

zap多个日志输出位置

通过zap的zapcore.NewMultiWriteSynce()还可以设定将日志输入到多个位置,比如zapcore.NewMultiWriteSyncer(zapcore.AddSync(os.Stdout), zapcore.AddSync(&hook)), 将日志同时输出到控制台和文件(hook结构体有配置日志文件的存储)

zap多个日志输出格式

通过zap的zapcore.NewConsoleEncoder和zapcore.NewJSONEncoder可以实现让日志是生成TEXT格式格式还是JSON格式。

  • TEXT格式:TEXT格式的日志具有良好的可读性,可以方便我们在开发联调阶段查看日志,例如:

2020-12-02T01:16:18+08:00 INFO example.go:11 std log

  • JSON格式:JSON格式的日志可以记录更详细的信息,日志中包含一些通用的或自定义的字段,可供日后的查询、分析使用,而且可以很方便地供filebeat、logstash这类日志采集工具采集并上报。下面是JSON格式的日志:
{"level":"DEBUG","time":"2020-12-02T01:16:18+08:00","file":"example.go:15","func":"main.main","message":"log in json format"}
{"level":"INFO","time":"2020-12-02T01:16:18+08:00","file":"example.go:16","func":"main.main","message":"an
zap在一次请求中支持RequestID

通过配合gin的中间件使用,可以让zap支持RequestID:使用RequestID串联一次请求的所有日志,这些日志可能分布在不同的组件,不同的机器上。支持RequestID可以大大提高排障的效率,降低排障难度。在一些大型分布式系统中,没有RequestID排障简直就是灾难。意思就是让一次请求中产生的日志都有同样的RequestID字段,便于搜索

{"level":"info","ts":1724831082.265542,"caller":"useZap/zapMain.go:57","msg":"Incoming request","RequestID":1828700161077940224,"RequestID":1828700218124668928,"Path":"/"}
{"level":"info","ts":1724831082.2659457,"caller":"useZap/zapMain.go:33","msg":"test request1","RequestID":1828700161077940224,"RequestID":1828700218124668928,"Path":"/"}
{"level":"info","ts":1724831082.2659457,"caller":"useZap/zapMain.go:35","msg":"test request2","RequestID":1828700161077940224,"RequestID":1828700218124668928,"Path":"/"}
{"level":"info","ts":1724831082.2659457,"caller":"useZap/zapMain.go:74","msg":"HTTP Request","RequestID":1828700161077940224,"RequestID":1828700218124668928,"method":"GET","path":"/","status":"200","duration":"0s"}
{"level":"info","ts":1724831082.2659457,"caller":"useZap/zapMain.go:61","msg":"Request completed","RequestID":1828700161077940224,"RequestID":1828700218124668928,"Path":"/"}

代码

在项目下创建pkg/log.go文件,相关代码的解释,注释已经放上了,即粘即用。

package log

import (
	"context"
	"github.com/gin-gonic/gin"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
	"gopkg.in/natefinch/lumberjack.v2"
	"time"
)

const ctxLoggerKey = "zapLogger"

type Logger struct {
	*zap.Logger
}

func NewLog() *Logger {
	// log address "out.log" User-defined
	lp := "./storage/logs/server.log"
	lv := "debug"
	var level zapcore.Level
	//debug<info<warn<error<fatal<panic
	switch lv {
	case "debug":
		level = zap.DebugLevel
	case "info":
		level = zap.InfoLevel
	case "warn":
		level = zap.WarnLevel
	case "error":
		level = zap.ErrorLevel
	default:
		level = zap.InfoLevel
	}
	//创建了一个 lumberjack.Logger 实例,用于配置日志文件的存储。
	hook := lumberjack.Logger{
		Filename:   lp,   // Log file path 日志文件的路径
		MaxSize:    1024, // Maximum size unit for each log file: M 每个日志最大大小
		MaxBackups: 30,   // The maximum number of backups that can be saved for log files 可以保存的日志文件数量
		MaxAge:     7,    // Maximum number of days the file can be saved 日志文件保存的最大天数
		Compress:   true, // Compression or not 是否对日志文件进行压缩
	}

	//encoder:这是一个 zapcore.Encoder 实例,用于定义日志的格式。
	//定义一个打印格式 json还是console
	console := "json"
	//console := "console"
	var encoder zapcore.Encoder
	if console == "console" {
		encoder = zapcore.NewConsoleEncoder(zapcore.EncoderConfig{
			TimeKey:        "ts",
			LevelKey:       "level",
			NameKey:        "Logger",
			CallerKey:      "caller",
			MessageKey:     "msg",
			StacktraceKey:  "stacktrace",
			LineEnding:     zapcore.DefaultLineEnding,
			EncodeLevel:    zapcore.LowercaseColorLevelEncoder,
			EncodeTime:     timeEncoder,
			EncodeDuration: zapcore.SecondsDurationEncoder,
			EncodeCaller:   zapcore.FullCallerEncoder,
		})
	} else {
		encoder = zapcore.NewJSONEncoder(zapcore.EncoderConfig{
			TimeKey:        "ts",
			LevelKey:       "level",
			NameKey:        "logger",
			CallerKey:      "caller",
			FunctionKey:    zapcore.OmitKey,
			MessageKey:     "msg",
			StacktraceKey:  "stacktrace",
			LineEnding:     zapcore.DefaultLineEnding,
			EncodeLevel:    zapcore.LowercaseLevelEncoder,
			EncodeTime:     zapcore.EpochTimeEncoder,
			EncodeDuration: zapcore.SecondsDurationEncoder,
			EncodeCaller:   zapcore.ShortCallerEncoder,
		})
	}
	core := zapcore.NewCore(
		encoder,
		//通过(NewMultiWriteSyncer)它将日志输出到多个目标。
		//zapcore.NewMultiWriteSyncer(zapcore.AddSync(os.Stdout), zapcore.AddSync(&hook)), // 同时讲日志输出到控制台和文件
		zapcore.NewMultiWriteSyncer(zapcore.AddSync(&hook)), // Print to file
		level,
	)
	//定义一个环境生产环境还是开发环境
	env := "prod"
	//env := "local"
	if env != "prod" {
		// 开发模式下 Zap 会记录更详细的日志信息,包括调用者信息和错误级别的堆栈跟踪
		return &Logger{zap.New(core, zap.Development(), zap.AddCaller(), zap.AddStacktrace(zap.ErrorLevel))}
	}
	// 如果是生产环境,创建一个 Logger 实例,仅记录调用者信息和错误级别的堆栈跟踪
	return &Logger{zap.New(core, zap.AddCaller(), zap.AddStacktrace(zap.ErrorLevel))}
}

// timeEncoder 格式化一下当前实际
func timeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
	//enc.AppendString(t.Format("2006-01-02 15:04:05"))
	enc.AppendString(t.Format("2006-01-02 15:04:05.000000000"))
}

// WithValue 将 Zap 日志字段和当前的 context.Context 绑定,以便在 HTTP 请求处理过程中可以方便地记录日志。
func (l *Logger) WithValue(ctx context.Context, fields ...zapcore.Field) context.Context {
	// 检查传入的 context 是否为 *gin.Context 类型
	if c, ok := ctx.(*gin.Context); ok {
		// 将 gin.Context 转换为标准的 context.Context
		ctx = c.Request.Context()
		// 使用 context.WithValue 将 Zap 日志字段绑定到 context
		// 这样在请求处理过程中就可以通过 context 获取日志记录器实例
		c.Request = c.Request.WithContext(context.WithValue(ctx, ctxLoggerKey, l.WithContext(ctx).With(fields...)))
		return c
	}
	// 如果 context 不是 *gin.Context 类型,直接绑定 Zap 日志字段
	return context.WithValue(ctx, ctxLoggerKey, l.WithContext(ctx).With(fields...))
}

// WithContext Returns a zap instance from the specified context
func (l *Logger) WithContext(ctx context.Context) *Logger {
	//检查传入的 context 是否为 *gin.Context 类型
	//将 gin.Context 转换为标准的 context.Context
	if c, ok := ctx.(*gin.Context); ok {
		ctx = c.Request.Context()
	}
	//从上下文中提取键为ctxLoggerKey的值,并尝试将其转换为*zap.Logger。
	zl := ctx.Value(ctxLoggerKey)
	ctxLogger, ok := zl.(*zap.Logger)
	if ok {
		return &Logger{ctxLogger}
	}
	return l
}

创建storage/logs/server.log 用来存储日志

在main里面创建一个路由,同时把zap的功能通过中间件放进去

package main

import (
	"bytes"
	"fmt"
	"github.com/bwmarrin/snowflake"
	"github.com/duke-git/lancet/v2/cryptor"
	"github.com/duke-git/lancet/v2/random"
	"github.com/gin-gonic/gin"
	"go.uber.org/zap"
	"io"
	"net/http"
	"os"
	"strconv"
	"time"
	log "useZap/pkg"
)

func main() {
	// 设置 Gin 模式为 Debug 模式
	//Gin 框架有三种运行模式:DebugMode、ReleaseMode 和 TestMode。在 DebugMode 下,Gin 会提供更多的调试信息,
	gin.SetMode(gin.DebugMode)

	// 初始化 Gin 引擎
	r := gin.Default()
	//r := gin.New()
	f, _ := os.Create("gin.log")
	gin.DefaultWriter = f // 将日志输出到文件
	logger := log.NewLog()

	// 添加 Zap 中间件
	r.Use(generateRequestID(logger),
		zapMiddleware(logger),
		RequestLogMiddleware(logger))

	// 设置路由
	r.GET("/", func(c *gin.Context) {
		logger.Info("test request1", zap.String("Path", c.Request.URL.Path))
		logger.WithValue(c, zap.String("request_url", c.Request.URL.String()))
		logger.Info("test request2", zap.String("Path", c.Request.URL.Path))
		c.JSON(http.StatusOK, gin.H{"message": "Hello, World!"})
	})

	// 启动服务
	r.Run(":8080")

}

var node *snowflake.Node

func init() {
	var err error
	node, err = snowflake.NewNode(1) // 1 是机器 ID
	if err != nil {
		fmt.Printf("Error creating snowflake node: %v", err)
	}
}

func generateRequestID(logger *log.Logger) gin.HandlerFunc {
	return func(c *gin.Context) {
		requestID := node.Generate().Int64()
		logger.Info("Incoming request", zap.Any("RequestID", requestID), zap.String("Path", c.Request.URL.Path))
		log := logger.With(zap.Any("RequestID", requestID))
		logger.Logger = log
		c.Next()
		logger.Info("Request completed", zap.String("Path", c.Request.URL.Path))
	}
}

// zapMiddleware 记录每个请求的基本信息
func zapMiddleware(zapLogger *log.Logger) gin.HandlerFunc {
	return func(c *gin.Context) {
		// 请求开始时间
		start := time.Now()

		// 处理请求
		c.Next()

		// 记录请求信息
		zapLogger.Info("HTTP Request",
			zap.String("method", c.Request.Method),
			zap.String("path", c.Request.URL.Path),
			zap.String("status", strconv.Itoa(c.Writer.Status())),
			zap.String("duration", time.Since(start).String()),
		)
	}
}

// RequestLogMiddleware 使用WithValuef方法,将每个请求的请求ID、请求方法、请求路径、请求参数等信息记录到日志中
// 也是将请求的ctx给绑定到日志的ctx中 这样子,在日志中就可以直接看到请求的请求ID等信息了
// 之后在多个层中的日志对象用到的ctx就会是同一个了
func RequestLogMiddleware(logger *log.Logger) gin.HandlerFunc {
	return func(ctx *gin.Context) {
		// The configuration is initialized once per request
		uuid, err := random.UUIdV4()
		if err != nil {
			return
		}
		trace := cryptor.Md5String(uuid)
		logger.WithValue(ctx, zap.String("trace", trace))
		logger.WithValue(ctx, zap.String("request_method", ctx.Request.Method))
		logger.WithValue(ctx, zap.Any("request_headers", ctx.Request.Header))
		logger.WithValue(ctx, zap.String("request_url", ctx.Request.URL.String()))
		if ctx.Request.Body != nil {
			bodyBytes, _ := ctx.GetRawData()
			ctx.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) // 关键点
			logger.WithValue(ctx, zap.String("request_params", string(bodyBytes)))
		}
		logger.WithContext(ctx).Info("Request")
		ctx.Next()
	}
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值