| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417 |
- // 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)
- }
- }
|