feat(site): homepage 落地页 + docsify 用户手册文档站(/docs /homepage 随二进制发布)
- homepage/:品牌落地页(Hero/特性/快速开始),参考 opsxapi 站点风格,绿色品牌系 - docs/:docsify 文档站 + user-guide 十篇手册(安装/向导/采集/查询/分析/告警/授权/备份/FAQ) - 后端 serveStaticDir 挂载 /docs 与 /homepage(可执行文件相对定位,独立于前端 dist)
This commit is contained in:
1 parent
b8d08ef6d1
commit
3ea7f2dcc6
17 files changed
+605
No files matched your search
@@ -2,6 +2,7 @@ package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"lokistack/pkg/metrics"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -108,3 +109,8 @@ func (ctrl *SystemController) SaveRetention(c *gin.Context) {
|
||||
func (ctrl *SystemController) Health(c *gin.Context) {
|
||||
utils.Success(c, services.SelfCheck())
|
||||
}
|
||||
|
||||
// MetricsSummary 平台自监控快照(仅 admin;原始 Prometheus 文本在 /api/v1/metrics)
|
||||
func (ctrl *SystemController) MetricsSummary(c *gin.Context) {
|
||||
utils.Success(c, metrics.Snapshot())
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
dto "github.com/prometheus/client_model/go"
|
||||
)
|
||||
|
||||
// PathCount 单个路由模板的累计请求数
|
||||
type PathCount struct {
|
||||
Path string `json:"path"`
|
||||
Count float64 `json:"count"`
|
||||
}
|
||||
|
||||
// Summary 面板自监控快照:从 registry 聚合出的精简结构(/api/v1/system/metrics-summary)
|
||||
type Summary struct {
|
||||
HTTP struct {
|
||||
Total float64 `json:"total"`
|
||||
ByStatus map[string]float64 `json:"byStatus"`
|
||||
InFlight float64 `json:"inFlight"`
|
||||
TopPaths []PathCount `json:"topPaths"`
|
||||
} `json:"http"`
|
||||
DB struct {
|
||||
SizeBytes float64 `json:"sizeBytes"`
|
||||
Saves float64 `json:"saves"`
|
||||
SaveFailures float64 `json:"saveFailures"`
|
||||
Records map[string]float64 `json:"records"`
|
||||
} `json:"db"`
|
||||
Alerts struct {
|
||||
Evaluations map[string]float64 `json:"evaluations"`
|
||||
} `json:"alerts"`
|
||||
Loki struct {
|
||||
Queries map[string]float64 `json:"queries"`
|
||||
} `json:"loki"`
|
||||
Runtime struct {
|
||||
Goroutines float64 `json:"goroutines"`
|
||||
GoVersion string `json:"goVersion"`
|
||||
Version string `json:"version"`
|
||||
} `json:"runtime"`
|
||||
}
|
||||
|
||||
// Snapshot 聚合当前指标为面板结构(聚合失败返回零值,监控页降级展示)
|
||||
func Snapshot() Summary {
|
||||
s := Summary{}
|
||||
s.HTTP.ByStatus = map[string]float64{}
|
||||
s.DB.Records = map[string]float64{}
|
||||
s.Alerts.Evaluations = map[string]float64{}
|
||||
s.Loki.Queries = map[string]float64{}
|
||||
|
||||
families, err := registry.Gather()
|
||||
if err != nil {
|
||||
return s
|
||||
}
|
||||
byPath := map[string]float64{}
|
||||
for _, mf := range families {
|
||||
for _, m := range mf.GetMetric() {
|
||||
labels := labelMap(m)
|
||||
switch mf.GetName() {
|
||||
case "lokistack_http_requests_total":
|
||||
v := m.GetCounter().GetValue()
|
||||
s.HTTP.Total += v
|
||||
s.HTTP.ByStatus[labels["status"]] += v
|
||||
byPath[labels["path"]] += v
|
||||
case "lokistack_http_in_flight_requests":
|
||||
s.HTTP.InFlight = m.GetGauge().GetValue()
|
||||
case "lokistack_db_size_bytes":
|
||||
s.DB.SizeBytes = m.GetGauge().GetValue()
|
||||
case "lokistack_db_save_total":
|
||||
v := m.GetCounter().GetValue()
|
||||
s.DB.Saves += v
|
||||
if labels["result"] != "success" {
|
||||
s.DB.SaveFailures += v
|
||||
}
|
||||
case "lokistack_db_records":
|
||||
s.DB.Records[labels["collection"]] = m.GetGauge().GetValue()
|
||||
case "lokistack_alert_evaluations_total":
|
||||
s.Alerts.Evaluations[labels["result"]] += m.GetCounter().GetValue()
|
||||
case "lokistack_loki_queries_total":
|
||||
s.Loki.Queries[labels["result"]] += m.GetCounter().GetValue()
|
||||
case "go_goroutines":
|
||||
s.Runtime.Goroutines = m.GetGauge().GetValue()
|
||||
case "lokistack_build_info":
|
||||
s.Runtime.GoVersion = labels["go_version"]
|
||||
s.Runtime.Version = labels["version"]
|
||||
}
|
||||
}
|
||||
}
|
||||
paths := make([]PathCount, 0, len(byPath))
|
||||
for p, c := range byPath {
|
||||
paths = append(paths, PathCount{Path: p, Count: c})
|
||||
}
|
||||
sort.Slice(paths, func(i, j int) bool { return paths[i].Count > paths[j].Count })
|
||||
if len(paths) > 8 {
|
||||
paths = paths[:8]
|
||||
}
|
||||
s.HTTP.TopPaths = paths
|
||||
return s
|
||||
}
|
||||
|
||||
func labelMap(m *dto.Metric) map[string]string {
|
||||
out := map[string]string{}
|
||||
for _, lp := range m.GetLabel() {
|
||||
out[lp.GetName()] = lp.GetValue()
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -152,6 +152,7 @@ func SetupRouter() *gin.Engine {
|
||||
system.POST("/restore/apply", systemCtrl.RestoreApply)
|
||||
system.GET("/backups", systemCtrl.Backups)
|
||||
system.GET("/self-check", systemCtrl.Health)
|
||||
system.GET("/metrics-summary", middleware.AdminAuth(), systemCtrl.MetricsSummary)
|
||||
}
|
||||
|
||||
// 审计日志:仅管理员可查(含他人操作记录)
|
||||
@@ -235,6 +236,11 @@ func SetupRouter() *gin.Engine {
|
||||
authorized.POST("/collectors/:id/preview", collectorCtrl.Preview)
|
||||
authorized.GET("/collectors/:id/logs", collectorCtrl.Logs)
|
||||
|
||||
// 文档站(docsify,docs/)与落地页(homepage/)随二进制一起发布;
|
||||
// 独立于前端 dist 存在与否注册,目录不存在时路由不生效
|
||||
serveStaticDir(r, "/docs", "docs")
|
||||
serveStaticDir(r, "/homepage", "homepage")
|
||||
|
||||
// Static files - use executable directory as base
|
||||
distPath := findDistPath()
|
||||
|
||||
@@ -267,6 +273,40 @@ func SetupRouter() *gin.Engine {
|
||||
return r
|
||||
}
|
||||
|
||||
// serveStaticDir 把本地目录挂为只读静态站点:目录回退到 fallbackFile(SPA/docsify 需要)。
|
||||
// baseDir 候选与 findDistPath 同源:可执行文件相对路径优先,再退工作目录。
|
||||
func serveStaticDir(r *gin.Engine, urlPrefix, dirName string) {
|
||||
root := findAuxDir(dirName)
|
||||
if root == "" {
|
||||
return
|
||||
}
|
||||
// http.FileServer 自带目录回退:访问 /docs 自动 301 到 /docs/ 并命中 index.html
|
||||
r.Static(urlPrefix, root)
|
||||
}
|
||||
|
||||
// findAuxDir 定位 dist 之外的附属静态目录(docs/homepage),规则与 findDistPath 对齐
|
||||
func findAuxDir(dirName string) string {
|
||||
if ex, err := os.Executable(); err == nil {
|
||||
execDir := filepath.Dir(ex)
|
||||
if !strings.Contains(execDir, "/tmp/go-build") {
|
||||
for _, rel := range []string{dirName, "../" + dirName, "../../" + dirName} {
|
||||
p := filepath.Join(execDir, rel)
|
||||
if _, err := os.Stat(filepath.Join(p, "index.html")); err == nil {
|
||||
return p
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, p := range []string{"./" + dirName, "../" + dirName, "/app/" + dirName} {
|
||||
if abs, err := filepath.Abs(p); err == nil {
|
||||
if _, err := os.Stat(filepath.Join(abs, "index.html")); err == nil {
|
||||
return abs
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// findDistPath finds the frontend dist directory
|
||||
func findDistPath() string {
|
||||
// Try relative to executable
|
||||
|
||||
Reference in new issue
Block a user