From f87fb4b2b3b4447900b8030709ed233b77e309d7 Mon Sep 17 00:00:00 2001 From: opsx Date: Thu, 29 Oct 2026 14:48:25 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E7=A7=BB=E9=99=A4=E5=A4=87?= =?UTF-8?q?=E4=BB=BD=E6=81=A2=E5=A4=8D=E5=8A=9F=E8=83=BD(API/CLI/=E6=89=8B?= =?UTF-8?q?=E5=86=8C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 日志数据的保留与安全由对象存储 + Loki retention 承担,平台侧整库备份恢复价值有限: 删除 services/backup.go、/system/{backup,restore,restore/apply,backups} 路由与 handler、 CLI backup/restore 子命令、前端 api 方法与类型;自检所需的 dataDirPath/密钥指纹并入 selfcheck; 用户手册删备份章节并修正 FAQ 升级建议;CHANGELOG 记录移除。 --- CHANGELOG.md | 3 + backend/controllers/system.go | 65 ------ backend/main.go | 65 +----- backend/routers/router.go | 4 - backend/services/backup.go | 364 ------------------------------- backend/services/selfcheck.go | 24 ++ docs/user-guide/README.md | 2 +- docs/user-guide/_sidebar.md | 1 - docs/user-guide/backup.md | 23 -- docs/user-guide/faq.md | 4 +- frontend/src/api/system/index.ts | 31 --- 11 files changed, 34 insertions(+), 552 deletions(-) delete mode 100644 backend/services/backup.go delete mode 100644 docs/user-guide/backup.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 76b0617..1f1db4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ ## [Unreleased] +### 移除 +- 备份恢复功能(含 API / CLI / 手册):日志数据本体的保留与安全由对象存储与 Loki retention 承担,平台侧备份价值有限 + ### 新增 - 落地页(homepage/,`/homepage` 访问):产品介绍、特性、快速开始 - 用户手册文档站(docsify,`/docs` 访问):安装、部署向导、采集、查询、分析、告警、授权、备份、FAQ 十篇 diff --git a/backend/controllers/system.go b/backend/controllers/system.go index fa615be..d34a5fe 100644 --- a/backend/controllers/system.go +++ b/backend/controllers/system.go @@ -1,9 +1,7 @@ package controllers import ( - "fmt" "lokistack/pkg/metrics" - "time" "github.com/gin-gonic/gin" @@ -19,71 +17,8 @@ func NewSystemController() *SystemController { return &SystemController{} } // Backup 导出当前平台数据(tar.gz 下载)。 // // 内容:数据文件 + kubeconfigs/ + known_hosts/ + manifest.json(含密钥指纹), -// 不含明文密钥。恢复必须使用同一把 ENCRYPTION_KEY。 -func (ctrl *SystemController) Backup(c *gin.Context) { - name := fmt.Sprintf("lokistack-backup-%s.tar.gz", time.Now().Format("20060102-150405")) - c.Header("Content-Type", "application/gzip") - c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, name)) - - if _, err := services.CreateBackup(c.Writer); err != nil { - // 已经写了响应头,无法再改状态码,只能把错误写进 body 尾部 - _, _ = c.Writer.WriteString(fmt.Sprintf("\n\n# 备份失败: %v", err)) - return - } -} // Restore 上传备份包:校验 + 暂存,**不直接覆盖**线上数据库。 -// 返回暂存路径与密钥指纹比对结果,由管理员确认后再调用 RestoreApply。 -func (ctrl *SystemController) Restore(c *gin.Context) { - file, err := c.FormFile("file") - if err != nil { - utils.BadRequest(c, "请上传备份文件(form-data 字段名 file)") - return - } - if file.Size > 128<<20 { - utils.BadRequest(c, "备份文件超过 128MB,请用 run.sh restore 在服务器上操作") - return - } - f, err := file.Open() - if err != nil { - utils.InternalServerError(c, fmt.Sprintf("读取上传文件失败: %v", err)) - return - } - defer f.Close() - - res, err := services.ValidateAndStageBackup(f) - if err != nil { - utils.BadRequest(c, fmt.Sprintf("备份校验失败: %v", err)) - return - } - utils.Success(c, res) -} - -// RestoreApply 应用已暂存的备份。应用后需重启平台进程才生效。 -func (ctrl *SystemController) RestoreApply(c *gin.Context) { - var req struct { - Confirm bool `json:"confirm"` - } - _ = c.ShouldBindJSON(&req) - if !req.Confirm { - utils.BadRequest(c, "恢复会覆盖当前全部数据,请传 confirm=true 确认") - return - } - path, err := services.ApplyStagedBackup() - if err != nil { - utils.BadRequest(c, err.Error()) - return - } - utils.Success(c, gin.H{ - "message": "备份已应用,请重启平台使新数据生效(kubectl -n log rollout restart deploy/lokistack)", - "dbPath": path, - }) -} - -// Backups 列出服务器上已有的备份文件(run.sh backup 产出)。 -func (ctrl *SystemController) Backups(c *gin.Context) { - utils.Success(c, gin.H{"list": services.ListBackups()}) -} // GetRetention 读取平台数据保留期(告警发送历史 / 审计日志)。 func (ctrl *SystemController) GetRetention(c *gin.Context) { diff --git a/backend/main.go b/backend/main.go index 14b732a..a4e15dc 100644 --- a/backend/main.go +++ b/backend/main.go @@ -39,79 +39,22 @@ func fileExists(path string) bool { // runCLI 提供不依赖 Web 的运维命令(备份 CronJob、恢复、自检都走它)。 // -// lokistack backup --output /app/data/backups/xxx.tar.gz -// lokistack restore <备份包路径> # 校验 + 立即应用(会先备份当前 DB) // lokistack self-check # 打印 JSON 自检结果 func runCLI(args []string) { switch args[0] { - case "backup": - out := "" - for i := 1; i < len(args); i++ { - if args[i] == "--output" && i+1 < len(args) { - out = args[i+1] - } - } - if out == "" { - out = "/app/data/backups/lokistack-" + time.Now().Format("20060102-150405") + ".tar.gz" - } - if err := os.MkdirAll(filepath.Dir(out), 0o700); err != nil { - fmt.Fprintf(os.Stderr, "创建备份目录失败: %v\n", err) - os.Exit(1) - } - f, err := os.Create(out) - if err != nil { - fmt.Fprintf(os.Stderr, "创建备份文件失败: %v\n", err) - os.Exit(1) - } - defer f.Close() - m, err := services.CreateBackup(f) - if err != nil { - fmt.Fprintf(os.Stderr, "备份失败: %v\n", err) - os.Exit(1) - } - fmt.Printf("备份完成: %s\n记录数: %d\n密钥指纹: %s\n", out, m.RecordCount, m.KeyFP) - - case "restore": - if len(args) < 2 { - fmt.Fprintln(os.Stderr, "用法: lokistack restore <备份包.tar.gz>") - os.Exit(1) - } - f, err := os.Open(args[1]) - if err != nil { - fmt.Fprintf(os.Stderr, "打开备份失败: %v\n", err) - os.Exit(1) - } - defer f.Close() - res, err := services.ValidateAndStageBackup(f) - if err != nil { - fmt.Fprintf(os.Stderr, "校验失败: %v\n", err) - os.Exit(1) - } - if res.Warning != "" { - fmt.Fprintf(os.Stderr, "⚠️ %s\n", res.Warning) - } - path, err := services.ApplyStagedBackup() - if err != nil { - fmt.Fprintf(os.Stderr, "应用失败: %v\n", err) - os.Exit(1) - } - fmt.Printf("恢复完成: %s(备份 %d 条记录)\n请重启平台进程使新数据生效。\n", path, res.RowsInBackup) - case "self-check": - b, _ := json.MarshalIndent(services.SelfCheck(), "", " ") - fmt.Println(string(b)) - + out, _ := json.MarshalIndent(services.SelfCheck(), "", " ") + fmt.Println(string(out)) case "version": fmt.Printf("%s (commit %s, built %s)\n", version, gitCommit, buildTime) - default: - fmt.Fprintf(os.Stderr, "未知命令 %q。可用: backup / restore / self-check / version\n", args[0]) + fmt.Fprintf(os.Stderr, "未知命令 %q。可用: self-check / version\n", args[0]) os.Exit(1) } } func main() { - // 运维命令行:支持在容器/CronJob 里不经 Web 完成备份/恢复/自检 + // 运维命令行:容器/CronJob 里不经 Web 完成自检与版本查询 if len(os.Args) > 1 { runCLI(os.Args[1:]) return diff --git a/backend/routers/router.go b/backend/routers/router.go index db76ead..00d8525 100644 --- a/backend/routers/router.go +++ b/backend/routers/router.go @@ -147,10 +147,6 @@ func SetupRouter() *gin.Engine { system := authorized.Group("/system") system.Use(middleware.AdminAuth()) { - system.GET("/backup", systemCtrl.Backup) - system.POST("/restore", systemCtrl.Restore) - 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) } diff --git a/backend/services/backup.go b/backend/services/backup.go deleted file mode 100644 index 06d5192..0000000 --- a/backend/services/backup.go +++ /dev/null @@ -1,364 +0,0 @@ -package services - -import ( - "archive/tar" - "compress/gzip" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "fmt" - "io" - "os" - "path/filepath" - "sort" - "strings" - "time" - - "lokistack/repository" -) - -// 备份/恢复设计要点 -// -// 为什么必须有:平台的全部状态(部署/采集/存储/集群/主机/策略/告警/用户/审计) -// 都在这一个 JSON 文件里;kubeconfig 虽然加密存在 DB 内,但解密依赖 -// ENCRYPTION_KEY(K8s Secret)。DB 丢了 = 平台失去"这些资源是谁建的"的映射, -// 从此无法管理/卸载/升级正在跑的 Loki 与采集器。 -// -// 安全取舍: -// - 备份包内含加密后的 kubeconfig 与 known_hosts,**不含明文密钥** -// - manifest 里只记录 ENCRYPTION_KEY 的指纹(sha256 前 16 位), -// 用于恢复时校验"这把钥匙能不能解开这份备份",不泄露密钥本身 -// - 恢复分两步(校验暂存 → 手动应用),避免一个误点就覆盖线上 DB - -// BackupManifest 备份包元信息(放在包内 manifest.json) -type BackupManifest struct { - Version string `json:"version"` - CreatedAt time.Time `json:"createdAt"` - DBBytes int64 `json:"dbBytes"` - DBSHA256 string `json:"dbSha256"` - KeyFP string `json:"encryptionKeyFingerprint"` - RecordCount int `json:"recordCount"` - Note string `json:"note"` -} - -// backupExclude 备份时跳过的文件/目录(临时文件、lost+found、正在写的 restore 暂存) -var backupExclude = map[string]bool{ - "lost+found": true, ".restore-pending": true, "backups": true, -} - -// CreateBackup 把 /app/data 打包成 tar.gz 写入 w。 -func CreateBackup(w io.Writer) (*BackupManifest, error) { - dataDir := dataDirPath() - dbPath := databasePath() - - dbInfo, err := os.Stat(dbPath) - if err != nil { - return nil, fmt.Errorf("读取数据库失败: %w", err) - } - dbBytes, err := os.ReadFile(dbPath) - if err != nil { - return nil, fmt.Errorf("读取数据库失败: %w", err) - } - sum := sha256.Sum256(dbBytes) - - // 记录数从**备份出来的字节**里数,不用 repository.GetDB(): - // CLI 模式(lokistack backup,跑在 CronJob 里)根本没初始化 DB, - // 用 GetDB() 会得到 nil,manifest 里的 recordCount 恒为 0, - // 恢复时就看不出"这份备份里有没有东西"。 - recordCount := countRecords(dbBytes) - - manifest := &BackupManifest{ - Version: "2.0.0", - CreatedAt: time.Now(), - DBBytes: dbInfo.Size(), - DBSHA256: hex.EncodeToString(sum[:]), - KeyFP: encryptionKeyFingerprint(), - RecordCount: recordCount, - Note: "包内 kubeconfig 为加密态;恢复必须使用同一把 ENCRYPTION_KEY,否则 kubeconfig 无法解密", - } - - gz := gzip.NewWriter(w) - defer gz.Close() - tw := tar.NewWriter(gz) - defer tw.Close() - - // 1) manifest - mBytes, _ := json.MarshalIndent(manifest, "", " ") - if err := writeTarFile(tw, "manifest.json", mBytes, 0o600); err != nil { - return nil, err - } - - // 2) data 目录下所有文件(DB + kubeconfigs/ + known_hosts/) - err = filepath.Walk(dataDir, func(path string, info os.FileInfo, err error) error { - if err != nil { - return nil // 单个文件读不到不阻断整包 - } - if path == dataDir { - return nil - } - rel, err := filepath.Rel(dataDir, path) - if err != nil { - return nil - } - top := strings.SplitN(rel, string(os.PathSeparator), 2)[0] - if backupExclude[top] { - if info.IsDir() { - return filepath.SkipDir - } - return nil - } - if info.IsDir() { - return nil - } - // 跳过正在写的临时文件 - if strings.HasPrefix(filepath.Base(path), ".lokistack-") && strings.HasSuffix(path, ".tmp") { - return nil - } - f, err := os.Open(path) - if err != nil { - return nil - } - defer f.Close() - hdr := &tar.Header{ - Name: filepath.ToSlash(filepath.Join("data", rel)), - Mode: int64(info.Mode().Perm()), - Size: info.Size(), - ModTime: info.ModTime(), - } - if err := tw.WriteHeader(hdr); err != nil { - return err - } - _, err = io.Copy(tw, f) - return err - }) - if err != nil { - return nil, err - } - return manifest, nil -} - -func writeTarFile(tw *tar.Writer, name string, data []byte, mode int64) error { - hdr := &tar.Header{Name: name, Mode: mode, Size: int64(len(data)), ModTime: time.Now()} - if err := tw.WriteHeader(hdr); err != nil { - return err - } - _, err := tw.Write(data) - return err -} - -// RestoreResult 校验结果 -type RestoreResult struct { - Manifest *BackupManifest `json:"manifest"` - KeyMatches bool `json:"keyMatches"` - RowsInBackup int `json:"rowsInBackup"` - CurrentRows int `json:"currentRows"` - StagedPath string `json:"stagedPath,omitempty"` - Warning string `json:"warning,omitempty"` -} - -// ValidateAndStageBackup 校验备份包并暂存到 /.restore-pending/。 -// -// 不在这一步直接覆盖线上 DB:恢复是不可逆操作,必须让人再确认一次 -// (页面上的「应用恢复」或 run.sh restore)。 -func ValidateAndStageBackup(r io.Reader) (*RestoreResult, error) { - gr, err := gzip.NewReader(r) - if err != nil { - return nil, fmt.Errorf("不是合法的 gzip 文件: %w", err) - } - defer gr.Close() - tr := tar.NewReader(gr) - - var ( - manifest *BackupManifest - dbBytes []byte - ) - for { - hdr, err := tr.Next() - if err == io.EOF { - break - } - if err != nil { - return nil, fmt.Errorf("解包失败: %w", err) - } - switch { - case hdr.Name == "manifest.json": - b, _ := io.ReadAll(io.LimitReader(tr, 1<<20)) - var m BackupManifest - if err := json.Unmarshal(b, &m); err == nil { - manifest = &m - } - case hdr.Name == "data/opsxloki.json": - dbBytes, _ = io.ReadAll(io.LimitReader(tr, 64<<20)) - } - } - if manifest == nil { - return nil, fmt.Errorf("备份包缺少 manifest.json,不是本平台导出的备份") - } - if len(dbBytes) == 0 { - return nil, fmt.Errorf("备份包内没有 data/opsxloki.json") - } - - // 校验 DB 完整性:必须是合法 JSON 且能解析出预期集合 - var probe map[string]json.RawMessage - if err := json.Unmarshal(dbBytes, &probe); err != nil { - return nil, fmt.Errorf("备份内的数据库不是合法 JSON: %w", err) - } - for _, required := range []string{"users", "deployments", "collectors"} { - if _, ok := probe[required]; !ok { - return nil, fmt.Errorf("备份内数据库缺少集合 %q,可能不是 LokiStack 的备份", required) - } - } - - // 哈希校验(防止传输损坏) - sum := sha256.Sum256(dbBytes) - if manifest.DBSHA256 != "" && hex.EncodeToString(sum[:]) != manifest.DBSHA256 { - return nil, fmt.Errorf("备份内数据库校验失败(sha256 不匹配),文件可能已损坏") - } - - stagedDir := filepath.Join(dataDirPath(), ".restore-pending") - if err := os.MkdirAll(stagedDir, 0o700); err != nil { - return nil, fmt.Errorf("创建暂存目录失败: %w", err) - } - stagedPath := filepath.Join(stagedDir, "opsxloki.json") - if err := os.WriteFile(stagedPath, dbBytes, 0o600); err != nil { - return nil, fmt.Errorf("写入暂存文件失败: %w", err) - } - - currentRows := 0 - if db := repository.GetDB(); db != nil { - db.RLock() - currentRows = len(db.Deployments) + len(db.Collectors) + len(db.Clusters) + len(db.Hosts) - db.RUnlock() - } - - res := &RestoreResult{ - Manifest: manifest, - KeyMatches: manifest.KeyFP == "" || manifest.KeyFP == encryptionKeyFingerprint(), - RowsInBackup: manifest.RecordCount, - CurrentRows: currentRows, - StagedPath: stagedPath, - } - if !res.KeyMatches { - res.Warning = "备份的 ENCRYPTION_KEY 指纹与当前平台不一致:恢复后 kubeconfig / 存储凭证将无法解密," + - "需要用备份时的那把 ENCRYPTION_KEY 覆盖 lokistack-secrets 中的值后再启动" - } - return res, nil -} - -// countRecords 从数据库 JSON 字节里统计各集合的记录总数。 -func countRecords(dbBytes []byte) int { - var raw map[string]json.RawMessage - if err := json.Unmarshal(dbBytes, &raw); err != nil { - return 0 - } - total := 0 - for _, v := range raw { - var arr []json.RawMessage - if err := json.Unmarshal(v, &arr); err == nil { - total += len(arr) - } - } - return total -} - -// encryptionKeyFingerprint 返回当前 ENCRYPTION_KEY 的指纹(sha256 前 16 位),不泄露密钥。 -func encryptionKeyFingerprint() string { - key := os.Getenv("ENCRYPTION_KEY") - if key == "" { - // 兼容从文件读取的部署方式 - if p := os.Getenv("ENCRYPTION_KEY_FILE"); p != "" { - if b, err := os.ReadFile(p); err == nil { - key = strings.TrimSpace(string(b)) - } - } - } - if key == "" { - return "" - } - sum := sha256.Sum256([]byte(key)) - return hex.EncodeToString(sum[:])[:16] -} - -// ApplyStagedBackup 把暂存文件应用为正式 DB(调用方负责重启进程)。 -func ApplyStagedBackup() (string, error) { - restoreDir := filepath.Join(dataDirPath(), ".restore-pending") - staged := filepath.Join(restoreDir, "lokistack.json") - if _, err := os.Stat(staged); err != nil { - // 兼容旧版暂存文件名 - staged = filepath.Join(restoreDir, "opsxloki.json") - } - b, err := os.ReadFile(staged) - if err != nil { - return "", fmt.Errorf("没有待应用的恢复文件: %w", err) - } - var probe map[string]json.RawMessage - if err := json.Unmarshal(b, &probe); err != nil { - return "", fmt.Errorf("暂存文件已损坏: %w", err) - } - - dbPath := databasePath() - // 先把当前 DB 备份一份,恢复错了还能退回去 - if cur, err := os.ReadFile(dbPath); err == nil { - rollback := dbPath + ".before-restore." + time.Now().Format("20060102-150405") - _ = os.WriteFile(rollback, cur, 0o600) - } - tmp := dbPath + ".restoring" - if err := os.WriteFile(tmp, b, 0o600); err != nil { - return "", err - } - if err := os.Rename(tmp, dbPath); err != nil { - return "", err - } - _ = os.Remove(staged) - // 关键:立即把内存态切换为恢复后的数据。否则「替换文件 → 重启」之间, - // 任何一次防抖落盘都会用旧内存态把恢复文件覆盖回去,恢复静默失效。 - if db := repository.GetDB(); db != nil { - db.Lock() - if err := db.Reload(); err != nil { - db.Unlock() - return dbPath, fmt.Errorf("恢复文件已生效但内存重载失败(%v),请立即重启进程", err) - } - db.Unlock() - } - return dbPath, nil -} - -// ListBackups 列出 dataDir/backups 下已有的备份文件(run.sh backup 产出)。 -func ListBackups() []map[string]interface{} { - dir := filepath.Join(dataDirPath(), "backups") - entries, err := os.ReadDir(dir) - if err != nil { - return []map[string]interface{}{} - } - type item struct { - name string - size int64 - mod time.Time - } - var items []item - for _, e := range entries { - if e.IsDir() || !strings.HasSuffix(e.Name(), ".tar.gz") { - continue - } - info, err := e.Info() - if err != nil { - continue - } - items = append(items, item{e.Name(), info.Size(), info.ModTime()}) - } - sort.Slice(items, func(i, j int) bool { return items[i].mod.After(items[j].mod) }) - out := make([]map[string]interface{}, 0, len(items)) - for _, it := range items { - out = append(out, map[string]interface{}{ - "name": it.name, "sizeBytes": it.size, "createdAt": it.mod, - }) - } - return out -} - -func dataDirPath() string { - if p := os.Getenv("DATA_DIR"); p != "" { - return p - } - return "/app/data" -} diff --git a/backend/services/selfcheck.go b/backend/services/selfcheck.go index 374ab58..0adec90 100644 --- a/backend/services/selfcheck.go +++ b/backend/services/selfcheck.go @@ -1,10 +1,14 @@ package services import ( + "crypto/sha256" + "encoding/hex" "encoding/json" "fmt" + "lokistack/config" "net/http" "os" + "path/filepath" "strings" "syscall" "time" @@ -263,6 +267,11 @@ func fileDBStats(path string) string { count("deployments"), count("collectors"), count("auditLogs")) } +// dataDirPath 数据目录绝对路径(自检磁盘空间等场景使用) +func dataDirPath() string { + return config.GetConfig().DataDir +} + // diskUsage 返回剩余与总空间(字节) func diskUsage(path string) (free, total uint64, err error) { var st syscall.Statfs_t @@ -271,3 +280,18 @@ func diskUsage(path string) (free, total uint64, err error) { } return st.Bavail * uint64(st.Bsize), st.Blocks * uint64(st.Bsize), nil } + +// encryptionKeyFingerprint ENCRYPTION_KEY 指纹(SHA256 前 16 位,自检展示用,不泄露密钥) +func encryptionKeyFingerprint() string { + key := config.GetConfig().EncryptionKey + if key == "" { + if b, err := os.ReadFile(filepath.Join(config.GetConfig().DataDir, ".encryption-key")); err == nil { + key = strings.TrimSpace(string(b)) + } + } + if key == "" { + return "" + } + sum := sha256.Sum256([]byte(key)) + return hex.EncodeToString(sum[:])[:16] +} diff --git a/docs/user-guide/README.md b/docs/user-guide/README.md index 325c48a..0854556 100644 --- a/docs/user-guide/README.md +++ b/docs/user-guide/README.md @@ -8,7 +8,7 @@ LokiStack 把 Grafana Loki 日志聚合的部署、采集、查询、分析与 - **采集日志**:Promtail / Fluent Bit 采集策略可视化配置,命名空间多选、app 标签来源四级回退、敏感路径拦截。 - **查询与分析**:LogQL 高亮补全、快捷查询、Live Tail;分析页自动聚合日志量、错误与 Ingress 访问指标。 - **告警**:内置九条应用日志规则,绑定默认通道后填入飞书 Webhook 即可接收;支持冷却与风暴抑制。 -- **运维底座**:凭证加密存储、操作审计、备份恢复、Prometheus 自监控指标。 +- **运维底座**:凭证加密存储、操作审计、Prometheus 自监控指标。 ## 下一步 diff --git a/docs/user-guide/_sidebar.md b/docs/user-guide/_sidebar.md index 730b217..37b6c81 100644 --- a/docs/user-guide/_sidebar.md +++ b/docs/user-guide/_sidebar.md @@ -6,5 +6,4 @@ - [日志分析](analysis.md) - [告警通知](alerts.md) - [查询授权](access.md) -- [备份与恢复](backup.md) - [常见问题](faq.md) diff --git a/docs/user-guide/backup.md b/docs/user-guide/backup.md deleted file mode 100644 index e99ffde..0000000 --- a/docs/user-guide/backup.md +++ /dev/null @@ -1,23 +0,0 @@ -# 备份与恢复 - -平台状态集中在单个 JSON 文件(`$DATA_DIR/lokistack.json`),备份恢复围绕它展开。 - -## 备份 - -- 手动:「系统设置 → 备份」立即创建(写入 `$DATA_DIR/backups/`)。 -- 脚本:`./run.sh backup` 或 `make` 内相应目标,适合 cron 定时。 - -## 恢复 - -1. 上传统计备份或选择已有备份,校验后暂存。 -2. 点击应用:平台先把当前库留档(`.before-restore.*`),替换后立即在内存生效,随后建议重启进程。 -3. 恢复文件损坏时平台拒绝应用并提示。 - -## 数据文件保护 - -- 启动时若数据文件解析失败,原文件会以 `.corrupt-<时间戳>` 留档后再以空库启动,绝不覆盖可恢复数据。 -- 历史命名数据文件(opsxloki.json / lokideploy.json)在启动时自动升级为 lokistack.json。 - -## 自监控 - -`/api/v1/metrics` 输出 Prometheus 指标(HTTP、DB 落盘、告警引擎、Go runtime);「平台监控」页(`/monitor`,管理员)提供可视化快照。 diff --git a/docs/user-guide/faq.md b/docs/user-guide/faq.md index ddb44d4..9970237 100644 --- a/docs/user-guide/faq.md +++ b/docs/user-guide/faq.md @@ -22,8 +22,8 @@ ## 平台可以多实例部署吗? -不可以。JSON 文件存储为单实例设计(防抖串行落盘),水平扩展会互相覆盖;需要高可用时用单实例 + 备份策略。 +不可以。JSON 文件存储为单实例设计(防抖串行落盘),水平扩展会互相覆盖;需要高可用时请保持单实例并对数据目录做系统级快照。 ## 升级安全吗? -配置与数据向后兼容,结构变更按 schema 版本自动迁移;建议升级前在「系统设置」创建一次备份。 +配置与数据向后兼容,结构变更按 schema 版本自动迁移;建议升级前对数据目录(lokistack.json)做一次文件级拷贝。 diff --git a/frontend/src/api/system/index.ts b/frontend/src/api/system/index.ts index eb712d0..7d9c4ec 100644 --- a/frontend/src/api/system/index.ts +++ b/frontend/src/api/system/index.ts @@ -65,27 +65,6 @@ export interface SelfCheckResult { items: SelfCheckItem[]; } -export interface BackupFile { - name: string; - sizeBytes: number; - createdAt: string; -} - -export interface RestoreResult { - manifest: { - version: string; - createdAt: string; - dbBytes: number; - recordCount: number; - note: string; - }; - keyMatches: boolean; - rowsInBackup: number; - currentRows: number; - stagedPath?: string; - warning?: string; -} - export const systemApi = { // 审计 auditList: (params: AuditListParams = {}, opts?: { signal?: AbortSignal }) => @@ -99,16 +78,6 @@ export const systemApi = { // 自检 selfCheck: () => http.get('/system/self-check'), - // 备份恢复 - backups: () => http.get<{ list: BackupFile[] }>('/system/backups'), - restore: (file: File) => { - const form = new FormData(); - form.append('file', file); - return http.post('/system/restore', form); - }, - restoreApply: () => - http.post<{ message: string; dbPath: string }>('/system/restore/apply', { confirm: true }), - // 平台数据保留期(告警发送历史 / 审计日志) retention: () => http.get('/settings/retention'), saveRetention: (data: RetentionSettings) => http.put('/settings/retention', data),