log.go 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  1. // Package baseapp provides application-level logging facilities.
  2. //
  3. // It supports dynamic log level and output target control,
  4. // hot reloading without restarting the process,
  5. // and safe lifecycle management for long-running applications.
  6. //
  7. // The logger is designed for industrial-grade applications,
  8. // providing features like:
  9. // - Console and file output (with log rotation and compression)
  10. // - Atomic-level dynamic log level changes
  11. // - Safe multi-threaded usage via internal synchronization
  12. //
  13. // Author: NiuJiuRu
  14. // Email: niujiuru@qq.com
  15. // Last modified: 2026-06-03
  16. package baseapp
  17. import "C"
  18. import (
  19. "errors"
  20. "fmt"
  21. "io"
  22. "os"
  23. "path/filepath"
  24. "strconv"
  25. "strings"
  26. "sync"
  27. "sync/atomic"
  28. "github.com/sirupsen/logrus"
  29. "gopkg.in/ini.v1"
  30. "gopkg.in/natefinch/lumberjack.v2"
  31. )
  32. // 日志级别
  33. type level uint32
  34. const (
  35. LEVEL_TRACE level = level(logrus.TraceLevel) // trace
  36. LEVEL_DEBUG = level(logrus.DebugLevel) // debug
  37. LEVEL_INFO = level(logrus.InfoLevel) // info
  38. LEVEL_WARN = level(logrus.WarnLevel) // warning
  39. LEVEL_ERROR = level(logrus.ErrorLevel) // error
  40. LEVEL_FATAL = level(logrus.FatalLevel) // fatal, 会结束程序运行, 会自动调用os.Exit(1)
  41. LEVEL_PANIC = level(logrus.PanicLevel) // panic, 会触发程序崩溃, 不处理的话, =程序退出
  42. )
  43. func (l *level) FromString(s string) {
  44. lvl, err := logrus.ParseLevel(strings.TrimSpace(s))
  45. if err == nil {
  46. *l = level(lvl)
  47. } else {
  48. *l = LEVEL_INFO
  49. }
  50. }
  51. func (l level) String() string {
  52. return logrus.Level(l).String()
  53. }
  54. // 输出目标
  55. type target uint32
  56. const (
  57. TARGET_NONE target = iota // 不输出日志
  58. TARGET_CONSOLE target = 1 << iota // 控制台输出
  59. TARGET_FILE // 输出到文件
  60. )
  61. func (t *target) FromString(s string) {
  62. switch strings.ToLower(strings.TrimSpace(s)) {
  63. case "console":
  64. *t = TARGET_CONSOLE
  65. case "file":
  66. *t = TARGET_FILE
  67. case "all":
  68. *t = (TARGET_CONSOLE | TARGET_FILE)
  69. default:
  70. *t = TARGET_NONE
  71. }
  72. }
  73. func (t target) String() string {
  74. if t == TARGET_NONE {
  75. return "none"
  76. }
  77. parts := make([]string, 0, 2)
  78. if (t & TARGET_CONSOLE) != 0 {
  79. parts = append(parts, "console")
  80. }
  81. if (t & TARGET_FILE) != 0 {
  82. parts = append(parts, "file")
  83. }
  84. return strings.Join(parts, ",")
  85. }
  86. // 输出格式
  87. type formatter struct{}
  88. func (f *formatter) Format(entry *logrus.Entry) ([]byte, error) {
  89. msg := ""
  90. pid := os.Getpid()
  91. if entry.Caller != nil {
  92. file := filepath.Base(entry.Caller.File)
  93. line := entry.Caller.Line
  94. 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)
  95. } else {
  96. msg = fmt.Sprintf("[%s, PID: %d] [%s]: %s\n", entry.Time.Format("2006-01-02 15:04:05"), pid, entry.Level.String(), entry.Message)
  97. }
  98. return []byte(msg), nil
  99. }
  100. // 日志配置
  101. type config struct {
  102. // 日志输出级别
  103. Level level
  104. // 日志输出目标
  105. Target target
  106. // 每个日志文件的最大大下, 以"MB"为单位
  107. MaxSize int `ini:"MaxFileSize"`
  108. // 保留日志文件的最大天数, 以"天"为单位
  109. MaxAge int `ini:"MaxReserveDays"`
  110. // 保留的最大备份文件数量, 以"个"为单位
  111. MaxBackups int `ini:"MaxBackupFileCnts"`
  112. // 轮转文件是否压缩(gzip)减少占用的空间
  113. Compress bool `ini:"IsCompress"`
  114. }
  115. var (
  116. Logger *logrus.Logger
  117. logFileWriter *lumberjack.Logger
  118. logMu sync.Mutex
  119. initialized atomic.Bool
  120. cfgFile string
  121. cfgLog = &config{
  122. Level: LEVEL_TRACE,
  123. Target: TARGET_CONSOLE,
  124. MaxSize: 0,
  125. MaxAge: 0,
  126. MaxBackups: 0,
  127. Compress: true,
  128. }
  129. ErrLoggerNotInitialized = errors.New("logger is not initialized")
  130. )
  131. func InitLogger(logCfgFile string) {
  132. logMu.Lock()
  133. defer logMu.Unlock()
  134. if initialized.Load() {
  135. return
  136. }
  137. cfgFile = logCfgFile
  138. if cfgFile == "" {
  139. cfgFile = filepath.Join(CFG_DIR, "config.ini")
  140. }
  141. levelStr := "trace" // ---初始化设置默认的日志级别---
  142. targetStr := "console" // ---初始化设置默认的输出目标---
  143. cfgIni, err := ini.Load(cfgFile) // 从配置文件中加载设置
  144. if err == nil && cfgIni.HasSection("Log") {
  145. tmpCfgLog := *cfgLog
  146. err = cfgIni.Section("Log").MapTo(&tmpCfgLog)
  147. if err != nil {
  148. fmt.Printf("Failed to load the \"[Log]\" section data from the \"%s\" file: %v\n", cfgFile, err)
  149. } else {
  150. *cfgLog = tmpCfgLog
  151. }
  152. if cfgIni.Section("Log").HasKey("Level") {
  153. levelStr = cfgIni.Section("Log").Key("Level").String()
  154. }
  155. if cfgIni.Section("Log").HasKey("Target") {
  156. targetStr = cfgIni.Section("Log").Key("Target").String()
  157. }
  158. }
  159. Logger = logrus.New()
  160. setLogLevelInternal(levelStr) // 1, 设置日志级别
  161. Logger.SetFormatter(new(formatter)) // 2, 设置日志格式
  162. setLogTargetInternal(targetStr) // 3, 设置输出目标
  163. initialized.Store(true) ////// 标记日志模块已初始化完成
  164. }
  165. func GetLogLevel() (string, error) {
  166. logMu.Lock()
  167. defer logMu.Unlock()
  168. if !initialized.Load() {
  169. return "", ErrLoggerNotInitialized
  170. }
  171. return cfgLog.Level.String(), nil
  172. }
  173. func setLogLevelInternal(levelStr string) {
  174. var lvl level
  175. lvl.FromString(levelStr)
  176. cfgLog.Level = lvl
  177. Logger.SetLevel(logrus.Level(cfgLog.Level))
  178. Logger.SetReportCaller(
  179. lvl == LEVEL_TRACE || lvl == LEVEL_DEBUG,
  180. ) // 使得输出日志时能够获取到文件名和行号
  181. }
  182. func UpdateLogLevel(levelStr string) error {
  183. logMu.Lock()
  184. defer logMu.Unlock()
  185. if !initialized.Load() {
  186. return ErrLoggerNotInitialized
  187. }
  188. setLogLevelInternal(levelStr)
  189. return nil
  190. }
  191. func GetLogTarget() (string, error) {
  192. logMu.Lock()
  193. defer logMu.Unlock()
  194. if !initialized.Load() {
  195. return "", ErrLoggerNotInitialized
  196. }
  197. return cfgLog.Target.String(), nil
  198. }
  199. func setLogTargetInternal(targetStr string) {
  200. var t target
  201. t.FromString(targetStr)
  202. cfgLog.Target = t
  203. writers := make([]io.Writer, 0)
  204. if (cfgLog.Target & TARGET_CONSOLE) != 0 {
  205. writers = append(writers, os.Stdout)
  206. }
  207. if (cfgLog.Target & TARGET_FILE) != 0 {
  208. logDir := LOG_DIR
  209. if logDir == "" {
  210. logDir = "."
  211. } else if info, err := os.Stat(logDir); err != nil || !info.IsDir() {
  212. logDir = "."
  213. }
  214. logFile := filepath.Join(logDir, EXEC_FILENAME+".log")
  215. old := logFileWriter
  216. logFileWriter = &lumberjack.Logger{Filename: logFile, MaxSize: cfgLog.MaxSize, MaxAge: cfgLog.MaxAge,
  217. MaxBackups: cfgLog.MaxBackups, Compress: cfgLog.Compress}
  218. if old != nil {
  219. old.Close()
  220. }
  221. writers = append(writers, logFileWriter)
  222. }
  223. if len(writers) > 0 {
  224. multiWriter := io.MultiWriter(writers...)
  225. Logger.SetOutput(multiWriter)
  226. } else {
  227. Logger.SetOutput(io.Discard) // 不输出日志
  228. }
  229. }
  230. func UpdateLogTarget(targetStr string) error {
  231. logMu.Lock()
  232. defer logMu.Unlock()
  233. if !initialized.Load() {
  234. return ErrLoggerNotInitialized
  235. }
  236. setLogTargetInternal(targetStr)
  237. return nil
  238. }
  239. func SaveLogConfig() error {
  240. logMu.Lock()
  241. defer logMu.Unlock()
  242. if !initialized.Load() {
  243. return ErrLoggerNotInitialized
  244. }
  245. dir := filepath.Dir(cfgFile)
  246. if err := os.MkdirAll(dir, 0755); err != nil {
  247. return err
  248. }
  249. cfg := ini.Empty()
  250. if _, err := os.Stat(cfgFile); err == nil {
  251. if c, err := ini.Load(cfgFile); err == nil {
  252. cfg = c
  253. }
  254. }
  255. sec := cfg.Section("Log")
  256. sec.Key("Level").SetValue(cfgLog.Level.String())
  257. sec.Key("Target").SetValue(cfgLog.Target.String())
  258. sec.Key("MaxFileSize").SetValue(strconv.Itoa(cfgLog.MaxSize))
  259. sec.Key("MaxReserveDays").SetValue(strconv.Itoa(cfgLog.MaxAge))
  260. sec.Key("MaxBackupFileCnts").SetValue(strconv.Itoa(cfgLog.MaxBackups))
  261. sec.Key("IsCompress").SetValue(strconv.FormatBool(cfgLog.Compress))
  262. tmp := cfgFile + ".tmp"
  263. if err := cfg.SaveTo(tmp); err != nil {
  264. return err
  265. }
  266. f, err := os.OpenFile(tmp, os.O_RDWR, 0644)
  267. if err != nil {
  268. return err
  269. }
  270. if err := f.Sync(); err != nil {
  271. f.Close()
  272. return err
  273. }
  274. f.Close()
  275. if err := os.Rename(tmp, cfgFile); err != nil {
  276. return err
  277. }
  278. if df, err := os.Open(dir); err == nil {
  279. _ = df.Sync()
  280. df.Close()
  281. }
  282. return nil
  283. }
  284. func ExitLogger() {
  285. logMu.Lock()
  286. defer logMu.Unlock()
  287. if Logger != nil {
  288. Logger.SetOutput(io.Discard) // 不输出日志
  289. }
  290. if logFileWriter != nil {
  291. logFileWriter.Close() // 关闭日志文件, 只是将日志提交到内核缓存区, 如果要强制落盘, 避免日志丢失,
  292. // 需要修改开源库代码, 在关闭函数中调用"l.file.Sync()"方法并等待一会儿, 20251021 by niujiuru.
  293. logFileWriter = nil
  294. }
  295. if initialized.Load() {
  296. initialized.Store(false)
  297. }
  298. }
  299. //export LogFromGo
  300. func LogFromGo(level *C.char, file *C.char, line C.int, message *C.char) {
  301. if !initialized.Load() {
  302. return
  303. }
  304. goLevel := C.GoString(level)
  305. goMessage := C.GoString(message)
  306. logMu.Lock()
  307. lvl := cfgLog.Level
  308. logMu.Unlock()
  309. if (lvl == LEVEL_TRACE || lvl == LEVEL_DEBUG) && line > 0 {
  310. gofile := filepath.Base(C.GoString(file))
  311. goline := int(line)
  312. goMessage = fmt.Sprintf("(from %s:%d): %s", gofile, goline, goMessage)
  313. }
  314. switch goLevel {
  315. case "trace":
  316. Logger.Tracef("%s", goMessage)
  317. case "debug":
  318. Logger.Debugf("%s", goMessage)
  319. case "info":
  320. Logger.Infof("%s", goMessage)
  321. case "warn":
  322. Logger.Warnf("%s", goMessage)
  323. case "error":
  324. Logger.Errorf("%s", goMessage)
  325. case "fatal":
  326. Logger.Fatalf("%s", goMessage)
  327. }
  328. }