=== 一、结构化 JSON 访问日志(middleware/accesslog.go)=== gin.Default() → gin.New() + AccessLog() + gin.Recovery() - 一行一个 JSON:ts/level/type/method/path/status/latency_ms/bytes/ip/ua - 自动分级:5xx=error / 4xx=warn / 其余=info - jwt 中间件写入的 userId/username 一并输出,便于审计 - 健康检查与 /assets 静音(探针每 10s 一次会淹没真实请求) - Recovery 显式加回(gin.Default 里包含它) ⚠️ Go map 序列化按 key 字母序,ts 会排到最后 → LogJSON 手工拼装, 把 ts/level/type/msg 固定在最前,其余排序输出。 === 二、生产默认 release 模式 === 代码里此前**完全没有 gin.SetMode**,一直跑在 debug: - 每次启动 dump 100+ 行路由表([GIN-debug] GET /api/v1/...) - 每请求有额外开销,且把完整路由暴露在日志里 现在:GIN_MODE 未显式设置且 DEBUG!=true 时自动 SetMode(ReleaseMode), 需要调试仍可用 DEBUG=true 或 GIN_MODE=debug 覆盖。 === 三、实测 === GIN-debug 路由 dump 行数: 0 ✅(修复前 100+) JSON 访问日志: {"ts":"2026-10-27T14:49:28.53Z","level":"info","type":"access","bytes":828, "ip":"172.19.16.92","latency_ms":173.5,"method":"POST", "path":"/api/v1/auth/login","status":200,"ua":"curl/8.4.0"} ✅ 带用户信息: ...,"user_id":1,"username":"admin" ✅ health 日志行数: 0 ✅ 总日志 15 行 / JSON 7 行 / 非 JSON 8 行(全部是启动日志) health ok、Promtail 12/12、lokistack 1/1 ✅ 镜像:lghost/lokistack:latest + v0.1 = sha256:8b25b0a29efa
90 lines
2.6 KiB
Go
90 lines
2.6 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"lokistack/config"
|
|
"lokistack/repository"
|
|
"lokistack/routers"
|
|
"lokistack/seed"
|
|
"lokistack/services"
|
|
)
|
|
|
|
func main() {
|
|
cfg := config.GetConfig()
|
|
|
|
// gin 默认是 debug 模式:每次启动会把上百行路由表 dump 到日志里,
|
|
// 且每个请求有额外开销。生产默认切 release,需要调试时显式设
|
|
// DEBUG=true 或 GIN_MODE=debug 覆盖。
|
|
if os.Getenv("GIN_MODE") == "" && !strings.EqualFold(os.Getenv("DEBUG"), "true") {
|
|
gin.SetMode(gin.ReleaseMode)
|
|
}
|
|
|
|
log.Println("Initializing database...")
|
|
if err := repository.InitDB(); err != nil {
|
|
log.Fatalf("Failed to init DB: %v", err)
|
|
}
|
|
|
|
log.Println("Seeding default data...")
|
|
seed.Seed()
|
|
|
|
// 安全迁移:历史明文 kubeconfig → 加密存储,并清理明文文件
|
|
services.MigrateClusterKubeconfigs()
|
|
|
|
// 孤儿部署状态恢复:进程重启会杀掉进行中的部署 goroutine,
|
|
// 残留的 "deploying" 状态将永远卡住(UI 无停止入口),启动时统一标记为失败。
|
|
repository.GetDB().RecoverOrphanDeployments()
|
|
|
|
services.StartAlertEngine()
|
|
defer services.StopAlertEngine()
|
|
|
|
r := routers.SetupRouter()
|
|
srv := &http.Server{
|
|
Addr: ":" + cfg.ServerPort,
|
|
Handler: r,
|
|
ReadTimeout: 30 * time.Second,
|
|
WriteTimeout: 130 * time.Second,
|
|
IdleTimeout: 120 * time.Second,
|
|
}
|
|
|
|
// 监听中断/终止信号,实现优雅关停:先停 HTTP(拒绝新请求、处理完在途请求),
|
|
// 再 flush 数据库,避免防抖/异步保存丢失最近写入。
|
|
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
|
defer stop()
|
|
|
|
go func() {
|
|
log.Printf("lokistack starting on port %s", cfg.ServerPort)
|
|
log.Printf("API: http://localhost:%s/api/v1", cfg.ServerPort)
|
|
// 初始密码:首次启动日志中的「已生成管理员账号」,或数据卷 initial-admin-password.txt
|
|
log.Printf("首次部署请查看启动日志或 %s/initial-admin-password.txt 获取 admin 初始密码", cfg.DataDir)
|
|
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
log.Fatalf("Server failed: %v", err)
|
|
}
|
|
}()
|
|
|
|
<-ctx.Done()
|
|
log.Println("Shutdown signal received, draining connections...")
|
|
|
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
|
defer cancel()
|
|
if err := srv.Shutdown(shutdownCtx); err != nil {
|
|
log.Printf("HTTP shutdown error: %v", err)
|
|
}
|
|
|
|
log.Println("Flushing database...")
|
|
if err := repository.Shutdown(); err != nil {
|
|
log.Printf("DB shutdown error: %v", err)
|
|
}
|
|
log.Println("lokistack stopped")
|
|
}
|