🎉 初始化项目
This commit is contained in:
9
server/core/internal/constant.go
Normal file
9
server/core/internal/constant.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package internal
|
||||
|
||||
const (
|
||||
ConfigEnv = "GVA_CONFIG"
|
||||
ConfigDefaultFile = "config.yaml"
|
||||
ConfigTestFile = "config.test.yaml"
|
||||
ConfigDebugFile = "config.debug.yaml"
|
||||
ConfigReleaseFile = "config.release.yaml"
|
||||
)
|
||||
125
server/core/internal/cutter.go
Normal file
125
server/core/internal/cutter.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Cutter 实现 io.Writer 接口
|
||||
// 用于日志切割, strings.Join([]string{director,layout, formats..., level+".log"}, os.PathSeparator)
|
||||
type Cutter struct {
|
||||
level string // 日志级别(debug, info, warn, error, dpanic, panic, fatal)
|
||||
layout string // 时间格式 2006-01-02 15:04:05
|
||||
formats []string // 自定义参数([]string{Director,"2006-01-02", "business"(此参数可不写), level+".log"}
|
||||
director string // 日志文件夹
|
||||
retentionDay int //日志保留天数
|
||||
file *os.File // 文件句柄
|
||||
mutex *sync.RWMutex // 读写锁
|
||||
}
|
||||
|
||||
type CutterOption func(*Cutter)
|
||||
|
||||
// CutterWithLayout 时间格式
|
||||
func CutterWithLayout(layout string) CutterOption {
|
||||
return func(c *Cutter) {
|
||||
c.layout = layout
|
||||
}
|
||||
}
|
||||
|
||||
// CutterWithFormats 格式化参数
|
||||
func CutterWithFormats(format ...string) CutterOption {
|
||||
return func(c *Cutter) {
|
||||
if len(format) > 0 {
|
||||
c.formats = format
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func NewCutter(director string, level string, retentionDay int, options ...CutterOption) *Cutter {
|
||||
rotate := &Cutter{
|
||||
level: level,
|
||||
director: director,
|
||||
retentionDay: retentionDay,
|
||||
mutex: new(sync.RWMutex),
|
||||
}
|
||||
for i := 0; i < len(options); i++ {
|
||||
options[i](rotate)
|
||||
}
|
||||
return rotate
|
||||
}
|
||||
|
||||
// Write satisfies the io.Writer interface. It writes to the
|
||||
// appropriate file handle that is currently being used.
|
||||
// If we have reached rotation time, the target file gets
|
||||
// automatically rotated, and also purged if necessary.
|
||||
func (c *Cutter) Write(bytes []byte) (n int, err error) {
|
||||
c.mutex.Lock()
|
||||
defer func() {
|
||||
if c.file != nil {
|
||||
_ = c.file.Close()
|
||||
c.file = nil
|
||||
}
|
||||
c.mutex.Unlock()
|
||||
}()
|
||||
length := len(c.formats)
|
||||
values := make([]string, 0, 3+length)
|
||||
values = append(values, c.director)
|
||||
if c.layout != "" {
|
||||
values = append(values, time.Now().Format(c.layout))
|
||||
}
|
||||
for i := 0; i < length; i++ {
|
||||
values = append(values, c.formats[i])
|
||||
}
|
||||
values = append(values, c.level+".log")
|
||||
filename := filepath.Join(values...)
|
||||
director := filepath.Dir(filename)
|
||||
err = os.MkdirAll(director, os.ModePerm)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer func() {
|
||||
err := removeNDaysFolders(c.director, c.retentionDay)
|
||||
if err != nil {
|
||||
fmt.Println("清理过期日志失败", err)
|
||||
}
|
||||
}()
|
||||
|
||||
c.file, err = os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return c.file.Write(bytes)
|
||||
}
|
||||
|
||||
func (c *Cutter) Sync() error {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
if c.file != nil {
|
||||
return c.file.Sync()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 增加日志目录文件清理 小于等于零的值默认忽略不再处理
|
||||
func removeNDaysFolders(dir string, days int) error {
|
||||
if days <= 0 {
|
||||
return nil
|
||||
}
|
||||
cutoff := time.Now().AddDate(0, 0, -days)
|
||||
return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.IsDir() && info.ModTime().Before(cutoff) && path != dir {
|
||||
err = os.RemoveAll(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
133
server/core/internal/zap_core.go
Normal file
133
server/core/internal/zap_core.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.echol.cn/loser/st/server/global"
|
||||
"git.echol.cn/loser/st/server/model/system"
|
||||
"git.echol.cn/loser/st/server/service"
|
||||
astutil "git.echol.cn/loser/st/server/utils/ast"
|
||||
"git.echol.cn/loser/st/server/utils/stacktrace"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ZapCore struct {
|
||||
level zapcore.Level
|
||||
zapcore.Core
|
||||
}
|
||||
|
||||
func NewZapCore(level zapcore.Level) *ZapCore {
|
||||
entity := &ZapCore{level: level}
|
||||
syncer := entity.WriteSyncer()
|
||||
levelEnabler := zap.LevelEnablerFunc(func(l zapcore.Level) bool {
|
||||
return l == level
|
||||
})
|
||||
entity.Core = zapcore.NewCore(global.GVA_CONFIG.Zap.Encoder(), syncer, levelEnabler)
|
||||
return entity
|
||||
}
|
||||
|
||||
func (z *ZapCore) WriteSyncer(formats ...string) zapcore.WriteSyncer {
|
||||
cutter := NewCutter(
|
||||
global.GVA_CONFIG.Zap.Director,
|
||||
z.level.String(),
|
||||
global.GVA_CONFIG.Zap.RetentionDay,
|
||||
CutterWithLayout(time.DateOnly),
|
||||
CutterWithFormats(formats...),
|
||||
)
|
||||
if global.GVA_CONFIG.Zap.LogInConsole {
|
||||
multiSyncer := zapcore.NewMultiWriteSyncer(os.Stdout, cutter)
|
||||
return zapcore.AddSync(multiSyncer)
|
||||
}
|
||||
return zapcore.AddSync(cutter)
|
||||
}
|
||||
|
||||
func (z *ZapCore) Enabled(level zapcore.Level) bool {
|
||||
return z.level == level
|
||||
}
|
||||
|
||||
func (z *ZapCore) With(fields []zapcore.Field) zapcore.Core {
|
||||
return z.Core.With(fields)
|
||||
}
|
||||
|
||||
func (z *ZapCore) Check(entry zapcore.Entry, check *zapcore.CheckedEntry) *zapcore.CheckedEntry {
|
||||
if z.Enabled(entry.Level) {
|
||||
return check.AddCore(entry, z)
|
||||
}
|
||||
return check
|
||||
}
|
||||
|
||||
func (z *ZapCore) Write(entry zapcore.Entry, fields []zapcore.Field) error {
|
||||
for i := 0; i < len(fields); i++ {
|
||||
if fields[i].Key == "business" || fields[i].Key == "folder" || fields[i].Key == "directory" {
|
||||
syncer := z.WriteSyncer(fields[i].String)
|
||||
z.Core = zapcore.NewCore(global.GVA_CONFIG.Zap.Encoder(), syncer, z.level)
|
||||
}
|
||||
}
|
||||
// 先写入原日志目标
|
||||
err := z.Core.Write(entry, fields)
|
||||
|
||||
// 捕捉 Error 及以上级别日志并入库,且可提取 zap.Error(err) 的错误内容
|
||||
if entry.Level >= zapcore.ErrorLevel {
|
||||
// 避免与 GORM zap 写入互相递归:跳过由 gorm logger writer 触发的日志
|
||||
if strings.Contains(entry.Caller.File, "gorm_logger_writer.go") {
|
||||
return err
|
||||
}
|
||||
|
||||
form := "后端"
|
||||
level := entry.Level.String()
|
||||
// 生成基础信息
|
||||
info := entry.Message
|
||||
|
||||
// 提取 zap.Error(err) 内容
|
||||
var errStr string
|
||||
for i := 0; i < len(fields); i++ {
|
||||
f := fields[i]
|
||||
if f.Type == zapcore.ErrorType || f.Key == "error" || f.Key == "err" {
|
||||
if f.Interface != nil {
|
||||
errStr = fmt.Sprintf("%v", f.Interface)
|
||||
} else if f.String != "" {
|
||||
errStr = f.String
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if errStr != "" {
|
||||
info = fmt.Sprintf("%s | 错误: %s", info, errStr)
|
||||
}
|
||||
|
||||
// 附加来源与堆栈信息
|
||||
if entry.Caller.File != "" {
|
||||
info = fmt.Sprintf("%s \n 源文件:%s:%d", info, entry.Caller.File, entry.Caller.Line)
|
||||
}
|
||||
stack := entry.Stack
|
||||
if stack != "" {
|
||||
info = fmt.Sprintf("%s \n 调用栈:%s", info, stack)
|
||||
// 解析最终业务调用方,并提取其方法源码
|
||||
if frame, ok := stacktrace.FindFinalCaller(stack); ok {
|
||||
fnName, fnSrc, sLine, eLine, exErr := astutil.ExtractFuncSourceByPosition(frame.File, frame.Line)
|
||||
if exErr == nil {
|
||||
info = fmt.Sprintf("%s \n 最终调用方法:%s:%d (%s lines %d-%d)\n----- 产生日志的方法代码如下 -----\n%s", info, frame.File, frame.Line, fnName, sLine, eLine, fnSrc)
|
||||
} else {
|
||||
info = fmt.Sprintf("%s \n 最终调用方法:%s:%d (%s) | extract_err=%v", info, frame.File, frame.Line, fnName, exErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 使用后台上下文,避免依赖 gin.Context
|
||||
ctx := context.Background()
|
||||
_ = service.ServiceGroupApp.SystemServiceGroup.SysErrorService.CreateSysError(ctx, &system.SysError{
|
||||
Form: &form,
|
||||
Info: &info,
|
||||
Level: level,
|
||||
})
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (z *ZapCore) Sync() error {
|
||||
return z.Core.Sync()
|
||||
}
|
||||
44
server/core/server.go
Normal file
44
server/core/server.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.echol.cn/loser/st/server/global"
|
||||
"git.echol.cn/loser/st/server/initialize"
|
||||
"git.echol.cn/loser/st/server/service/system"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func RunServer() {
|
||||
if global.GVA_CONFIG.System.UseRedis {
|
||||
// 初始化redis服务
|
||||
initialize.Redis()
|
||||
if global.GVA_CONFIG.System.UseMultipoint {
|
||||
initialize.RedisList()
|
||||
}
|
||||
}
|
||||
|
||||
if global.GVA_CONFIG.System.UseMongo {
|
||||
err := initialize.Mongo.Initialization()
|
||||
if err != nil {
|
||||
zap.L().Error(fmt.Sprintf("%+v", err))
|
||||
}
|
||||
}
|
||||
// 从db加载jwt数据
|
||||
if global.GVA_DB != nil {
|
||||
system.LoadAll()
|
||||
}
|
||||
|
||||
Router := initialize.Routers()
|
||||
|
||||
address := fmt.Sprintf(":%d", global.GVA_CONFIG.System.Addr)
|
||||
|
||||
fmt.Printf(`
|
||||
默认自动化文档地址:http://127.0.0.1%s/swagger/index.html
|
||||
默认MCP SSE地址:http://127.0.0.1%s%s
|
||||
默认MCP Message地址:http://127.0.0.1%s%s
|
||||
默认前端文件运行地址:http://127.0.0.1:8080
|
||||
`, address, address, global.GVA_CONFIG.MCP.SSEPath, address, global.GVA_CONFIG.MCP.MessagePath)
|
||||
initServer(address, Router, 10*time.Minute, 10*time.Minute)
|
||||
}
|
||||
60
server/core/server_run.go
Normal file
60
server/core/server_run.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type server interface {
|
||||
ListenAndServe() error
|
||||
Shutdown(context.Context) error
|
||||
}
|
||||
|
||||
// initServer 启动服务并实现优雅关闭
|
||||
func initServer(address string, router *gin.Engine, readTimeout, writeTimeout time.Duration) {
|
||||
// 创建服务
|
||||
srv := &http.Server{
|
||||
Addr: address,
|
||||
Handler: router,
|
||||
ReadTimeout: readTimeout,
|
||||
WriteTimeout: writeTimeout,
|
||||
MaxHeaderBytes: 1 << 20,
|
||||
}
|
||||
|
||||
// 在goroutine中启动服务
|
||||
go func() {
|
||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
fmt.Printf("listen: %s\n", err)
|
||||
zap.L().Error("server启动失败", zap.Error(err))
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
|
||||
// 等待中断信号以优雅地关闭服务器
|
||||
quit := make(chan os.Signal, 1)
|
||||
// kill (无参数) 默认发送 syscall.SIGTERM
|
||||
// kill -2 发送 syscall.SIGINT
|
||||
// kill -9 发送 syscall.SIGKILL,但是无法被捕获,所以不需要添加
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
zap.L().Info("关闭WEB服务...")
|
||||
|
||||
// 设置5秒的超时时间
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
|
||||
defer cancel()
|
||||
|
||||
if err := srv.Shutdown(ctx); err != nil {
|
||||
zap.L().Fatal("WEB服务关闭异常", zap.Error(err))
|
||||
}
|
||||
|
||||
zap.L().Info("WEB服务已关闭")
|
||||
}
|
||||
76
server/core/viper.go
Normal file
76
server/core/viper.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"git.echol.cn/loser/st/server/core/internal"
|
||||
"git.echol.cn/loser/st/server/global"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Viper 配置
|
||||
func Viper() *viper.Viper {
|
||||
config := getConfigPath()
|
||||
|
||||
v := viper.New()
|
||||
v.SetConfigFile(config)
|
||||
v.SetConfigType("yaml")
|
||||
err := v.ReadInConfig()
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("fatal error config file: %w", err))
|
||||
}
|
||||
v.WatchConfig()
|
||||
|
||||
v.OnConfigChange(func(e fsnotify.Event) {
|
||||
fmt.Println("config file changed:", e.Name)
|
||||
if err = v.Unmarshal(&global.GVA_CONFIG); err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
})
|
||||
if err = v.Unmarshal(&global.GVA_CONFIG); err != nil {
|
||||
panic(fmt.Errorf("fatal error unmarshal config: %w", err))
|
||||
}
|
||||
|
||||
// root 适配性 根据root位置去找到对应迁移位置,保证root路径有效
|
||||
global.GVA_CONFIG.AutoCode.Root, _ = filepath.Abs("..")
|
||||
return v
|
||||
}
|
||||
|
||||
// getConfigPath 获取配置文件路径, 优先级: 命令行 > 环境变量 > 默认值
|
||||
func getConfigPath() (config string) {
|
||||
// `-c` flag parse
|
||||
flag.StringVar(&config, "c", "", "choose config file.")
|
||||
flag.Parse()
|
||||
if config != "" { // 命令行参数不为空 将值赋值于config
|
||||
fmt.Printf("您正在使用命令行的 '-c' 参数传递的值, config 的路径为 %s\n", config)
|
||||
return
|
||||
}
|
||||
if env := os.Getenv(internal.ConfigEnv); env != "" { // 判断环境变量 GVA_CONFIG
|
||||
config = env
|
||||
fmt.Printf("您正在使用 %s 环境变量, config 的路径为 %s\n", internal.ConfigEnv, config)
|
||||
return
|
||||
}
|
||||
|
||||
switch gin.Mode() { // 根据 gin 模式文件名
|
||||
case gin.DebugMode:
|
||||
config = internal.ConfigDebugFile
|
||||
case gin.ReleaseMode:
|
||||
config = internal.ConfigReleaseFile
|
||||
case gin.TestMode:
|
||||
config = internal.ConfigTestFile
|
||||
}
|
||||
fmt.Printf("您正在使用 gin 的 %s 模式运行, config 的路径为 %s\n", gin.Mode(), config)
|
||||
|
||||
_, err := os.Stat(config)
|
||||
if err != nil || os.IsNotExist(err) {
|
||||
config = internal.ConfigDefaultFile
|
||||
fmt.Printf("配置文件路径不存在, 使用默认配置文件路径: %s\n", config)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
36
server/core/zap.go
Normal file
36
server/core/zap.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.echol.cn/loser/st/server/core/internal"
|
||||
"git.echol.cn/loser/st/server/global"
|
||||
"git.echol.cn/loser/st/server/utils"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Zap 获取 zap.Logger
|
||||
// Author [SliverHorn](https://github.com/SliverHorn)
|
||||
func Zap() (logger *zap.Logger) {
|
||||
if ok, _ := utils.PathExists(global.GVA_CONFIG.Zap.Director); !ok { // 判断是否有Director文件夹
|
||||
fmt.Printf("create %v directory\n", global.GVA_CONFIG.Zap.Director)
|
||||
_ = os.Mkdir(global.GVA_CONFIG.Zap.Director, os.ModePerm)
|
||||
}
|
||||
levels := global.GVA_CONFIG.Zap.Levels()
|
||||
length := len(levels)
|
||||
cores := make([]zapcore.Core, 0, length)
|
||||
for i := 0; i < length; i++ {
|
||||
core := internal.NewZapCore(levels[i])
|
||||
cores = append(cores, core)
|
||||
}
|
||||
// 构建基础 logger(错误级别的入库逻辑已在自定义 ZapCore 中处理)
|
||||
logger = zap.New(zapcore.NewTee(cores...))
|
||||
// 启用 Error 及以上级别的堆栈捕捉,确保 entry.Stack 可用
|
||||
opts := []zap.Option{zap.AddStacktrace(zapcore.ErrorLevel)}
|
||||
if global.GVA_CONFIG.Zap.ShowLine {
|
||||
opts = append(opts, zap.AddCaller())
|
||||
}
|
||||
logger = logger.WithOptions(opts...)
|
||||
return logger
|
||||
}
|
||||
Reference in New Issue
Block a user