feat(app): 实现优雅停机功能
- 新增 shuttingDown 原子布尔标志和 shutdownOnce 保证单次执行 - 添加 DrainTimeout 和 ShutdownTimeout 配置参数,默认 5s 和 30s - 修改 ServeHTTP 拦截新请求返回 503 Service Unavailable - 实现跨平台信号处理:Windows 关窗口和 Linux SIGTERM/SIGINT - 添加 recover 保护避免停机过程中 panic 触发重启 - 优化 HTTP 服务退出判断,排除正常关闭情况 - 新增 log.LoggerNoCaller 用于访问日志等无意义调用方场景 - 更新 README 文档增加优雅停机特性说明和文档链接 - 创建详细的优雅停机文档和测试用例 - 添加 Windows 平台特定信号处理实现 - 新增 .cursor 计划文件记录开发方案
This commit is contained in:
+78
-12
@@ -49,9 +49,9 @@ func NewLogger(logLevel int, logFile string, maxErrors int) *Logger {
|
||||
}
|
||||
|
||||
consoleWriter := zerolog.ConsoleWriter{
|
||||
Out: os.Stderr,
|
||||
TimeFormat: "2006-01-02 15:04:05",
|
||||
NoColor: false,
|
||||
Out: os.Stderr,
|
||||
TimeFormat: "2006-01-02 15:04:05",
|
||||
NoColor: false,
|
||||
FormatLevel: formatLevelColor,
|
||||
FormatCaller: func(i interface{}) string {
|
||||
if i == nil {
|
||||
@@ -89,6 +89,52 @@ func NewLogger(logLevel int, logFile string, maxErrors int) *Logger {
|
||||
return l
|
||||
}
|
||||
|
||||
// NewLoggerNoCaller 创建不带自动 Caller 字段的日志实例(用于访问日志等 caller 无意义的场景)
|
||||
func NewLoggerNoCaller(logLevel int, logFile string, maxErrors int) *Logger {
|
||||
zerolog.CallerMarshalFunc = callerMarshalFunc
|
||||
zerolog.TimeFieldFormat = "2006-01-02 15:04:05"
|
||||
|
||||
var level zerolog.Level
|
||||
if logLevel == 0 {
|
||||
level = zerolog.ErrorLevel
|
||||
} else {
|
||||
level = zerolog.DebugLevel
|
||||
}
|
||||
|
||||
consoleWriter := zerolog.ConsoleWriter{
|
||||
Out: os.Stderr,
|
||||
TimeFormat: "2006-01-02 15:04:05",
|
||||
NoColor: false,
|
||||
FormatLevel: formatLevelColor,
|
||||
}
|
||||
|
||||
var writers []io.Writer
|
||||
writers = append(writers, consoleWriter)
|
||||
|
||||
if logFile != "" {
|
||||
fw := &TemplateFileWriter{pathTemplate: logFile}
|
||||
writers = append(writers, fw)
|
||||
}
|
||||
|
||||
multi := zerolog.MultiLevelWriter(writers...)
|
||||
zl := zerolog.New(multi).
|
||||
Level(level).
|
||||
With().
|
||||
Timestamp().
|
||||
Logger()
|
||||
|
||||
l := &Logger{
|
||||
zl: zl,
|
||||
logLevel: logLevel,
|
||||
maxErrors: maxErrors,
|
||||
}
|
||||
if maxErrors > 0 {
|
||||
l.errors = make([]ErrorRecord, maxErrors)
|
||||
}
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
// NewTestLogger 创建用于测试的静默 Logger(输出到 io.Discard)
|
||||
func NewTestLogger() *Logger {
|
||||
zl := zerolog.New(io.Discard).Level(zerolog.Disabled)
|
||||
@@ -102,9 +148,9 @@ func (l *Logger) SetOutput(w io.Writer) {
|
||||
return
|
||||
}
|
||||
consoleWriter := zerolog.ConsoleWriter{
|
||||
Out: w,
|
||||
TimeFormat: "2006-01-02 15:04:05",
|
||||
NoColor: true,
|
||||
Out: w,
|
||||
TimeFormat: "2006-01-02 15:04:05",
|
||||
NoColor: true,
|
||||
FormatLevel: formatLevelPlain,
|
||||
FormatCaller: func(i interface{}) string {
|
||||
if i == nil {
|
||||
@@ -319,6 +365,8 @@ func callerMarshalFunc(_ uintptr, file string, line int) string {
|
||||
func findCaller(origFile string, origLine int) string {
|
||||
var lastInfraFile string
|
||||
var lastInfraLine int
|
||||
var primaryCaller string
|
||||
var appLayerCaller string
|
||||
|
||||
for i := 1; i < 20; i++ {
|
||||
_, file, line, ok := runtime.Caller(i)
|
||||
@@ -333,13 +381,28 @@ func findCaller(origFile string, origLine int) string {
|
||||
continue
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s:%d", shortFile, line)
|
||||
if primaryCaller == "" {
|
||||
primaryCaller = fmt.Sprintf("%s:%d", shortFile, line)
|
||||
}
|
||||
if appLayerCaller == "" && !strings.Contains(strings.ToLower(file), "hotime") {
|
||||
appLayerCaller = fmt.Sprintf("%s:%d", shortFile, line)
|
||||
}
|
||||
}
|
||||
|
||||
if lastInfraFile != "" {
|
||||
return fmt.Sprintf("%s:%d", lastInfraFile, lastInfraLine)
|
||||
result := ""
|
||||
if primaryCaller != "" {
|
||||
result = primaryCaller
|
||||
} else if lastInfraFile != "" {
|
||||
result = fmt.Sprintf("%s:%d", lastInfraFile, lastInfraLine)
|
||||
} else {
|
||||
result = fmt.Sprintf("%s:%d", shortenPath(origFile), origLine)
|
||||
}
|
||||
return fmt.Sprintf("%s:%d", shortenPath(origFile), origLine)
|
||||
|
||||
if appLayerCaller != "" && result != appLayerCaller {
|
||||
result += " <- " + appLayerCaller
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func formatCaller(file string, line int) string {
|
||||
@@ -361,8 +424,8 @@ func shortenPath(file string) string {
|
||||
|
||||
// isInfrastructureFile 判断文件是否为基础设施(日志/DB/缓存/通用工具)管线,
|
||||
// 这些文件在调用栈中会被跳过,以显示真正的业务调用者。
|
||||
// 框架业务文件(application.go、context.go、code/makecode.go 等)不在此列,
|
||||
// 会作为有意义的调用者返回。
|
||||
// context.go 是 Display/View 的纯中间层,也作为基础设施跳过。
|
||||
// application.go、code/makecode.go 等保留,会作为有意义的调用者返回。
|
||||
func isInfrastructureFile(file string) bool {
|
||||
if strings.HasPrefix(file, "zerolog/") || strings.HasPrefix(file, "zerolog@") {
|
||||
return true
|
||||
@@ -392,6 +455,9 @@ func isInfrastructureFile(file string) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if strings.HasSuffix(file, "/context.go") || strings.HasSuffix(file, "\\context.go") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
|
||||
Reference in New Issue
Block a user