Files
fun/logger.go
chiyi 4bf991b3e7 fun: HTTP RPC framework with DI, guards, streams and codegen
- Service binding via reflection with 4 method signatures
- Dependency injection (Wired/auto tags) and guards
- Streamable HTTP (NDJSON) streaming responses
- Go/TypeScript client code generation (self-contained, with interceptors)
- Enum support, structured Result, logging
- 16 regression tests covering all fixed bugs
2026-08-16 23:51:58 +08:00

385 lines
8.5 KiB
Go

package fun
import (
"bytes"
"encoding/json"
"fmt"
"os"
"path/filepath"
"runtime"
"sort"
"strconv"
"strings"
"sync"
"time"
)
const (
PanicLevel uint8 = iota
ErrorLevel
WarnLevel
InfoLevel
DebugLevel
TraceLevel
)
var logChan = make(chan string, 100)
var logWg sync.WaitGroup
const (
TerminalMode uint8 = iota
FileMode
)
var logMutex sync.Mutex
type Logger struct {
Level uint8
Mode uint8
MaxSizeFile uint8 //文件最大大小(MB)
MaxNumberFiles uint64 //文件最多数量
ExpireLogsDays uint8 //文件保留时间
LogFilePath string
}
var logger Logger = Logger{
Level: TraceLevel,
Mode: TerminalMode,
MaxSizeFile: 0,
MaxNumberFiles: 0,
ExpireLogsDays: 0,
LogFilePath: "../log",
}
func init() {
go deleteLogWorker()
go logWriterWorker()
}
func logWriterWorker() {
for text := range logChan {
logMutex.Lock()
if logger.Mode == FileMode {
fileLogger(text)
} else {
fmt.Println(text)
}
}
}
func deleteLogWorker() {
cleanupExpiredLogs()
ticker := time.NewTicker(24 * time.Hour)
defer ticker.Stop()
for {
select {
case <-ticker.C:
if logger.Mode == FileMode {
cleanupExpiredLogs()
}
}
}
}
func getLogFilePath() string {
if logger.LogFilePath == "" {
return "./log"
}
return logger.LogFilePath
}
func cleanupExpiredLogs() {
if logger.ExpireLogsDays <= 0 {
return
}
_, err := os.Stat(getLogFilePath())
if os.IsNotExist(err) {
return
}
if err != nil {
return
}
entries, err := os.ReadDir(getLogFilePath())
if err != nil {
return
}
expireDuration := time.Duration(logger.ExpireLogsDays) * 24 * time.Hour
currentTimeMillis := time.Now().UnixMilli()
expireThreshold := currentTimeMillis - expireDuration.Milliseconds()
for _, entry := range entries {
if !entry.IsDir() {
fileNameInfo := getFileNameInfo(entry.Name())
if fileNameInfo.LoggerTime == 0 {
continue
}
if fileNameInfo.LoggerTime < expireThreshold {
fullPath := filepath.Join(getLogFilePath(), entry.Name())
err := os.Remove(fullPath)
if err != nil && !os.IsNotExist(err) {
return
}
}
}
}
}
func getFileNameInfo(name string) fileName {
fileNameParts := strings.Split(name, ".log.")
if len(fileNameParts) != 2 {
deleteLog(name)
return fileName{}
}
dateLayout := "2006-01-02"
dateString := fileNameParts[0]
fileDate, err := time.Parse(dateLayout, dateString)
if err != nil {
deleteLog(name)
return fileName{}
}
indexString := fileNameParts[1]
indexString = strings.TrimSuffix(indexString, ".log")
fileIndex, err := strconv.ParseInt(indexString, 10, 32)
if err != nil {
deleteLog(name)
return fileName{}
}
return fileName{
index: int32(fileIndex),
LoggerTime: fileDate.UnixMilli(),
}
}
type fileName struct {
LoggerTime int64
index int32
}
func deleteLog(name string) {
fullPath := filepath.Join(getLogFilePath(), name)
err := os.Remove(fullPath)
if err != nil && !os.IsNotExist(err) {
return
}
}
func fileLogger(text string) {
_, err := os.Stat(getLogFilePath())
if os.IsNotExist(err) {
err = os.MkdirAll(getLogFilePath(), os.ModePerm)
if err != nil {
return
}
}
currentDate := getCurrentData()
logFileName := currentDate + ".log"
logFilePath := filepath.Join(getLogFilePath(), logFileName)
logFilePath, err = getNextLogFile(getLogFilePath(), currentDate, text)
if err != nil {
return
}
file, err := os.OpenFile(logFilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
return
}
defer func(file *os.File) {
_ = file.Close()
}(file)
_, _ = file.WriteString(text + "\n")
}
func removeOldestLogFile(entries []os.DirEntry) {
if logger.MaxNumberFiles == 0 {
return
}
if uint64(len(entries)) < logger.MaxNumberFiles {
return
}
var newEntries []fileName
for _, v := range entries {
fileNameInfo := getFileNameInfo(v.Name())
if fileNameInfo.LoggerTime != 0 {
newEntries = append(newEntries, fileNameInfo)
}
}
if uint64(len(newEntries)) < logger.MaxNumberFiles {
return
}
delNum := uint64(len(newEntries)) - logger.MaxNumberFiles + 1
sort.Slice(newEntries, func(i, j int) bool {
if newEntries[i].LoggerTime != newEntries[j].LoggerTime {
return newEntries[i].LoggerTime < newEntries[j].LoggerTime
}
return newEntries[i].index < newEntries[j].index
})
for i := 0; i < int(delNum); i++ {
fileName := newEntries[i]
t := time.Unix(0, fileName.LoggerTime*int64(time.Millisecond))
fileNamePath := filepath.Join(getLogFilePath(), t.Format("2006-01-02")+".log."+strconv.Itoa(int(fileName.index)))
deleteLog(fileNamePath)
}
}
// getNextLogFile 获取下一个应该写入的日志文件
func getNextLogFile(dirPath, dateStr string, text string) (string, error) {
entries, err := os.ReadDir(dirPath)
if err != nil {
return filepath.Join(dirPath, dateStr+".log.1"), err
}
var maxIndex int32 = 0
var existingFiles []string
for _, entry := range entries {
if !entry.IsDir() && strings.HasPrefix(entry.Name(), dateStr+".log") {
existingFiles = append(existingFiles, entry.Name())
}
}
if len(existingFiles) == 0 {
removeOldestLogFile(entries)
return filepath.Join(dirPath, dateStr+".log.1"), nil
}
for _, fileName := range existingFiles {
fileNameInfo := getFileNameInfo(fileName)
if fileNameInfo.LoggerTime != 0 && fileNameInfo.index > maxIndex {
maxIndex = fileNameInfo.index
}
}
if maxIndex == 0 {
removeOldestLogFile(entries)
return filepath.Join(dirPath, dateStr+".log.1"), nil
}
if logger.MaxSizeFile > 0 && maxIndex > 0 {
currentFile := filepath.Join(dirPath, fmt.Sprintf("%s.log.%d", dateStr, maxIndex))
if fileInfo, err := os.Stat(currentFile); err == nil {
maxSizeBytes := int64(logger.MaxSizeFile) * 1024 * 1024
if fileInfo.Size()+int64(len(text)) > maxSizeBytes {
removeOldestLogFile(entries)
return filepath.Join(dirPath, fmt.Sprintf("%s.log.%d", dateStr, maxIndex+1)), nil
}
} else {
return "", err
}
}
return filepath.Join(dirPath, fmt.Sprintf("%s.log.%d", dateStr, maxIndex)), nil
}
func ConfigLogger(log Logger) {
logger = log
}
func getCurrentTime() string {
return time.Now().Format("2006-01-02 15:04:05")
}
func getCurrentData() string {
return time.Now().Format("2006-01-02")
}
func getMethodNameLogger() string {
pc, _, _, _ := runtime.Caller(3)
fn := runtime.FuncForPC(pc)
charsToRemove := []string{"(", "*", ")"}
name := fn.Name()
for _, char := range charsToRemove {
name = strings.ReplaceAll(name, char, "")
}
funcName := "[" + padString(strings.ReplaceAll(name, "/", "."), 40) + "] "
return funcName
}
func getLevelName(level uint8) string {
switch level {
case TraceLevel:
return "TRACE"
case DebugLevel:
return "DEBUG"
case InfoLevel:
return "INFO"
case ErrorLevel:
return "ERROR"
case WarnLevel:
return "WARN"
default:
return "PANIC"
}
}
func sendLogWorker(level uint8, message []any) {
if logger.Level >= level {
var text1 strings.Builder
for _, m := range message {
var msgStr string
var temp interface{}
var trimmedStr string
switch v := m.(type) {
case string:
err := json.Unmarshal([]byte(v), &temp)
if err != nil {
msgStr = fmt.Sprintf("%s", v)
break
}
bs, _ := json.Marshal(&temp)
trimmedStr = string(bs)
case []byte:
err := json.Unmarshal(v, &temp)
if err != nil {
msgStr = fmt.Sprintf("%s", v)
break
}
bs, _ := json.Marshal(&temp)
trimmedStr = string(bs)
default:
bs, _ := json.Marshal(v)
err := json.Unmarshal(bs, &temp)
if err != nil {
msgStr = fmt.Sprintf("%v", v)
break
}
trimmedStr = string(bs)
}
switch temp.(type) {
case map[string]any, []any:
var out bytes.Buffer
err := json.Indent(&out, []byte(trimmedStr), "", "\t")
if err != nil {
return
}
msgStr = fmt.Sprintf("\n%s", out.String())
default:
msgStr = fmt.Sprintf("%v", m)
}
text1.WriteString(msgStr + " ")
}
text := "[" + getCurrentTime() + "] [" + padString(getLevelName(level), 7) + "] " + getMethodNameLogger() + text1.String()
logWg.Add(1)
logChan <- text
}
}
func DebugLogger(message ...any) {
sendLogWorker(DebugLevel, message)
}
func InfoLogger(message ...any) {
sendLogWorker(InfoLevel, message)
}
func TraceLogger(message ...any) {
sendLogWorker(TraceLevel, message)
}
func ErrorLogger(message ...any) {
sendLogWorker(ErrorLevel, message)
}
func WarnLogger(message ...any) {
sendLogWorker(WarnLevel, message)
}
func PanicLogger(message ...any) {
sendLogWorker(PanicLevel, message)
}
func padString(str string, totalLength int) string {
return fmt.Sprintf("%-*s", totalLength, str)[0:totalLength]
}