Zap介绍
ZAP(Zed Attack Proxy)是一种开源的网络应用程序安全扫描工具,旨在帮助发现和解决Web应用程序中的安全漏洞。以下是关于ZAP的介绍:
一个好的项目日志系统是必不可少的。
Zap的日志功能
- 记录活动:ZAP日志记录了ZAP工具的各种活动,包括扫描过程中发现的漏洞、HTTP请求和响应、插件运行情况等。
- 调试信息:日志还可以用于记录调试信息,有助于了解ZAP的内部运行情况,识别问题和优化性能。
- 级别设置:ZAP日志通常支持多个日志级别,如调试、信息、警告和错误。用户可以根据需要设置不同级别的日志详细程度。
- 可配置性:用户通常可以配置日志的输出位置(如控制台、文件)、日志格式、日志滚动策略等,以满足不同的需求和偏好。
- 安全性:由于日志可能包含敏感信息,如用户凭证、敏感数据等,ZAP通常会提供安全措施,如日志加密、访问控制等来保护日志的安全性。
使用日志功能的好处:
- 故障排查:日志记录可帮助用户追踪和诊断问题,找出导致错误或异常行为的原因。
- 性能优化:通过分析日志,可以发现性能瓶颈,优化ZAP的配置和运行方式。
- 合规性:在一些情况下,需要记录ZAP的活动以符合合规性要求,如在安全审计中需要提供详细的操作日志。
- 学习和培训:ZAP的日志可以用于培训新用户,了解ZAP的功能和操作流程。
- 报告生成:日志记录的信息可以用于生成详细的报告,包括漏洞报告、扫描结果等。
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()
}
}