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
This commit is contained in:
@@ -0,0 +1,384 @@
|
||||
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]
|
||||
}
|
||||
Reference in New Issue
Block a user