diff --git a/server/constants.go b/server/constants.go index d7e0585..1df8385 100644 --- a/server/constants.go +++ b/server/constants.go @@ -1,6 +1,19 @@ package server -import "time" +import ( + "github.com/kercylan98/minotaur/utils/log" + "time" +) + +type ( + RunMode = log.RunMode +) + +const ( + RunModeDev RunMode = log.RunModeDev + RunModeProd RunMode = log.RunModeProd + RunModeTest RunMode = log.RunModeTest +) const ( serverMultipleMark = "Minotaur Multiple Server" diff --git a/server/message.go b/server/message.go index 293991d..f9a098e 100644 --- a/server/message.go +++ b/server/message.go @@ -3,9 +3,7 @@ package server import ( "encoding/json" "fmt" - "github.com/kercylan98/minotaur/utils/str" "reflect" - "runtime/debug" ) const ( @@ -69,11 +67,14 @@ func (slf *Message) String() string { } attrs = append(attrs, attr) } - raw, _ := json.Marshal(attrs) - s := string(raw) - if s == str.None { + var s string + if len(slf.attrs) == 0 { s = "NoneAttr" + } else { + raw, _ := json.Marshal(attrs) + s = string(raw) } + return fmt.Sprintf("[%s] %s", slf.t, s) } @@ -93,7 +94,7 @@ func PushPacketMessage(srv *Server, conn *Conn, packet []byte, mark ...any) { func PushErrorMessage(srv *Server, err error, action MessageErrorAction, mark ...any) { msg := srv.messagePool.Get() msg.t = MessageTypeError - msg.attrs = append([]any{err, action, string(debug.Stack())}, mark...) + msg.attrs = append([]any{err, action}, mark...) srv.pushMessage(msg) } @@ -128,6 +129,6 @@ func PushTickerMessage(srv *Server, caller func(), mark ...any) { func PushAsyncMessage(srv *Server, caller func() error, callback func(err error), mark ...any) { msg := srv.messagePool.Get() msg.t = MessageTypeAsync - msg.attrs = append([]any{caller, callback, string(debug.Stack())}, mark...) + msg.attrs = append([]any{caller, callback}, mark...) srv.pushMessage(msg) } diff --git a/server/options.go b/server/options.go index 15b5166..a7ad9d6 100644 --- a/server/options.go +++ b/server/options.go @@ -37,7 +37,6 @@ type runtime struct { certFile, keyFile string // TLS文件 messagePoolSize int // 消息池大小 messageChannelSize int // 消息通道大小 - prod bool // 是否为生产模式 ticker *timer.Ticker // 定时器 websocketReadDeadline time.Duration // websocket连接超时时间 websocketCompression int // websocket压缩等级 @@ -184,10 +183,11 @@ func WithGRPCServerOptions(options ...grpc.ServerOption) Option { } } -// WithProd 通过生产模式运行服务器 -func WithProd() Option { +// WithRunMode 通过特定模式运行服务器 +// - 默认为 RunModeDev +func WithRunMode(mode RunMode) Option { return func(srv *Server) { - srv.prod = true + srv.runMode = mode } } diff --git a/server/server.go b/server/server.go index 2365a54..3bc658b 100644 --- a/server/server.go +++ b/server/server.go @@ -60,7 +60,7 @@ func New(network Network, options ...Option) *Server { server.antsPoolSize = DefaultAsyncPoolSize } var err error - server.ants, err = ants.NewPool(server.antsPoolSize, ants.WithLogger(log.Logger())) + server.ants, err = ants.NewPool(server.antsPoolSize, ants.WithLogger(log.GetLogger())) if err != nil { panic(err) } @@ -91,6 +91,7 @@ type Server struct { messageChannel chan *Message // 消息管道 multiple *MultipleServer // 多服务器模式下的服务器 multipleRuntimeErrorChan chan error // 多服务器模式下的运行时错误 + runMode RunMode // 运行模式 } // Run 使用特定地址运行服务器 @@ -156,8 +157,8 @@ func (slf *Server) Run(addr string) error { slf.isRunning = true slf.OnStartBeforeEvent() if err := gnet.Serve(slf.gServer, protoAddr, - gnet.WithLogger(log.Logger().Sugar()), - gnet.WithLogLevel(super.If(slf.IsProd(), logging.ErrorLevel, logging.DebugLevel)), + gnet.WithLogger(log.GetLogger()), + gnet.WithLogLevel(super.If(slf.runMode == RunModeProd, logging.ErrorLevel, logging.DebugLevel)), gnet.WithTicker(true), gnet.WithMulticore(true), ); err != nil { @@ -201,8 +202,12 @@ func (slf *Server) Run(addr string) error { } }) case NetworkHttp: - if slf.prod { - log.SetProd(slf.prod) + switch slf.runMode { + case RunModeDev: + gin.SetMode(gin.DebugMode) + case RunModeTest: + gin.SetMode(gin.TestMode) + case RunModeProd: gin.SetMode(gin.ReleaseMode) } go func() { @@ -359,16 +364,6 @@ func (slf *Server) CloseConn(id string) { } } -// IsProd 是否为生产模式 -func (slf *Server) IsProd() bool { - return slf.prod -} - -// IsDev 是否为开发模式 -func (slf *Server) IsDev() bool { - return !slf.prod -} - // GetID 获取服务器id func (slf *Server) GetID() int64 { if slf.cross == nil { @@ -391,7 +386,7 @@ func (slf *Server) Shutdown() { } // shutdown 停止运行服务器 -func (slf *Server) shutdown(err error, stack ...string) { +func (slf *Server) shutdown(err error) { slf.OnStopEvent() defer func() { if slf.multipleRuntimeErrorChan != nil { @@ -426,13 +421,9 @@ func (slf *Server) shutdown(err error, stack ...string) { } if err != nil { - var s string - if len(stack) > 0 { - s = stack[0] - } if slf.multiple != nil { slf.multiple.RegExitEvent(func() { - log.ErrorWithStack("Server", s, zap.Any("network", slf.network), zap.String("listen", slf.addr), + log.Panic("Server", zap.Any("network", slf.network), zap.String("listen", slf.addr), zap.String("action", "shutdown"), zap.String("state", "exception"), zap.Error(err)) }) for i, server := range slf.multiple.servers { @@ -442,7 +433,7 @@ func (slf *Server) shutdown(err error, stack ...string) { } } } else { - log.ErrorWithStack("Server", s, zap.Any("network", slf.network), zap.String("listen", slf.addr), + log.Panic("Server", zap.Any("network", slf.network), zap.String("listen", slf.addr), zap.String("action", "shutdown"), zap.String("state", "exception"), zap.Error(err)) } } else { @@ -482,7 +473,7 @@ func (slf *Server) pushMessage(message *Message) { func (slf *Server) low(message *Message, present time.Time, expect time.Duration) { cost := time.Since(present) if cost > expect { - log.Warn("Server", zap.String("LowExecCost", cost.String()), zap.Any("Message", message)) + log.Warn("Server", zap.String("type", "low-message"), zap.String("cost", cost.String()), zap.String("message", message.String()), zap.Stack("stack")) slf.OnMessageLowExecEvent(message, cost) } } @@ -508,18 +499,21 @@ func (slf *Server) dispatchMessage(msg *Message) { present := time.Now() defer func() { if err := recover(); err != nil { - log.Error("Server", zap.String("MessageType", messageNames[msg.t]), zap.Any("MessageAttrs", msg.attrs), zap.Any("error", err)) + log.Error("Server", zap.String("MessageType", messageNames[msg.t]), zap.Any("MessageAttrs", msg.attrs), zap.Any("error", err), zap.Stack("stack")) if e, ok := err.(error); ok { slf.OnMessageErrorEvent(msg, e) } } - if msg.t != MessageTypeAsync { - super.Handle(cancel) - slf.low(msg, present, time.Millisecond*100) - if !slf.isShutdown.Load() { - slf.messagePool.Release(msg) - } + if msg.t == MessageTypeAsync { + return + } + + super.Handle(cancel) + slf.low(msg, present, time.Millisecond*100) + + if !slf.isShutdown.Load() { + slf.messagePool.Release(msg) } }() @@ -530,12 +524,12 @@ func (slf *Server) dispatchMessage(msg *Message) { var wst = int(packet[len(packet)-1]) slf.OnConnectionReceivePacketEvent(attrs[0].(*Conn), Packet{Data: packet[:len(packet)-1], WebsocketType: wst}) case MessageTypeError: - err, action, stack := attrs[0].(error), attrs[1].(MessageErrorAction), attrs[2].(string) + err, action := attrs[0].(error), attrs[1].(MessageErrorAction) switch action { case MessageErrorActionNone: - log.ErrorWithStack("Server", stack, zap.Error(err)) + log.Panic("Server", zap.Error(err)) case MessageErrorActionShutdown: - slf.shutdown(err, stack) + slf.shutdown(err) default: log.Warn("Server", zap.String("not support message error action", action.String())) } @@ -549,13 +543,14 @@ func (slf *Server) dispatchMessage(msg *Message) { if err := slf.ants.Submit(func() { defer func() { if err := recover(); err != nil { - log.Error("Server", zap.String("MessageType", messageNames[msg.t]), zap.Any("MessageAttrs", msg.attrs), zap.Any("error", err)) + log.Error("Server", zap.String("MessageType", messageNames[msg.t]), zap.Any("error", err), zap.Stack("stack")) if e, ok := err.(error); ok { slf.OnMessageErrorEvent(msg, e) } } super.Handle(cancel) slf.low(msg, present, time.Second) + if !slf.isShutdown.Load() { slf.messagePool.Release(msg) } diff --git a/utils/hash/convert.go b/utils/hash/convert.go index a12a7e5..35113ff 100644 --- a/utils/hash/convert.go +++ b/utils/hash/convert.go @@ -36,6 +36,15 @@ func ToMap[V any](slice []V) map[int]V { return m } +// ToIterator 将切片转换为 Iterator +func ToIterator[V comparable](slice []V) map[V]struct{} { + var m = make(map[V]struct{}) + for _, v := range slice { + m[v] = struct{}{} + } + return m +} + // ToMapBool 将切片转换为 map,value作为Key func ToMapBool[V comparable](slice []V) map[V]bool { var m = make(map[V]bool) diff --git a/utils/log/core.go b/utils/log/core.go new file mode 100644 index 0000000..5d8965e --- /dev/null +++ b/utils/log/core.go @@ -0,0 +1,5 @@ +package log + +import "go.uber.org/zap/zapcore" + +type Core = zapcore.Core diff --git a/utils/log/encoder.go b/utils/log/encoder.go new file mode 100644 index 0000000..c7d5570 --- /dev/null +++ b/utils/log/encoder.go @@ -0,0 +1,26 @@ +package log + +import ( + "go.uber.org/zap/zapcore" + "time" +) + +type Encoder = zapcore.Encoder + +// NewEncoder 创建一个 Minotaur 默认使用的编码器 +func NewEncoder() Encoder { + return zapcore.NewConsoleEncoder(zapcore.EncoderConfig{ + MessageKey: "msg", + LevelKey: "level", + EncodeLevel: zapcore.CapitalLevelEncoder, + TimeKey: "ts", + EncodeTime: func(t time.Time, enc zapcore.PrimitiveArrayEncoder) { + enc.AppendString(t.Format(time.DateTime)) + }, + CallerKey: "file", + EncodeCaller: zapcore.ShortCallerEncoder, + EncodeDuration: func(d time.Duration, enc zapcore.PrimitiveArrayEncoder) { + enc.AppendInt64(int64(d) / 1000000) + }, + }) +} diff --git a/utils/log/field.go b/utils/log/field.go new file mode 100644 index 0000000..5919e66 --- /dev/null +++ b/utils/log/field.go @@ -0,0 +1,159 @@ +package log + +import ( + "go.uber.org/zap" +) + +type Field = zap.Field + +var ( + // Skip 构造一个无操作字段,这在处理其他 Field 构造函数中的无效输入时通常很有用 + Skip = zap.Skip + + // Binary 构造一个携带不透明二进制 blob 的字段。二进制数据以适合编码的格式进行序列化。例如,JSON 编码器对二进制 blob 进行 base64 编码。要记录 UTF-8 编码文本,请使用 ByteString + Binary = zap.Binary + + // Bool 构造一个带有布尔值的字段 + Bool = zap.Bool + + // BoolP 构造一个带有布尔值的字段。返回的 Field 将在适当的时候安全且显式地表示“nil” + BoolP = zap.Boolp + + // ByteString 构造一个将 UTF-8 编码文本作为 [] 字节传送的字段。要记录不透明的二进制 blob(不一定是有效的 UTF-8),请使用 Binary + ByteString = zap.ByteString + + // Complex128 构造一个带有复数的字段。与大多数数字字段不同,这需要分配(将complex128转换为interface{}) + Complex128 = zap.Complex128 + + // Complex128P 构造一个带有complex128 的字段。返回的 Field 将在适当的时候安全且显式地表示“nil” + Complex128P = zap.Complex128p + + // Complex64 构造一个带有复数的字段。与大多数数字字段不同,这需要分配(将complex64转换为interface{}) + Complex64 = zap.Complex64 + + // Complex64P 构造一个带有complex64 的字段。返回的 Field 将在适当的时候安全且显式地表示“nil” + Complex64P = zap.Complex64p + + // Float64 构造一个带有 float64 的字段。浮点值的表示方式取决于编码器,因此封送处理必然是惰性的 + Float64 = zap.Float64 + + // Float64P 构造一个带有 float64 的字段。返回的 Field 将在适当的时候安全且显式地表示“nil” + Float64P = zap.Float64p + + // Float32 构造一个带有 float32 的字段。浮点值的表示方式取决于编码器,因此封送处理必然是惰性的 + Float32 = zap.Float32 + + // Float32P 构造一个带有 float32 的字段。返回的 Field 将在适当的时候安全且显式地表示“nil” + Float32P = zap.Float32p + + // Int constructs a field with the given key and value. + Int = zap.Int + + // IntP 构造一个带有 int 的字段。返回的 Field 将在适当的时候安全且显式地表示“nil” + IntP = zap.Intp + + // Int64 使用给定的键和值构造一个字段. + Int64 = zap.Int64 + + // Int64P 构造一个带有 int64 的字段。返回的 Field 将在适当的时候安全且显式地表示“nil” + Int64P = zap.Int64p + + // Int32 使用给定的键和值构造一个字段 + Int32 = zap.Int32 + + // Int32P 构造一个带有 int32 的字段。返回的 Field 将在适当的时候安全且显式地表示“nil”. + Int32P = zap.Int32p + + // Int16 使用给定的键和值构造一个字段 + Int16 = zap.Int16 + + // Int16P 构造一个带有 int16 的字段。返回的 Field 将在适当的时候安全且显式地表示“nil” + Int16P = zap.Int16p + + // Int8 使用给定的键和值构造一个字段 + Int8 = zap.Int8 + + // Int8P 构造一个带有 int8 的字段。返回的 Field 将在适当的时候安全且显式地表示“nil” + Int8P = zap.Int8p + + // String 使用给定的键和值构造一个字段 + String = zap.String + + // StringP 构造一个带有字符串的字段。返回的 Field 将在适当的时候安全且显式地表示“nil” + StringP = zap.Stringp + + // Uint 使用给定的键和值构造一个字段 + Uint = zap.Uint + + // UintP 构造一个带有 uint 的字段。返回的 Field 将在适当的时候安全且显式地表示“nil” + UintP = zap.Uintp + + // Uint64 使用给定的键和值构造一个字段 + Uint64 = zap.Uint64 + + // Uint64P 构造一个带有 uint64 的字段。返回的 Field 将在适当的时候安全且显式地表示“nil” + Uint64P = zap.Uint64p + + // Uint32 使用给定的键和值构造一个字段 + Uint32 = zap.Uint32 + + // Uint32P 构造一个带有 uint32 的字段。返回的 Field 将在适当的时候安全且显式地表示“nil” + Uint32P = zap.Uint32p + + // Uint16 使用给定的键和值构造一个字段 + Uint16 = zap.Uint16 + + // Uint16P 构造一个带有 uint16 的字段。返回的 Field 将在适当的时候安全且显式地表示“nil” + Uint16P = zap.Uint16p + + // Uint8 使用给定的键和值构造一个字段 + Uint8 = zap.Uint8 + + // Uint8P 构造一个带有 uint8 的字段。返回的 Field 将在适当的时候安全且显式地表示“nil” + Uint8P = zap.Uint8p + + // Uintptr 使用给定的键和值构造一个字段 + Uintptr = zap.Uintptr + + // UintptrP 构造一个带有 uintptr 的字段。返回的 Field 将在适当的时候安全且显式地表示“nil” + UintptrP = zap.Uintptrp + + // Reflect 使用给定的键和任意对象构造一个字段。它使用适当的编码、基于反射的方式将几乎任何对象延迟序列化到日志记录上下文中,但它相对较慢且分配繁重。在测试之外,Any 始终是更好的选择 + // - 如果编码失败(例如,尝试将 map[int]string 序列化为 JSON),Reflect 将在最终日志输出中包含错误消息 + Reflect = zap.Reflect + + // Namespace 命名空间在记录器的上下文中创建一个命名的、隔离的范围。所有后续字段都将添加到新的命名空间中 + // - 这有助于防止将记录器注入子组件或第三方库时发生按键冲突 + Namespace = zap.Namespace + + // Stringer 使用给定的键和值的 String 方法的输出构造一个字段。 Stringer 的 String 方法被延迟调用 + Stringer = zap.Stringer + + // Time 使用给定的键和值构造一个 Field。编码器控制时间的序列化方式 + Time = zap.Time + + // TimeP 构造一个带有 time.Time 的字段。返回的 Field 将在适当的时候安全且显式地表示“nil” + TimeP = zap.Timep + + // Stack 构造一个字段,在提供的键下存储当前 goroutine 的堆栈跟踪。请记住,进行堆栈跟踪是急切且昂贵的(相对而言);此操作会进行分配并花费大约两微秒的时间 + Stack = zap.Stack + + // StackSkip 构造一个与 Stack 类似的字段,但也会从堆栈跟踪顶部跳过给定数量的帧 + StackSkip = zap.StackSkip + + // Duration 使用给定的键和值构造一个字段。编码器控制持续时间的序列化方式 + Duration = zap.Duration + + // DurationP 构造一个带有 time.Duration 的字段。返回的 Field 将在适当的时候安全且显式地表示“nil” + DurationP = zap.Durationp + + // Object 使用给定的键和 ObjectMarshaler 构造一个字段。它提供了一种灵活但仍然类型安全且高效的方法来将类似映射或结构的用户定义类型添加到日志记录上下文。该结构的 MarshalLogObject 方法被延迟调用 + Object = zap.Object + + // Inline 构造一个与 Object 类似的 Field,但它会将提供的 ObjectMarshaler 的元素添加到当前命名空间 + Inline = zap.Inline + + // Any 接受一个键和一个任意值,并选择将它们表示为字段的最佳方式,仅在必要时才回退到基于反射的方法。 + // 由于 byteuint8 和 runeint32 是别名,Any 无法区分它们。为了尽量减少意外情况,[]byte 值被视为二进制 blob,字节值被视为 uint8,而 runes 始终被视为整数 + Any = zap.Any +) diff --git a/utils/log/level.go b/utils/log/level.go new file mode 100644 index 0000000..b1dac5b --- /dev/null +++ b/utils/log/level.go @@ -0,0 +1,110 @@ +package log + +import ( + "github.com/kercylan98/minotaur/utils/hash" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +type Level = zapcore.Level +type LevelEnablerFunc = zap.LevelEnablerFunc + +const ( + // DebugLevel 调试级别日志通常非常庞大,并且通常在生产中被禁用 + DebugLevel Level = zapcore.DebugLevel + // InfoLevel 是默认的日志记录优先级 + InfoLevel Level = zapcore.InfoLevel + // WarnLevel 日志比信息更重要,但不需要单独的人工审核 + WarnLevel Level = zapcore.WarnLevel + // ErrorLevel 日志具有高优先级。如果应用程序运行顺利,它不应该生成任何错误级别的日志 + ErrorLevel Level = zapcore.ErrorLevel + // DPanicLevel 日志是特别重要的错误。在开发中,记录器在写入消息后会出现恐慌 + DPanicLevel Level = zapcore.DPanicLevel + // PanicLevel 记录一条消息,然后出现恐慌 + PanicLevel Level = zapcore.PanicLevel + // FatalLevel 记录一条消息,然后调用 os.Exit(1) + FatalLevel Level = zapcore.FatalLevel +) + +var ( + levels = []Level{DebugLevel, InfoLevel, WarnLevel, ErrorLevel, DPanicLevel, PanicLevel, FatalLevel} + defaultLevelPartition = map[Level]func() LevelEnablerFunc{ + DebugLevel: DebugLevelPartition, + InfoLevel: InfoLevelPartition, + WarnLevel: WarnLevelPartition, + ErrorLevel: ErrorLevelPartition, + DPanicLevel: DPanicLevelPartition, + PanicLevel: PanicLevelPartition, + FatalLevel: FatalLevelPartition, + } +) + +// Levels 返回所有日志级别 +func Levels() []Level { + return levels +} + +// MultiLevelPartition 返回一个 LevelEnablerFunc,该函数在指定的多个级别时返回 true +// - 该函数被用于划分不同级别的日志输出 +func MultiLevelPartition(levels ...Level) LevelEnablerFunc { + var levelMap = hash.ToIterator(levels) + return func(level zapcore.Level) bool { + return hash.Exist(levelMap, level) + } +} + +// DebugLevelPartition 返回一个 LevelEnablerFunc,该函数在 DebugLevel 时返回 true +// - 该函数被用于划分不同级别的日志输出 +func DebugLevelPartition() LevelEnablerFunc { + return func(level zapcore.Level) bool { + return level == DebugLevel + } +} + +// InfoLevelPartition 返回一个 LevelEnablerFunc,该函数在 InfoLevel 时返回 true +// - 该函数被用于划分不同级别的日志输出 +func InfoLevelPartition() LevelEnablerFunc { + return func(level zapcore.Level) bool { + return level == InfoLevel + } +} + +// WarnLevelPartition 返回一个 LevelEnablerFunc,该函数在 WarnLevel 时返回 true +// - 该函数被用于划分不同级别的日志输出 +func WarnLevelPartition() LevelEnablerFunc { + return func(level zapcore.Level) bool { + return level == WarnLevel + } +} + +// ErrorLevelPartition 返回一个 LevelEnablerFunc,该函数在 ErrorLevel 时返回 true +// - 该函数被用于划分不同级别的日志输出 +func ErrorLevelPartition() LevelEnablerFunc { + return func(level zapcore.Level) bool { + return level == ErrorLevel + } +} + +// DPanicLevelPartition 返回一个 LevelEnablerFunc,该函数在 DPanicLevel 时返回 true +// - 该函数被用于划分不同级别的日志输出 +func DPanicLevelPartition() LevelEnablerFunc { + return func(level zapcore.Level) bool { + return level == DPanicLevel + } +} + +// PanicLevelPartition 返回一个 LevelEnablerFunc,该函数在 PanicLevel 时返回 true +// - 该函数被用于划分不同级别的日志输出 +func PanicLevelPartition() LevelEnablerFunc { + return func(level zapcore.Level) bool { + return level == PanicLevel + } +} + +// FatalLevelPartition 返回一个 LevelEnablerFunc,该函数在 FatalLevel 时返回 true +// - 该函数被用于划分不同级别的日志输出 +func FatalLevelPartition() LevelEnablerFunc { + return func(level zapcore.Level) bool { + return level == FatalLevel + } +} diff --git a/utils/log/log.go b/utils/log/log.go index cfab117..bc77bd2 100644 --- a/utils/log/log.go +++ b/utils/log/log.go @@ -2,187 +2,169 @@ package log import ( "fmt" - rotatelogs "github.com/lestrrat-go/file-rotatelogs" + "github.com/kercylan98/minotaur/utils/str" + "github.com/kercylan98/minotaur/utils/times" + rotateLogs "github.com/lestrrat-go/file-rotatelogs" "go.uber.org/zap" "go.uber.org/zap/zapcore" - "io" "os" - "runtime/debug" - "time" + "path/filepath" + "strings" ) -var ( - logger *zap.Logger - prod bool - logPath string - logDevWrite bool - logTime = 7 -) - -func init() { - logger = newLogger() - if prod && len(logPath) == 0 { - Warn("Logger", zap.String("Tip", "in production mode, if the log file output directory is not set, only the console will be output")) +// NewLog 创建一个日志记录器 +func NewLog(options ...Option) *Log { + log := &Log{ + filename: func(level Level) string { + return fmt.Sprintf("%s.log", level.String()) + }, + rotateFilename: func(level Level) string { + return strings.Join([]string{level.String(), "%Y%m%d.log"}, ".") + }, + levelPartition: defaultLevelPartition, } -} -func newLogger() *zap.Logger { - encoder := zapcore.NewConsoleEncoder(zapcore.EncoderConfig{ - MessageKey: "msg", - LevelKey: "level", - EncodeLevel: zapcore.CapitalLevelEncoder, - TimeKey: "ts", - EncodeTime: func(t time.Time, enc zapcore.PrimitiveArrayEncoder) { - enc.AppendString(t.Format(time.DateTime)) - }, - CallerKey: "file", - EncodeCaller: zapcore.ShortCallerEncoder, - EncodeDuration: func(d time.Duration, enc zapcore.PrimitiveArrayEncoder) { - enc.AppendInt64(int64(d) / 1000000) - }, - }) + for _, option := range options { + option(log) + } - infoLevel := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { - return lvl == zapcore.InfoLevel - }) - debugLevel := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { - return lvl <= zapcore.FatalLevel - }) - errorLevel := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { - return lvl >= zapcore.ErrorLevel - }) - - var cores zapcore.Core - - if !prod { - if len(logPath) > 0 && logDevWrite { - infoWriter := getWriter(fmt.Sprintf("%s/info.log", logPath), logTime) - errorWriter := getWriter(fmt.Sprintf("%s/error.log", logPath), logTime) - cores = zapcore.NewTee( - zapcore.NewCore(encoder, zapcore.AddSync(infoWriter), infoLevel), - zapcore.NewCore(encoder, zapcore.AddSync(errorWriter), errorLevel), - zapcore.NewCore(encoder, zapcore.AddSync(os.Stdout), debugLevel), - ) - } else { - cores = zapcore.NewTee( - zapcore.NewCore(encoder, zapcore.AddSync(os.Stdout), debugLevel), - ) - } - } else { - if len(logPath) == 0 { - cores = zapcore.NewTee( - zapcore.NewCore(encoder, zapcore.AddSync(os.Stdout), debugLevel), - ) - } else { - infoWriter := getWriter(fmt.Sprintf("%s/info.log", logPath), logTime) - errorWriter := getWriter(fmt.Sprintf("%s/error.log", logPath), logTime) - cores = zapcore.NewTee( - zapcore.NewCore(encoder, zapcore.AddSync(infoWriter), infoLevel), - zapcore.NewCore(encoder, zapcore.AddSync(errorWriter), errorLevel), - zapcore.NewCore(encoder, zapcore.AddSync(os.Stdout), errorLevel), - ) + if len(log.rotateOptions) == 0 { + log.rotateOptions = []rotateLogs.Option{ + rotateLogs.WithMaxAge(times.Week), + rotateLogs.WithRotationTime(times.Day), } } - return zap.New(cores, zap.AddCaller(), zap.AddCallerSkip(1)) -} + if len(log.cores) == 0 { + var encoder = NewEncoder() -func getWriter(filename string, times int) io.Writer { - hook, err := rotatelogs.New( - filename+".%Y%m%d", - rotatelogs.WithLinkName(filename), - rotatelogs.WithMaxAge(time.Hour*24*7), - rotatelogs.WithRotationTime(time.Hour*time.Duration(times)), - ) - - if err != nil { - panic(err) + switch log.mode { + case RunModeDev: + var partition LevelEnablerFunc = func(lvl Level) bool { + return true + } + log.cores = append(log.cores, zapcore.NewCore(encoder, os.Stdout, partition)) + case RunModeTest, RunModeProd: + if log.mode == RunModeTest { + infoRotate, err := rotateLogs.New( + filepath.Join(log.rotateLogDir, log.rotateFilename(InfoLevel)), + append([]rotateLogs.Option{rotateLogs.WithLinkName(filepath.Join(log.logDir, log.filename(InfoLevel)))}, log.rotateOptions...)..., + ) + if err != nil { + panic(err) + } + errRotate, err := rotateLogs.New( + filepath.Join(log.rotateLogDir, log.rotateFilename(ErrorLevel)), + append([]rotateLogs.Option{rotateLogs.WithLinkName(filepath.Join(log.logDir, log.filename(ErrorLevel)))}, log.rotateOptions...)..., + ) + if err != nil { + panic(err) + } + if log.logDir != str.None { + log.cores = append(log.cores, zapcore.NewCore(encoder, zapcore.AddSync(infoRotate), LevelEnablerFunc(func(lvl Level) bool { return lvl < ErrorLevel }))) + log.cores = append(log.cores, zapcore.NewCore(encoder, zapcore.AddSync(errRotate), LevelEnablerFunc(func(lvl Level) bool { return lvl >= ErrorLevel }))) + log.cores = append(log.cores, zapcore.NewCore(encoder, os.Stdout, LevelEnablerFunc(func(lvl Level) bool { return lvl < ErrorLevel }))) + log.cores = append(log.cores, zapcore.NewCore(encoder, os.Stdout, LevelEnablerFunc(func(lvl Level) bool { return lvl >= ErrorLevel }))) + } + } else { + infoRotate, err := rotateLogs.New( + filepath.Join(log.rotateLogDir, log.rotateFilename(InfoLevel)), + append([]rotateLogs.Option{rotateLogs.WithLinkName(filepath.Join(log.logDir, log.filename(InfoLevel)))}, log.rotateOptions...)..., + ) + if err != nil { + panic(err) + } + errRotate, err := rotateLogs.New( + filepath.Join(log.rotateLogDir, log.rotateFilename(ErrorLevel)), + append([]rotateLogs.Option{rotateLogs.WithLinkName(filepath.Join(log.logDir, log.filename(ErrorLevel)))}, log.rotateOptions...)..., + ) + if err != nil { + panic(err) + } + if log.logDir != str.None { + log.cores = append(log.cores, zapcore.NewCore(encoder, zapcore.AddSync(infoRotate), LevelEnablerFunc(func(lvl Level) bool { return lvl == InfoLevel }))) + log.cores = append(log.cores, zapcore.NewCore(encoder, zapcore.AddSync(errRotate), LevelEnablerFunc(func(lvl Level) bool { return lvl >= ErrorLevel }))) + } + } + } } - return hook + + log.zap = zap.New(zapcore.NewTee(log.cores...), zap.AddCaller(), zap.AddCallerSkip(1)) + log.sugar = log.zap.Sugar() + return log } -type MLogger struct { - *zap.Logger +type Log struct { + zap *zap.Logger + sugar *zap.SugaredLogger + filename func(level Level) string + rotateFilename func(level Level) string + rotateOptions []rotateLogs.Option + levelPartition map[Level]func() LevelEnablerFunc + cores []Core + mode RunMode + logDir string + rotateLogDir string } -func (slf *MLogger) Printf(format string, args ...interface{}) { - slf.Info(fmt.Sprintf(format, args...)) +func (slf *Log) Debugf(format string, args ...interface{}) { + slf.sugar.Debugf(format, args...) } -func Logger() *MLogger { - return &MLogger{logger} +func (slf *Log) Infof(format string, args ...interface{}) { + slf.sugar.Infof(format, args...) } -func Info(msg string, fields ...zap.Field) { - logger.Info(msg, fields...) +func (slf *Log) Warnf(format string, args ...interface{}) { + slf.sugar.Warnf(format, args...) } -func Warn(msg string, fields ...zap.Field) { - logger.Warn(msg, fields...) +func (slf *Log) Errorf(format string, args ...interface{}) { + slf.sugar.Errorf(format, args...) } -func Debug(msg string, fields ...zap.Field) { - logger.Debug(msg, fields...) +func (slf *Log) Fatalf(format string, args ...interface{}) { + slf.sugar.Fatalf(format, args...) } -func Error(msg string, fields ...zap.Field) { - logger.Error(msg, fields...) - fmt.Println(string(debug.Stack())) +func (slf *Log) Printf(format string, args ...interface{}) { + slf.sugar.Infof(format, args...) } -func ErrorHideStack(msg string, fields ...zap.Field) { - logger.Error(msg, fields...) +// Debug 在 DebugLevel 记录一条消息。该消息包括在日志站点传递的任何字段以及记录器上累积的任何字段 +func (slf *Log) Debug(msg string, fields ...Field) { + slf.zap.Debug(msg, fields...) } -// ErrorWithStack 通过额外的堆栈信息打印错误日志 -func ErrorWithStack(msg, stack string, fields ...zap.Field) { - logger.Error(msg, fields...) - var stackMerge string - if len(stack) > 0 { - stackMerge = stack - } - stackMerge += string(debug.Stack()) - fmt.Println(stackMerge) +// Info 在 InfoLevel 记录一条消息。该消息包括在日志站点传递的任何字段以及记录器上累积的任何字段 +func (slf *Log) Info(msg string, fields ...Field) { + slf.zap.Info(msg, fields...) } -// SetProd 设置生产环境模式 -func SetProd(isProd bool) { - if prod == isProd { - return - } - prod = isProd - if logger != nil { - _ = logger.Sync() - } - logger = newLogger() +// Warn 在 WarnLevel 记录一条消息。该消息包括在日志站点传递的任何字段以及记录器上累积的任何字段 +func (slf *Log) Warn(msg string, fields ...Field) { + slf.zap.Warn(msg, fields...) } -// SetLogDir 设置日志输出目录 -func SetLogDir(dir string) { - logPath = dir - if logger != nil { - _ = logger.Sync() - } - logger = newLogger() +// Error 在 ErrorLevel 记录一条消息。该消息包括在日志站点传递的任何字段以及记录器上累积的任何字段 +func (slf *Log) Error(msg string, fields ...Field) { + slf.zap.Error(msg, fields...) } -// SetWriteFileWithDev 设置开发环境下写入文件 -func SetWriteFileWithDev(isWrite bool) { - if isWrite == logDevWrite { - return - } - logDevWrite = isWrite - if logger != nil { - _ = logger.Sync() - } - logger = newLogger() +// DPanic 在 DPanicLevel 记录一条消息。该消息包括在日志站点传递的任何字段以及记录器上累积的任何字段 +// - 如果记录器处于开发模式,它就会出现 panic(DPanic 的意思是“development panic”)。这对于捕获可恢复但不应该发生的错误很有用 +func (slf *Log) DPanic(msg string, fields ...Field) { + slf.zap.DPanic(msg, fields...) } -// SetLogRotate 设置日志切割时间 -func SetLogRotate(t int) { - logTime = t - if logger != nil { - _ = logger.Sync() - } - logger = newLogger() +// Panic 在 PanicLevel 记录一条消息。该消息包括在日志站点传递的任何字段以及记录器上累积的任何字段 +// - 即使禁用了 PanicLevel 的日志记录,记录器也会出现 panic +func (slf *Log) Panic(msg string, fields ...Field) { + slf.zap.Panic(msg, fields...) +} + +// Fatal 在 FatalLevel 记录一条消息。该消息包括在日志站点传递的任何字段以及记录器上累积的任何字段 +// - 然后记录器调用 os.Exit(1),即使 FatalLevel 的日志记录被禁用 +func (slf *Log) Fatal(msg string, fields ...Field) { + slf.zap.Fatal(msg, fields...) } diff --git a/utils/log/logger.go b/utils/log/logger.go new file mode 100644 index 0000000..a0a1c96 --- /dev/null +++ b/utils/log/logger.go @@ -0,0 +1,79 @@ +package log + +import ( + "github.com/panjf2000/ants/v2" + "github.com/panjf2000/gnet/pkg/logging" +) + +var logger Logger = NewLog() + +// Logger 适用于 Minotaur 的日志接口 +type Logger interface { + ants.Logger + logging.Logger + // Debug 在 DebugLevel 记录一条消息。该消息包括在日志站点传递的任何字段以及记录器上累积的任何字段 + Debug(msg string, fields ...Field) + // Info 在 InfoLevel 记录一条消息。该消息包括在日志站点传递的任何字段以及记录器上累积的任何字段 + Info(msg string, fields ...Field) + // Warn 在 WarnLevel 记录一条消息。该消息包括在日志站点传递的任何字段以及记录器上累积的任何字段 + Warn(msg string, fields ...Field) + // Error 在 ErrorLevel 记录一条消息。该消息包括在日志站点传递的任何字段以及记录器上累积的任何字段 + Error(msg string, fields ...Field) + // DPanic 在 DPanicLevel 记录一条消息。该消息包括在日志站点传递的任何字段以及记录器上累积的任何字段 + // - 如果记录器处于开发模式,它就会出现 panic(DPanic 的意思是“development panic”)。这对于捕获可恢复但不应该发生的错误很有用 + DPanic(msg string, fields ...Field) + // Panic 在 PanicLevel 记录一条消息。该消息包括在日志站点传递的任何字段以及记录器上累积的任何字段 + // - 即使禁用了 PanicLevel 的日志记录,记录器也会出现 panic + Panic(msg string, fields ...Field) + // Fatal 在 FatalLevel 记录一条消息。该消息包括在日志站点传递的任何字段以及记录器上累积的任何字段 + // - 然后记录器调用 os.Exit(1),即使 FatalLevel 的日志记录被禁用 + Fatal(msg string, fields ...Field) +} + +// Debug 在 DebugLevel 记录一条消息。该消息包括在日志站点传递的任何字段以及记录器上累积的任何字段 +func Debug(msg string, fields ...Field) { + logger.Debug(msg, fields...) +} + +// Info 在 InfoLevel 记录一条消息。该消息包括在日志站点传递的任何字段以及记录器上累积的任何字段 +func Info(msg string, fields ...Field) { + logger.Info(msg, fields...) +} + +// Warn 在 WarnLevel 记录一条消息。该消息包括在日志站点传递的任何字段以及记录器上累积的任何字段 +func Warn(msg string, fields ...Field) { + logger.Warn(msg, fields...) +} + +// Error 在 ErrorLevel 记录一条消息。该消息包括在日志站点传递的任何字段以及记录器上累积的任何字段 +func Error(msg string, fields ...Field) { + logger.Error(msg, fields...) +} + +// DPanic 在 DPanicLevel 记录一条消息。该消息包括在日志站点传递的任何字段以及记录器上累积的任何字段 +// - 如果记录器处于开发模式,它就会出现 panic(DPanic 的意思是“development panic”)。这对于捕获可恢复但不应该发生的错误很有用 +func DPanic(msg string, fields ...Field) { + logger.DPanic(msg, fields...) +} + +// Panic 在 PanicLevel 记录一条消息。该消息包括在日志站点传递的任何字段以及记录器上累积的任何字段 +// - 即使禁用了 PanicLevel 的日志记录,记录器也会出现 panic +func Panic(msg string, fields ...Field) { + logger.Panic(msg, fields...) +} + +// Fatal 在 FatalLevel 记录一条消息。该消息包括在日志站点传递的任何字段以及记录器上累积的任何字段 +// - 然后记录器调用 os.Exit(1),即使 FatalLevel 的日志记录被禁用 +func Fatal(msg string, fields ...Field) { + logger.Fatal(msg, fields...) +} + +// SetLogger 设置日志记录器 +func SetLogger(log Logger) { + logger = log +} + +// GetLogger 获取日志记录器 +func GetLogger() Logger { + return logger +} diff --git a/utils/log/options.go b/utils/log/options.go new file mode 100644 index 0000000..c00297c --- /dev/null +++ b/utils/log/options.go @@ -0,0 +1,52 @@ +package log + +import ( + rotateLogs "github.com/lestrrat-go/file-rotatelogs" +) + +type Option func(log *Log) + +// WithRunMode 设置运行模式 +// - 默认的运行模式为: RunModeDev +// - 当 handle 不为空时,将会调用 handle(),并将返回值添加到日志记录器中,同时将会抑制默认的日志记录器 +func WithRunMode(mode RunMode, handle func() Core) Option { + return func(log *Log) { + log.mode = mode + if handle != nil { + log.cores = append(log.cores, handle()) + } + } +} + +// WithFilename 设置日志文件名 +// - 默认的日志文件名为: {level}.log +func WithFilename(filename func(level Level) string) Option { + return func(log *Log) { + log.filename = filename + } +} + +// WithRotateFilename 设置日志分割文件名 +// - 默认的日志分割文件名为: {level}.%Y%m%d.log +func WithRotateFilename(filename func(level Level) string) Option { + return func(log *Log) { + log.rotateFilename = filename + } +} + +// WithRotateOption 设置日志分割选项 +// - 默认的日志分割选项为: WithMaxAge(7天), WithRotationTime(1天) +func WithRotateOption(options ...rotateLogs.Option) Option { + return func(log *Log) { + log.rotateOptions = options + } +} + +// WithLogDir 设置日志文件夹 +// - 默认情况下不会设置日志文件夹,日志将不会被文件存储 +func WithLogDir(logDir, rotateLogDir string) Option { + return func(log *Log) { + log.logDir = logDir + log.rotateLogDir = rotateLogDir + } +} diff --git a/utils/log/run_mode.go b/utils/log/run_mode.go new file mode 100644 index 0000000..b49a7d4 --- /dev/null +++ b/utils/log/run_mode.go @@ -0,0 +1,16 @@ +package log + +const ( + // RunModeDev 开发模式是默认的运行模式,同时也是最基础的运行模式 + // - 开发模式下,将会输出所有级别的日志到控制台 + // - 默认不再输出日志到文件 + RunModeDev RunMode = iota + // RunModeTest 测试模式是一种特殊的运行模式,用于测试 + // - 测试模式下,将会输出所有级别的日志到控制台和文件 + RunModeTest + // RunModeProd 生产模式是一种特殊的运行模式,用于生产 + // - 生产模式下,将会输出 InfoLevel 及以上级别的日志到控制台和文件 + RunModeProd +) + +type RunMode uint8