// Package baseapp provides application-level logging facilities. // // It supports dynamic log level and output target control, // hot reloading without restarting the process, // and safe lifecycle management for long-running applications. // // The logger is designed for industrial-grade applications, // providing features like: // - Console and file output (with log rotation and compression) // - Atomic-level dynamic log level changes // - Safe multi-threaded usage via internal synchronization // // Author: NiuJiuRu // Email: niujiuru@qq.com // Last modified: 2026-06-03 package baseapp import "C" import ( "bytes" "errors" "fmt" "io" "os" "path/filepath" "strconv" "strings" "sync" "sync/atomic" "github.com/sirupsen/logrus" "gopkg.in/ini.v1" "gopkg.in/natefinch/lumberjack.v2" ) // 日志级别 type level uint32 const ( LEVEL_TRACE level = level(logrus.TraceLevel) // trace LEVEL_DEBUG = level(logrus.DebugLevel) // debug LEVEL_INFO = level(logrus.InfoLevel) // info LEVEL_WARN = level(logrus.WarnLevel) // warning LEVEL_ERROR = level(logrus.ErrorLevel) // error LEVEL_FATAL = level(logrus.FatalLevel) // fatal, 会结束程序运行, 会自动调用os.Exit(1) LEVEL_PANIC = level(logrus.PanicLevel) // panic, 会触发程序崩溃, 不处理的话, =程序退出 ) func (l *level) FromString(s string) { lvl, err := logrus.ParseLevel(strings.TrimSpace(s)) if err == nil { *l = level(lvl) } else { *l = LEVEL_INFO } } func (l level) String() string { return logrus.Level(l).String() } // 输出目标 type target uint32 const ( TARGET_NONE target = iota // 不输出日志 TARGET_CONSOLE target = 1 << iota // 控制台输出 TARGET_FILE // 输出到文件 ) func (t *target) FromString(s string) { *t = TARGET_NONE for part := range strings.SplitSeq(s, ",") { switch strings.ToLower(strings.TrimSpace(part)) { case "console": *t |= TARGET_CONSOLE case "file": *t |= TARGET_FILE case "all": *t |= TARGET_CONSOLE | TARGET_FILE } } } func (t target) String() string { if t == TARGET_NONE { return "none" } parts := make([]string, 0, 2) if (t & TARGET_CONSOLE) != 0 { parts = append(parts, "console") } if (t & TARGET_FILE) != 0 { parts = append(parts, "file") } return strings.Join(parts, ",") } // 输出格式 type formatter struct{} func (f *formatter) Format(entry *logrus.Entry) ([]byte, error) { msg := "" pid := os.Getpid() if entry.Caller != nil { file := filepath.Base(entry.Caller.File) line := entry.Caller.Line msg = fmt.Sprintf("[%s, PID: %d] [%s] (%s:%d): %s\n", entry.Time.Format("2006-01-02 15:04:05"), pid, entry.Level.String(), file, line, entry.Message) } else { msg = fmt.Sprintf("[%s, PID: %d] [%s]: %s\n", entry.Time.Format("2006-01-02 15:04:05"), pid, entry.Level.String(), entry.Message) } return []byte(msg), nil } type systemdConsoleWriter struct{} func (w *systemdConsoleWriter) Write(p []byte) (int, error) { msg := p if idx := bytes.Index(msg, []byte("] ")); idx != -1 { msg = msg[idx+2:] } _, _ = os.Stdout.Write(msg) return len(p), nil } // 日志配置 type config struct { // 日志输出级别 Level level // 日志输出目标 Target target // 每个日志文件的最大大下, 以"MB"为单位 MaxSize int `ini:"MaxFileSize"` // 保留日志文件的最大天数, 以"天"为单位 MaxAge int `ini:"MaxReserveDays"` // 保留的最大备份文件数量, 以"个"为单位 MaxBackups int `ini:"MaxBackupFileCnts"` // 轮转文件是否压缩(gzip)减少占用的空间 Compress bool `ini:"IsCompress"` } var ( Logger *logrus.Logger logFileWriter *lumberjack.Logger logMu sync.Mutex initialized atomic.Bool cfgFile string cfgLog = &config{ Level: LEVEL_TRACE, Target: TARGET_CONSOLE, MaxSize: 0, MaxAge: 0, MaxBackups: 0, Compress: true, } ErrLoggerNotInitialized = errors.New("logger is not initialized") ) func InitLogger(logCfgFile string) { logMu.Lock() defer logMu.Unlock() if initialized.Load() { return } cfgFile = logCfgFile if cfgFile == "" { cfgFile = filepath.Join(CFG_DIR, "config.ini") } levelStr := "trace" // ---初始化设置默认的日志级别--- targetStr := "console" // ---初始化设置默认的输出目标--- cfgIni, err := ini.Load(cfgFile) // 从配置文件中加载设置 if err == nil && cfgIni.HasSection("Log") { tmpCfgLog := *cfgLog err = cfgIni.Section("Log").MapTo(&tmpCfgLog) if err != nil { fmt.Printf("Failed to load the \"[Log]\" section data from the \"%s\" file: %v\n", cfgFile, err) } else { *cfgLog = tmpCfgLog } if cfgIni.Section("Log").HasKey("Level") { levelStr = cfgIni.Section("Log").Key("Level").String() } if cfgIni.Section("Log").HasKey("Target") { targetStr = cfgIni.Section("Log").Key("Target").String() } } Logger = logrus.New() setLogLevelInternal(levelStr) // 1, 设置日志级别 Logger.SetFormatter(new(formatter)) // 2, 设置日志格式 setLogTargetInternal(targetStr) // 3, 设置输出目标 initialized.Store(true) ////// 标记日志模块已初始化完成 } func GetLogLevel() (string, error) { logMu.Lock() defer logMu.Unlock() if !initialized.Load() { return "", ErrLoggerNotInitialized } return cfgLog.Level.String(), nil } func setLogLevelInternal(levelStr string) { var lvl level lvl.FromString(levelStr) cfgLog.Level = lvl Logger.SetLevel(logrus.Level(cfgLog.Level)) Logger.SetReportCaller( lvl == LEVEL_TRACE || lvl == LEVEL_DEBUG, ) // 使得输出日志时能够获取到文件名和行号 } func UpdateLogLevel(levelStr string) error { logMu.Lock() defer logMu.Unlock() if !initialized.Load() { return ErrLoggerNotInitialized } setLogLevelInternal(levelStr) return nil } func GetLogTarget() (string, error) { logMu.Lock() defer logMu.Unlock() if !initialized.Load() { return "", ErrLoggerNotInitialized } return cfgLog.Target.String(), nil } func setLogTargetInternal(targetStr string) { var t target t.FromString(targetStr) cfgLog.Target = t writers := make([]io.Writer, 0) if (cfgLog.Target & TARGET_CONSOLE) != 0 { isSystemd := os.Getenv("INVOCATION_ID") != "" || os.Getenv("JOURNAL_STREAM") != "" if isSystemd { writers = append(writers, &systemdConsoleWriter{}) } else { writers = append(writers, os.Stdout) } } if (cfgLog.Target & TARGET_FILE) != 0 { logDir := LOG_DIR if logDir == "" { logDir = "." } else if info, err := os.Stat(logDir); err != nil || !info.IsDir() { logDir = "." } logFile := filepath.Join(logDir, EXEC_FILENAME+".log") old := logFileWriter logFileWriter = &lumberjack.Logger{Filename: logFile, MaxSize: cfgLog.MaxSize, MaxAge: cfgLog.MaxAge, MaxBackups: cfgLog.MaxBackups, Compress: cfgLog.Compress} if old != nil { old.Close() } writers = append(writers, logFileWriter) } if len(writers) > 0 { multiWriter := io.MultiWriter(writers...) Logger.SetOutput(multiWriter) } else { Logger.SetOutput(io.Discard) // 不输出日志 } } func UpdateLogTarget(targetStr string) error { logMu.Lock() defer logMu.Unlock() if !initialized.Load() { return ErrLoggerNotInitialized } setLogTargetInternal(targetStr) return nil } func SaveLogConfig() error { logMu.Lock() defer logMu.Unlock() if !initialized.Load() { return ErrLoggerNotInitialized } dir := filepath.Dir(cfgFile) if err := os.MkdirAll(dir, 0755); err != nil { return err } cfg := ini.Empty() if _, err := os.Stat(cfgFile); err == nil { if c, err := ini.Load(cfgFile); err == nil { cfg = c } } sec := cfg.Section("Log") sec.Key("Level").SetValue(cfgLog.Level.String()) sec.Key("Target").SetValue(cfgLog.Target.String()) sec.Key("MaxFileSize").SetValue(strconv.Itoa(cfgLog.MaxSize)) sec.Key("MaxReserveDays").SetValue(strconv.Itoa(cfgLog.MaxAge)) sec.Key("MaxBackupFileCnts").SetValue(strconv.Itoa(cfgLog.MaxBackups)) sec.Key("IsCompress").SetValue(strconv.FormatBool(cfgLog.Compress)) tmp := cfgFile + ".tmp" if err := cfg.SaveTo(tmp); err != nil { return err } f, err := os.OpenFile(tmp, os.O_RDWR, 0644) if err != nil { return err } if err := f.Sync(); err != nil { f.Close() return err } f.Close() if err := os.Rename(tmp, cfgFile); err != nil { return err } if df, err := os.Open(dir); err == nil { _ = df.Sync() df.Close() } return nil } func ExitLogger() { logMu.Lock() defer logMu.Unlock() if Logger != nil { Logger.SetOutput(io.Discard) // 不输出日志 } if logFileWriter != nil { logFileWriter.Close() // 关闭日志文件, 只是将日志提交到内核缓存区, 如果要强制落盘, 避免日志丢失, // 需要修改开源库代码, 在关闭函数中调用"l.file.Sync()"方法并等待一会儿, 20251021 by niujiuru. logFileWriter = nil } if initialized.Load() { initialized.Store(false) } } //export LogFromGo func LogFromGo(level *C.char, file *C.char, line C.int, message *C.char) { if !initialized.Load() { return } goLevel := C.GoString(level) goMessage := C.GoString(message) if (cfgLog.Level == LEVEL_TRACE || cfgLog.Level == LEVEL_DEBUG) && line > 0 { gofile := filepath.Base(C.GoString(file)) goline := int(line) goMessage = fmt.Sprintf("(from %s:%d): %s", gofile, goline, goMessage) } switch goLevel { case "trace": Logger.Tracef("%s", goMessage) case "debug": Logger.Debugf("%s", goMessage) case "info": Logger.Infof("%s", goMessage) case "warn": Logger.Warnf("%s", goMessage) case "error": Logger.Errorf("%s", goMessage) case "fatal": Logger.Fatalf("%s", goMessage) } }