package routers import ( "net/http" "os" "path/filepath" "strings" "time" "lokistack/controllers" "lokistack/middleware" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" ) func SetupRouter() *gin.Engine { // 用 gin.New() 而不是 gin.Default():Default 自带文本格式的 gin.Logger(), // 这里换成结构化 JSON 访问日志(middleware.AccessLog),便于 Promtail→Loki // 按字段过滤,同时跳过健康检查/静态资源,避免探针轮询把日志量刷满。 // Recovery 必须显式加回来(Default 里包含它),否则 panic 会打挂进程。 r := gin.New() r.Use(middleware.AccessLog(), gin.Recovery()) // CORS cfg := cors.DefaultConfig() cfg.AllowAllOrigins = true cfg.AllowHeaders = []string{"Origin", "Content-Type", "Accept", "Authorization", "X-Requested-With"} cfg.AllowMethods = []string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"} r.Use(cors.New(cfg)) // Controllers authCtrl := controllers.NewAuthController() clusterCtrl := controllers.NewClusterController() hostCtrl := controllers.NewHostController() storageCtrl := controllers.NewStorageController() deploymentCtrl := controllers.NewDeploymentController() collectorCtrl := controllers.NewCollectorController() userCtrl := controllers.NewUserController() ldapCtrl := controllers.NewLdapController() logsCtrl := controllers.NewLogsController() alertsCtrl := controllers.NewAlertsController() registryCtrl := controllers.NewImageRegistryController() // API v1 api := r.Group("/api/v1") // 健康检查(公开访问,用于负载均衡和监控) api.GET("/health", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "status": "ok", "timestamp": time.Now().Unix(), "version": "2.0.0", }) }) // Prometheus metrics 端点(公开访问,可被Prometheus抓取) api.GET("/metrics", func(c *gin.Context) { // 基础metrics输出(符合Prometheus文本格式) c.Header("Content-Type", "text/plain; version=0.0.4; charset=utf-8") c.String(http.StatusOK, "# LokiStack Metrics\n") }) // Public(登录接口挂 IP 限流防爆破:20 次/分钟) api.POST("/auth/login", middleware.LoginRateLimit(20, time.Minute), authCtrl.Login) api.POST("/auth/refresh", authCtrl.RefreshToken) // Auth required authorized := api.Group("") authorized.Use(middleware.JWTAuth()) // Auth routes authorized.GET("/auth/user", authCtrl.GetUser) authorized.POST("/auth/password", authCtrl.ChangePassword) // 日志查询:数据源管理 + Loki 代理查询 logs := authorized.Group("/logs") { logs.GET("/sources", logsCtrl.ListSources) logs.POST("/sources", middleware.AdminAuth(), logsCtrl.CreateSource) logs.PUT("/sources/:id", middleware.AdminAuth(), logsCtrl.UpdateSource) logs.DELETE("/sources/:id", middleware.AdminAuth(), logsCtrl.DeleteSource) logs.POST("/sources/test", middleware.AdminAuth(), logsCtrl.TestSource) logs.GET("/policy-groups", middleware.AdminAuth(), logsCtrl.ListPolicyGroups) logs.POST("/policy-groups", middleware.AdminAuth(), logsCtrl.CreatePolicyGroup) logs.PUT("/policy-groups/:id", middleware.AdminAuth(), logsCtrl.UpdatePolicyGroup) logs.DELETE("/policy-groups/:id", middleware.AdminAuth(), logsCtrl.DeletePolicyGroup) logs.POST("/query", logsCtrl.Query) logs.POST("/context", logsCtrl.Context) logs.GET("/labels", logsCtrl.Labels) logs.GET("/label/:name/values", logsCtrl.LabelValues) logs.GET("/my-scope", logsCtrl.MyScope) logs.POST("/histogram", logsCtrl.Histogram) // 总量统计(index/stats):只读索引,用于算总页数 logs.POST("/stats", logsCtrl.Stats) logs.POST("/metrics", middleware.AdminAuth(), logsCtrl.Metrics) logs.GET("/health", logsCtrl.Health) } // 镜像源(国内加速/海外/私有仓库) authorized.GET("/settings/image-registry", registryCtrl.Get) authorized.PUT("/settings/image-registry", middleware.AdminAuth(), registryCtrl.Save) // 告警:飞书通知配置 + 告警规则 alertNotify := authorized.Group("/alerts") { alertNotify.GET("/notify-config", alertsCtrl.GetNotifyConfig) alertNotify.PUT("/notify-config", middleware.AdminAuth(), alertsCtrl.UpdateNotifyConfig) alertNotify.POST("/notify-config/test", middleware.AdminAuth(), alertsCtrl.TestNotifyConfig) alertNotify.GET("/rules", alertsCtrl.ListRules) alertNotify.POST("/rules", middleware.AdminAuth(), alertsCtrl.CreateRule) alertNotify.PUT("/rules/:id", middleware.AdminAuth(), alertsCtrl.UpdateRule) alertNotify.DELETE("/rules/:id", middleware.AdminAuth(), alertsCtrl.DeleteRule) alertNotify.POST("/check", middleware.AdminAuth(), alertsCtrl.CheckNow) alertNotify.GET("/deliveries", alertsCtrl.Deliveries) alertNotify.GET("/channels", alertsCtrl.ListChannels) alertNotify.POST("/channels", middleware.AdminAuth(), alertsCtrl.SaveChannel) alertNotify.PUT("/channels/:id", middleware.AdminAuth(), alertsCtrl.SaveChannel) alertNotify.DELETE("/channels/:id", middleware.AdminAuth(), alertsCtrl.DeleteChannel) alertNotify.POST("/channels/test", middleware.AdminAuth(), alertsCtrl.TestChannel) } // LDAP 配置(admin 读写;查询供用户页判断导入按钮显隐,登录用户即可读) authorized.GET("/settings/ldap", middleware.AdminAuth(), ldapCtrl.GetConfig) authorized.PUT("/settings/ldap", middleware.AdminAuth(), ldapCtrl.UpdateConfig) authorized.POST("/settings/ldap/test", middleware.AdminAuth(), ldapCtrl.TestConnection) authorized.POST("/settings/ldap/users", middleware.AdminAuth(), ldapCtrl.ListUsers) // Users(admin 专用) users := authorized.Group("/users") users.Use(middleware.AdminAuth()) { users.GET("", userCtrl.List) users.POST("", userCtrl.Create) users.DELETE("/:id", userCtrl.Delete) users.POST("/batch-delete", userCtrl.BatchDelete) users.POST("/:id/reset-password", userCtrl.ResetPassword) users.POST("/batch-import-ldap", userCtrl.BatchImportLDAP) } // 普通用户仅可访问日志查询及自己的账号接口;管理资源统一在服务端保护。 authorized.Use(middleware.AdminAuth()) // Clusters authorized.GET("/clusters", clusterCtrl.List) authorized.GET("/clusters/:id", clusterCtrl.Get) authorized.POST("/clusters", clusterCtrl.Create) authorized.PUT("/clusters/:id", clusterCtrl.Update) authorized.DELETE("/clusters/:id", clusterCtrl.Delete) authorized.POST("/clusters/:id/validate", clusterCtrl.Validate) authorized.GET("/clusters/:id/info", clusterCtrl.GetInfo) authorized.GET("/clusters/:id/namespaces", clusterCtrl.Namespaces) authorized.GET("/clusters/:id/namespaces/:namespace/deployments", clusterCtrl.Deployments) // Hosts authorized.GET("/hosts", hostCtrl.List) authorized.GET("/hosts/:id", hostCtrl.Get) authorized.POST("/hosts", hostCtrl.Create) authorized.PUT("/hosts/:id", hostCtrl.Update) authorized.DELETE("/hosts/:id", hostCtrl.Delete) authorized.POST("/hosts/:id/validate", hostCtrl.Validate) authorized.GET("/hosts/:id/info", hostCtrl.GetInfo) // Storage authorized.GET("/storages", storageCtrl.List) authorized.GET("/storages/:id", storageCtrl.Get) authorized.POST("/storages", storageCtrl.Create) authorized.POST("/storages/test", storageCtrl.TestConnection) authorized.PUT("/storages/:id", storageCtrl.Update) authorized.DELETE("/storages/:id", storageCtrl.Delete) authorized.GET("/storages/default", storageCtrl.GetDefault) // Deployments authorized.GET("/deployments", deploymentCtrl.List) authorized.GET("/deployments/:id", deploymentCtrl.Get) authorized.POST("/deployments", deploymentCtrl.Create) authorized.PUT("/deployments/:id", deploymentCtrl.Update) authorized.POST("/deployments/:id/retention", deploymentCtrl.UpdateRetention) authorized.GET("/deployments/:id/config", middleware.AdminAuth(), deploymentCtrl.GetLiveConfig) authorized.DELETE("/deployments/:id", deploymentCtrl.Delete) authorized.POST("/deployments/:id/deploy", deploymentCtrl.Deploy) authorized.POST("/deployments/:id/stop", deploymentCtrl.Stop) authorized.POST("/deployments/:id/restart", deploymentCtrl.Restart) authorized.GET("/deployments/:id/logs", deploymentCtrl.GetLogs) // 资源用量(CPU/内存/限流),页面用来判断是否要扩容 authorized.GET("/deployments/:id/usage", deploymentCtrl.GetUsage) authorized.GET("/deployments/stats", deploymentCtrl.GetStats) authorized.GET("/deployments/preflight", deploymentCtrl.Preflight) authorized.GET("/deployments/:id/ready", deploymentCtrl.Ready) authorized.GET("/deployments/versions", deploymentCtrl.GetVersions) // Collectors authorized.GET("/collectors", collectorCtrl.List) authorized.GET("/collectors/:id", collectorCtrl.Get) authorized.POST("/collectors", collectorCtrl.Create) authorized.PUT("/collectors/:id", collectorCtrl.Update) authorized.DELETE("/collectors/:id", collectorCtrl.Delete) authorized.POST("/collectors/:id/install", collectorCtrl.Install) authorized.POST("/collectors/:id/uninstall", collectorCtrl.Uninstall) // Static files - use executable directory as base distPath := findDistPath() if distPath != "" { r.Static("/assets", filepath.Join(distPath, "assets")) // favicon:NoRoute 会把所有非 /api/ 请求回退到 index.html, // 故此处显式注册 /favicon.svg 与 /favicon.ico,否则浏览器请求图标只会拿到 HTML。 faviconSVG := filepath.Join(distPath, "favicon.svg") serveFavicon := func(c *gin.Context) { if _, err := os.Stat(faviconSVG); err != nil { c.Status(404) return } c.Header("Content-Type", "image/svg+xml") c.File(faviconSVG) } r.GET("/favicon.svg", serveFavicon) r.GET("/favicon.ico", serveFavicon) r.NoRoute(func(c *gin.Context) { if strings.HasPrefix(c.Request.URL.Path, "/api/") { c.JSON(404, gin.H{"code": 404, "message": "API endpoint not found"}) return } c.File(filepath.Join(distPath, "index.html")) }) } return r } // findDistPath finds the frontend dist directory func findDistPath() string { // Try relative to executable if ex, err := os.Executable(); err == nil { execDir := filepath.Dir(ex) // If running via "go run", executable is in temp dir - fall through if !strings.Contains(execDir, "/tmp/go-build") { for _, rel := range []string{"dist", "../dist", "../../dist"} { p := filepath.Join(execDir, rel) if _, err := os.Stat(filepath.Join(p, "index.html")); err == nil { return p } } } } // Try working directory relative paths for _, p := range []string{"./dist", "../dist", "/app/dist"} { if abs, err := filepath.Abs(p); err == nil { if _, err := os.Stat(filepath.Join(abs, "index.html")); err == nil { return abs } } } return "" }