package logx import ( "fmt" "git.diulo.com/mogfee/kit/core/sysx" "io" "log" "os" "path" "runtime/debug" "sync" "sync/atomic" "time" ) const callerDepth = 4 var ( timeFormat = "2006-01-02T15:04:05.000Z07:00" logLevel uint32 encoding uint32 = jsonEncodingType maxContentLength uint32 disableLog uint32 disableStat uint32 options logOptions writer = new(atomicWriter) setupOnce sync.Once ) type ( LogField struct { Key string Value any } LogOption func(opts *logOptions) logEntry map[string]any logOptions struct { gzipEnabled bool logStackCooldownMills int keepDays int maxBackups int maxSize int rotationRule string } ) func Alert(v string) { getWriter().Alert(v) } func Close() error { if w := writer.Swap(nil); w != nil { return w.(io.Closer).Close() } return nil } func Disable() { atomic.StoreUint32(&disableLog, 1) writer.Store(nopWriter{}) } func DisableStat() { atomic.StoreUint32(&disableStat, 1) } func Debug(v ...any) { writeDebug(fmt.Sprint(v...)) } func Debugf(format string, v ...any) { writeDebug(fmt.Sprintf(format, v...)) } func Error(v ...any) { writeError(fmt.Sprint(v...)) } func Errorf(format string, v ...any) { writeError(fmt.Errorf(format, v...)) } func ErrorStack(v ...any) { writeStack(fmt.Sprint(v...)) } func ErrorStackf(format string, v ...any) { writeStack(fmt.Sprintf(format, v...)) } func Info(v ...any) { writeInfo(fmt.Sprint(v...)) } func Infof(format string, v ...any) { writeInfo(fmt.Sprintf(format, v...)) } func Must(err error) { if err == nil { return } msg := err.Error() log.Print(err) getWriter().Severe(msg) os.Exit(1) } func Rest() Writer { return writer.Swap(nil) } func SetLevel(level uint32) { atomic.StoreUint32(&logLevel, level) } func SetWriter(w Writer) { if atomic.LoadUint32(&disableLog) == 0 { writer.Store(w) } } func SetUp(c LogConf) (err error) { setupOnce.Do(func() { setupLogLevel(c) if !c.Stat { DisableStat() } if len(c.TimeFormat) > 0 { timeFormat = c.TimeFormat } atomic.StoreUint32(&maxContentLength, c.MaxContentLength) switch c.Encoding { case plainEncoding: atomic.StoreUint32(&encoding, plainEncodingType) default: atomic.StoreUint32(&encoding, jsonEncodingType) } switch c.Mode { case fileMode: err = setupWithFiles(c) case volumeMode: err = setupWithVolume(c) default: setupWithConsole() } }) return } func Severe(v ...any) { writeSevere(fmt.Sprint(v...)) } func Severef(format string, v ...any) { writeSevere(fmt.Sprintf(format, v...)) } func Slow(v ...any) { writeSlow(fmt.Sprint(v...)) } func Slowf(format string, v ...any) { writeSlow(fmt.Sprintf(format, v...)) } func Stat(v ...any) { writeStat(fmt.Sprint(v...)) } func Statf(format string, v ...any) { writeStat(fmt.Sprintf(format, v...)) } func WithCooldownMillis(mills int) LogOption { return func(opts *logOptions) { opts.logStackCooldownMills = mills } } func WithKeepDays(days int) LogOption { return func(opts *logOptions) { opts.keepDays = days } } func WithGzip() LogOption { return func(opts *logOptions) { opts.gzipEnabled = true } } func WithMaxBackups(count int) LogOption { return func(opts *logOptions) { opts.maxBackups = count } } func WithMaxSize(size int) LogOption { return func(opts *logOptions) { opts.maxSize = size } } func WithRotation(r string) LogOption { return func(opts *logOptions) { opts.rotationRule = r } } func addCaller(fields ...LogField) []LogField { return append(fields, Field(callerKey, getCaller(callerDepth))) } func createOutput(path string) (io.WriteCloser, error) { if len(path) == 0 { return nil, ErrLogPathNotSet } switch options.rotationRule { case sizeRotationRule: return NewLogger(path, NewSizeLimitRotateRule(path, backupFileDelimiter, options.keepDays, options.maxSize, options.maxBackups, options.gzipEnabled), options.gzipEnabled) default: return NewLogger(path, DefaultRotateRule(path, backupFileDelimiter, options.keepDays, options.gzipEnabled), options.gzipEnabled) } } func getWriter() Writer { w := writer.Load() if w == nil { w = writer.StoreIfNil(newConsoleWriter()) } return w } func Field(key string, value any) LogField { switch val := value.(type) { case error: return LogField{Key: key, Value: val.Error()} case []error: var errs []string for _, err := range val { errs = append(errs, err.Error()) } return LogField{Key: key, Value: errs} case time.Duration: return LogField{Key: key, Value: fmt.Sprint(val)} case []time.Duration: var durs []string for _, dur := range val { durs = append(durs, fmt.Sprint(dur)) } return LogField{Key: key, Value: durs} case []time.Time: var times []string for _, t := range val { times = append(times, fmt.Sprint(t)) } return LogField{Key: key, Value: times} case fmt.Stringer: return LogField{Key: key, Value: val.String()} case []fmt.Stringer: var strs []string for _, str := range val { strs = append(strs, str.String()) } return LogField{Key: key, Value: strs} default: return LogField{Key: key, Value: val} } } func setupLogLevel(c LogConf) { switch c.Level { case levelDebug: SetLevel(DebugLevel) case levelInfo: SetLevel(InfoLevel) case levelError: SetLevel(ErrorLevel) case levelSevere: SetLevel(SevereLevel) } } func setupWithConsole() { SetWriter(newConsoleWriter()) } func setupWithFiles(c LogConf) error { w, err := newFileWriter(c) if err != nil { return err } SetWriter(w) return nil } func setupWithVolume(c LogConf) error { if len(c.ServiceName) == 0 { return ErrLogServiceNameNotSet } c.Path = path.Join(c.Path, c.ServiceName, sysx.Hostname()) return setupWithFiles(c) } func shallLog(level uint32) bool { return atomic.LoadUint32(&logLevel) <= level } func shallLogStat() bool { return atomic.LoadUint32(&disableStat) == 0 } func writeDebug(val any, fields ...LogField) { if shallLog(DebugLevel) { getWriter().Debug(val, addCaller(fields...)...) } } func writeError(val any, fields ...LogField) { if shallLog(ErrorLevel) { getWriter().Error(val, addCaller(fields...)...) } } func writeInfo(val any, fields ...LogField) { if shallLog(InfoLevel) { getWriter().Info(val, addCaller(fields...)...) } } func writeSevere(msg string) { if shallLog(SevereLevel) { getWriter().Severe(fmt.Sprintf("%s\n%s", msg, string(debug.Stack()))) } } func writeSlow(val any, fields ...LogField) { if shallLog(ErrorLevel) { getWriter().Slow(val, addCaller(fields...)...) } } func writeStack(msg string) { if shallLog(ErrorLevel) { getWriter().Stack(fmt.Sprintf("%s\n%s", msg, string(debug.Stack()))) } } func writeStat(msg string) { if shallLogStat() && shallLog(InfoLevel) { getWriter().Stat(msg, addCaller()...) } } func newFileWriter(c LogConf) (Writer, error) { var err error var opts []LogOption var infoLog io.WriteCloser var errorLog io.WriteCloser var severeLog io.WriteCloser var slowLog io.WriteCloser var statLog io.WriteCloser var stackLog io.Writer if len(c.Path) == 0 { return nil, ErrLogPathNotSet } opts = append(opts, WithCooldownMillis(c.StackCooldownMillis)) if c.Compress { opts = append(opts, WithGzip()) } if c.KeepDays > 0 { opts = append(opts, WithKeepDays(c.KeepDays)) } if c.MaxBackups > 0 { opts = append(opts, WithMaxBackups(c.MaxBackups)) } if c.MaxSize > 0 { opts = append(opts, WithMaxSize(c.MaxSize)) } opts = append(opts, WithRotation(c.Rotation)) accessFile := path.Join(c.Path, accessFilename) errorFile := path.Join(c.Path, errorFilename) severeFile := path.Join(c.Path, severeFilename) slowFile := path.Join(c.Path, slowFilename) statFile := path.Join(c.Path, statFilename) handleOptions(opts) setupLogLevel(c) if infoLog, err = createOutput(accessFile); err != nil { return nil, err } if errorLog, err = createOutput(errorFile); err != nil { return nil, err } if severeLog, err = createOutput(severeFile); err != nil { return nil, err } if slowLog, err = createOutput(slowFile); err != nil { return nil, err } if statLog, err = createOutput(statFile); err != nil { return nil, err } stackLog = newLessWriter(errorLog, options.logStackCooldownMills) return &concreteWriter{ infoLog: infoLog, errorLog: errorLog, severeLog: severeLog, slowLog: slowLog, statLog: statLog, stackLog: stackLog, }, nil } func handleOptions(opts []LogOption) { for _, opt := range opts { opt(&options) } }