feat: 品牌统一 LokiStack + 全站中英 i18n + 用户批量删除 + 主题定时切换与浅色侧栏

- 品牌: Go 模块/数据文件/二进制/构建部署资产全部 opsx→lokistack;前端 localStorage 命名空间迁移(读旧写新,含按用户 id 前缀迁移);修复 nginx upstream 与 Makefile 引用已改名服务的断裂
- i18n: react-i18next 落地,common + 11 个页面域命名空间全站中英(菜单/顶栏/登录/列表页/表单/确认弹窗/状态徽章);设置页语言即时切换;修复词典自注册导致的循环导入白屏
- 用户管理: 批量删除(POST /users/batch-delete,不删自己/最后管理员,部分失败不阻断)+ 行复选/全选/批量工具条
- 主题: 浅色侧栏(中性灰激活态)、四态模式(浅色/深色/跟随系统/定时切换,按时区判定)、顶栏太阳月亮二态、设置页紧凑分段选择器
- UI: 登录记住用户名、CardActionButton 统一五页卡片操作钮、shadcn Badge 替换手写徽章、语义色修正
- 清理: 移除主机 Web 终端(前后端)、k8s-helm/Helm 遗留代码与字段、README 重写精简
- 修复: 侧栏与日志告警 order 重复、路由标题 i18n key 未翻译、词典双逗号等
This commit is contained in:
opsx committed 2026-10-27 17:11:38 +08:00
1 parent 02d1bd87a7
commit f3733f0bfc
201 files changed
+5438 -2388

No files matched your search

+8 -5
View File
@@ -68,15 +68,18 @@ func getEnv(key, defaultVal string) string {
return defaultVal
}
// databasePath 新名优先;旧 lokideploy.json 存在且新文件不存在时沿用旧文件(数据平滑迁移)
// databasePath 新名优先;历史命名的数据文件存在且新文件不存在时沿用旧文件
// (数据平滑迁移,不搬移文件、零丢失风险)
func databasePath(dataDir string) string {
newPath := filepath.Join(dataDir, "opsxloki.json")
newPath := filepath.Join(dataDir, "lokistack.json")
if _, err := os.Stat(newPath); err == nil {
return newPath
}
oldPath := filepath.Join(dataDir, "lokideploy.json")
if _, err := os.Stat(oldPath); err == nil {
return oldPath
for _, legacy := range []string{"opsxloki.json", "lokideploy.json"} {
legacyPath := filepath.Join(dataDir, legacy)
if _, err := os.Stat(legacyPath); err == nil {
return legacyPath
}
}
return newPath
}
+8 -8
View File
@@ -4,16 +4,16 @@ import (
"strconv"
"time"
"opsxloki/dto"
"opsxloki/models"
"opsxloki/repository"
"opsxloki/services"
"opsxloki/utils"
"lokistack/dto"
"lokistack/models"
"lokistack/repository"
"lokistack/services"
"lokistack/utils"
"github.com/gin-gonic/gin"
)
// AlertsController 告警规则 + 飞书通知配置(参照 opsx-sec 通知 API 设计)
// AlertsController 告警规则 + 飞书通知配置
type AlertsController struct{}
func NewAlertsController() *AlertsController { return &AlertsController{} }
@@ -48,7 +48,7 @@ func (ctrl *AlertsController) TestNotifyConfig(c *gin.Context) {
utils.Error(c, 400, "请先保存并启用飞书通知")
return
}
if err := services.SendLarkCard("✅ opsx-loki 通知测试", "green", []string{
if err := services.SendLarkCard("✅ LokiStack 通知测试", "green", []string{
"**时间**: " + time.Now().Format("2006-01-02 15:04:05"),
"**说明**: 管理员在设置页手动触发的连通性测试。",
}); err != nil {
@@ -272,7 +272,7 @@ func (ctrl *AlertsController) TestChannel(c *gin.Context) {
utils.BadRequest(c, "请提供通道 ID 或 webhook 地址")
return
}
if err := services.SendLarkCardToChannel(channelID, "✅ opsx-loki 通道测试", "green", []string{
if err := services.SendLarkCardToChannel(channelID, "✅ LokiStack 通道测试", "green", []string{
"**时间**: " + time.Now().Format("2006-01-02 15:04:05"),
"**说明**: 通道连通性测试。",
}); err != nil {
+6 -6
View File
@@ -1,12 +1,12 @@
package controllers
import (
"opsxloki/dto"
"opsxloki/models"
"opsxloki/pkg/jwt"
"opsxloki/seed"
"opsxloki/services"
"opsxloki/utils"
"lokistack/dto"
"lokistack/models"
"lokistack/pkg/jwt"
"lokistack/seed"
"lokistack/services"
"lokistack/utils"
"github.com/gin-gonic/gin"
)
+4 -4
View File
@@ -2,10 +2,10 @@ package controllers
import (
"context"
"opsxloki/dto"
"opsxloki/models"
"opsxloki/services"
"opsxloki/utils"
"lokistack/dto"
"lokistack/models"
"lokistack/services"
"lokistack/utils"
"time"
"github.com/gin-gonic/gin"
+4 -4
View File
@@ -1,10 +1,10 @@
package controllers
import (
"opsxloki/dto"
"opsxloki/models"
"opsxloki/services"
"opsxloki/utils"
"lokistack/dto"
"lokistack/models"
"lokistack/services"
"lokistack/utils"
"github.com/gin-gonic/gin"
)
+4 -4
View File
@@ -2,10 +2,10 @@ package controllers
import (
"context"
"opsxloki/dto"
"opsxloki/models"
"opsxloki/services"
"opsxloki/utils"
"lokistack/dto"
"lokistack/models"
"lokistack/services"
"lokistack/utils"
"time"
"github.com/gin-gonic/gin"
+4 -24
View File
@@ -1,10 +1,10 @@
package controllers
import (
"opsxloki/dto"
"opsxloki/models"
"opsxloki/services"
"opsxloki/utils"
"lokistack/dto"
"lokistack/models"
"lokistack/services"
"lokistack/utils"
"github.com/gin-gonic/gin"
)
@@ -124,23 +124,3 @@ func (ctrl *HostController) GetInfo(c *gin.Context) {
}
utils.Success(c, info)
}
func (ctrl *HostController) Execute(c *gin.Context) {
id, err := utils.IntParam(c, "id")
if err != nil {
utils.BadRequest(c, "ID 无效")
return
}
var req struct {
Command string `json:"command" binding:"required"`
}
if !utils.BindAndValidate(c, &req) {
return
}
stdout, stderr, err := ctrl.service.ExecuteCommand(uint(id), req.Command)
if err != nil {
utils.Error(c, 500, err.Error())
return
}
utils.Success(c, gin.H{"stdout": stdout, "stderr": stderr})
}
+3 -3
View File
@@ -1,9 +1,9 @@
package controllers
import (
"opsxloki/dto"
"opsxloki/services"
"opsxloki/utils"
"lokistack/dto"
"lokistack/services"
"lokistack/utils"
"github.com/gin-gonic/gin"
)
+3 -3
View File
@@ -1,9 +1,9 @@
package controllers
import (
"opsxloki/dto"
"opsxloki/services"
"opsxloki/utils"
"lokistack/dto"
"lokistack/services"
"lokistack/utils"
"github.com/gin-gonic/gin"
)
+3 -3
View File
@@ -3,9 +3,9 @@ package controllers
import (
"strconv"
"opsxloki/models"
"opsxloki/services"
"opsxloki/utils"
"lokistack/models"
"lokistack/services"
"lokistack/utils"
"github.com/gin-gonic/gin"
)
+4 -4
View File
@@ -6,10 +6,10 @@ import (
"strings"
"time"
"opsxloki/dto"
"opsxloki/models"
"opsxloki/services"
"opsxloki/utils"
"lokistack/dto"
"lokistack/models"
"lokistack/services"
"lokistack/utils"
"github.com/gin-gonic/gin"
)
+5 -5
View File
@@ -1,11 +1,11 @@
package controllers
import (
"opsxloki/dto"
"opsxloki/models"
"opsxloki/pkg/crypto"
"opsxloki/services"
"opsxloki/utils"
"lokistack/dto"
"lokistack/models"
"lokistack/pkg/crypto"
"lokistack/services"
"lokistack/utils"
"github.com/gin-gonic/gin"
)
+20 -5
View File
@@ -3,11 +3,11 @@ package controllers
import (
"strconv"
"opsxloki/dto"
"opsxloki/models"
"opsxloki/pkg/jwt"
"opsxloki/services"
"opsxloki/utils"
"lokistack/dto"
"lokistack/models"
"lokistack/pkg/jwt"
"lokistack/services"
"lokistack/utils"
"github.com/gin-gonic/gin"
)
@@ -77,6 +77,21 @@ func (ctrl *UserController) Delete(c *gin.Context) {
utils.Success(c, gin.H{"message": "user deleted"})
}
// BatchDelete 批量删除用户;逐个应用守卫(不能删自己/最后一个 admin),失败项单独返回不阻断整体
func (ctrl *UserController) BatchDelete(c *gin.Context) {
var req dto.BatchDeleteUsersRequest
if !utils.BindAndValidate(c, &req) {
return
}
claims, ok := jwt.GetUserFromContext(c)
if !ok {
utils.Unauthorized(c, "")
return
}
deleted, failures := ctrl.service.BatchDeleteUsers(req.IDs, claims.UserID)
utils.Success(c, gin.H{"deleted": deleted, "failures": failures})
}
// ResetPassword 管理员重置密码(递增 TokenVersion,目标用户旧会话全部失效)
func (ctrl *UserController) ResetPassword(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
+1 -1
View File
@@ -1,6 +1,6 @@
package dto
import "opsxloki/services"
import "lokistack/services"
type ImageRegistryRequest struct {
Mode string `json:"mode" binding:"required,oneof=overseas cn custom"`
+1 -1
View File
@@ -1,6 +1,6 @@
package dto
import "opsxloki/models"
import "lokistack/models"
// 日志查询域
type LogSourceRequest struct {
+4
View File
@@ -11,3 +11,7 @@ type UserCreateRequest struct {
type ResetPasswordRequest struct {
NewPassword string `json:"newPassword" binding:"required,min=6"`
}
type BatchDeleteUsersRequest struct {
IDs []uint `json:"ids" binding:"required,min=1,dive"`
}
+1 -1
View File
@@ -1,4 +1,4 @@
module opsxloki
module lokistack
go 1.24.0
+7 -7
View File
@@ -9,11 +9,11 @@ import (
"syscall"
"time"
"opsxloki/config"
"opsxloki/repository"
"opsxloki/routers"
"opsxloki/seed"
"opsxloki/services"
"lokistack/config"
"lokistack/repository"
"lokistack/routers"
"lokistack/seed"
"lokistack/services"
)
func main() {
@@ -52,7 +52,7 @@ func main() {
defer stop()
go func() {
log.Printf("opsx-loki starting on port %s", cfg.ServerPort)
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)
@@ -74,5 +74,5 @@ func main() {
if err := repository.Shutdown(); err != nil {
log.Printf("DB shutdown error: %v", err)
}
log.Println("LokiDeploy stopped")
log.Println("lokistack stopped")
}
+4 -4
View File
@@ -3,10 +3,10 @@ package middleware
import (
"strings"
"opsxloki/models"
"opsxloki/pkg/jwt"
"opsxloki/repository"
"opsxloki/utils"
"lokistack/models"
"lokistack/pkg/jwt"
"lokistack/repository"
"lokistack/utils"
"github.com/gin-gonic/gin"
)
+2 -2
View File
@@ -5,12 +5,12 @@ import (
"sync"
"time"
"opsxloki/utils"
"lokistack/utils"
"github.com/gin-gonic/gin"
)
// ipLimiter 内存滑动窗口 IP 限流(借鉴 opsx-web-lite rate_limit.go 的简化版)。
// ipLimiter 内存滑动窗口 IP 限流(简化实现)。
// 用于登录防爆破:超限返回 429。单实例部署下内存实现即可,无需外部存储。
type ipLimiter struct {
mu sync.Mutex
+1 -1
View File
@@ -3,7 +3,7 @@ package models
import (
"time"
"opsxloki/pkg/crypto"
"lokistack/pkg/crypto"
)
type Cluster struct {
-2
View File
@@ -34,7 +34,6 @@ type LokiDeployment struct {
GrafanaEndpoint string `json:"grafanaEndpoint"`
Status string `json:"status"`
StatusMessage string `json:"statusMessage"`
HelmValues string `json:"helmValues"`
Endpoint string `json:"endpoint"`
AdminUser string `json:"adminUser"`
AdminPassword string `json:"adminPassword"`
@@ -76,7 +75,6 @@ func (d *LokiDeployment) Sanitized() map[string]interface{} {
"grafanaEndpoint": d.GrafanaEndpoint,
"status": d.Status,
"statusMessage": d.StatusMessage,
"helmValues": d.HelmValues,
"endpoint": d.Endpoint,
"adminUser": d.AdminUser,
// AdminPassword 故意省略
+1 -1
View File
@@ -3,7 +3,7 @@ package models
import (
"time"
"opsxloki/pkg/crypto"
"lokistack/pkg/crypto"
)
// SSHAuthConfig SSH认证配置(定义在models包避免循环导入)
+1 -1
View File
@@ -3,7 +3,7 @@ package models
import (
"time"
"opsxloki/pkg/crypto"
"lokistack/pkg/crypto"
)
type StorageConfig struct {
+1 -1
View File
@@ -3,7 +3,7 @@ package models
import "time"
// User TokenVersion 用于服务端会话吊销:改密/管理员重置密码时递增,
// 旧 token(access 与 refresh)随版本不匹配立即失效(借鉴 opsx-web-lite)。
// 旧 token(access 与 refresh)随版本不匹配立即失效。
type User struct {
ID uint `json:"id"`
Username string `json:"username"`
+1 -1
View File
@@ -14,7 +14,7 @@ import (
"io"
"strings"
"opsxloki/config"
"lokistack/config"
"golang.org/x/crypto/argon2"
"golang.org/x/crypto/hkdf"
+1 -1
View File
@@ -10,6 +10,6 @@ import (
func TestMain(m *testing.M) {
os.Setenv("JWT_SECRET", "crypto-pkg-test-secret-32chars!!")
os.Setenv("ENCRYPTION_KEY", "crypto-pkg-test-encryption-32ch!!")
os.Setenv("DATA_DIR", os.TempDir()+"/opsxloki-crypto-pkg-test")
os.Setenv("DATA_DIR", os.TempDir()+"/lokistack-crypto-pkg-test")
os.Exit(m.Run())
}
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"strconv"
"time"
"opsxloki/config"
"lokistack/config"
"github.com/gin-gonic/gin"
jwtv5 "github.com/golang-jwt/jwt/v5"
+1 -1
View File
@@ -10,6 +10,6 @@ import (
func TestMain(m *testing.M) {
os.Setenv("JWT_SECRET", "jwt-pkg-test-secret-32chars-min!!")
os.Setenv("ENCRYPTION_KEY", "jwt-pkg-test-encryption-32chars!!")
os.Setenv("DATA_DIR", os.TempDir()+"/lokideploy-jwt-pkg-test")
os.Setenv("DATA_DIR", os.TempDir()+"/lokistack-jwt-pkg-test")
os.Exit(m.Run())
}
+3 -3
View File
@@ -8,8 +8,8 @@ import (
"sync"
"time"
"opsxloki/config"
"opsxloki/models"
"lokistack/config"
"lokistack/models"
)
// saveRequest 表示一个保存请求,通过channel序列化
@@ -256,7 +256,7 @@ func (d *JsonDB) doSave() error {
// 原子写入:先写入临时文件,再重命名
dir := filepath.Dir(d.dataFile)
tmpFile := filepath.Join(dir, fmt.Sprintf(".opsxloki-%d.tmp", time.Now().UnixNano()))
tmpFile := filepath.Join(dir, fmt.Sprintf(".lokistack-%d.tmp", time.Now().UnixNano()))
if err := os.WriteFile(tmpFile, data, 0600); err != nil {
return err
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"sync"
"testing"
"opsxloki/models"
"lokistack/models"
)
// TestConcurrentSaveNoDataLoss 是并发写安全(B1/B2)的回归测试:
+1 -1
View File
@@ -1,7 +1,7 @@
package repository
import (
"opsxloki/models"
"lokistack/models"
"testing"
)
+2 -2
View File
@@ -4,13 +4,13 @@ import (
"os"
"testing"
"opsxloki/config"
"lokistack/config"
)
// TestMain 为 repository 包预置安全环境变量与临时 DATA_DIR,并管理 JsonDB 生命周期。
// config.GetConfig() 在 JWT_SECRET/ENCRYPTION_KEY 缺失时会 log.Fatal,故须先设置。
func TestMain(m *testing.M) {
dir, err := os.MkdirTemp("", "opsxloki-repository-test")
dir, err := os.MkdirTemp("", "lokistack-repository-test")
if err != nil {
panic(err)
}
+5 -5
View File
@@ -7,8 +7,8 @@ import (
"strings"
"time"
"opsxloki/controllers"
"opsxloki/middleware"
"lokistack/controllers"
"lokistack/middleware"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
@@ -53,7 +53,7 @@ func SetupRouter() *gin.Engine {
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, "# LokiDeploy Metrics\n")
c.String(http.StatusOK, "# LokiStack Metrics\n")
})
// Public(登录接口挂 IP 限流防爆破:20 次/分钟)
@@ -93,7 +93,7 @@ func SetupRouter() *gin.Engine {
authorized.GET("/settings/image-registry", registryCtrl.Get)
authorized.PUT("/settings/image-registry", middleware.AdminAuth(), registryCtrl.Save)
// 告警:飞书通知配置 + 告警规则(参照 opsx-sec)
// 告警:飞书通知配置 + 告警规则
alertNotify := authorized.Group("/alerts")
{
alertNotify.GET("/notify-config", alertsCtrl.GetNotifyConfig)
@@ -125,6 +125,7 @@ func SetupRouter() *gin.Engine {
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)
}
@@ -150,7 +151,6 @@ func SetupRouter() *gin.Engine {
authorized.DELETE("/hosts/:id", hostCtrl.Delete)
authorized.POST("/hosts/:id/validate", hostCtrl.Validate)
authorized.GET("/hosts/:id/info", hostCtrl.GetInfo)
authorized.POST("/hosts/:id/execute", hostCtrl.Execute)
// Storage
authorized.GET("/storages", storageCtrl.List)
+3 -3
View File
@@ -8,8 +8,8 @@ package seed
import (
"time"
"opsxloki/models"
"opsxloki/repository"
"lokistack/models"
"lokistack/repository"
)
type builtinRule struct {
@@ -26,7 +26,7 @@ var builtinRules = []builtinRule{
{name: "Go panic", query: `{namespace=~".+"} |= "panic:"`, windowMin: 15, threshold: 0, cooldown: 60},
{name: "JVM OutOfMemoryError", query: `{namespace=~".+"} |~ "(^|[^a-zA-Z])OutOfMemoryError"`, windowMin: 15, threshold: 0, cooldown: 60},
{name: "StackOverflowError", query: `{namespace=~".+"} |= "StackOverflowError"`, windowMin: 15, threshold: 0, cooldown: 60},
{name: "K8s 节点 NotReady", query: `{app="kube-state-metrics"} |= "NotReady"`, windowMin: 15, threshold: 0, cooldown: 120},
{name: "Kubernetes 节点 NotReady", query: `{app="kube-state-metrics"} |= "NotReady"`, windowMin: 15, threshold: 0, cooldown: 120},
{name: "容器 OOMKilled", query: `{app=~"kube-state-metrics|event-exporter"} |= "OOMKilled"`, windowMin: 15, threshold: 0, cooldown: 120},
// —— 突增类:超过基线阈值才报 ——
{name: "应用错误过多(error > 1000)", query: `{namespace=~".+"} |~ "(?i)\berror\b"`, windowMin: 15, threshold: 1000, cooldown: 60},
+4 -4
View File
@@ -7,9 +7,9 @@ import (
"path/filepath"
"time"
"opsxloki/models"
"opsxloki/pkg/crypto"
"opsxloki/repository"
"lokistack/models"
"lokistack/pkg/crypto"
"lokistack/repository"
)
// InitPasswordFile 首次启动生成的随机管理员密码,存放于数据卷根目录。
@@ -75,7 +75,7 @@ func Seed() {
log.Printf(" 已保存 : %s", path)
log.Printf(" 获取方式:")
log.Printf(" docker compose exec <服务名> cat %s", path)
log.Printf(" kubectl -n <ns> exec deploy/opsx-loki -- cat %s", path)
log.Printf(" kubectl -n <ns> exec deploy/lokistack -- cat %s", path)
log.Printf(" ⚠️ 首次登录后请立即修改密码(改密后本文件自动删除)")
log.Printf("==================================================")
} else {
+2 -2
View File
@@ -11,8 +11,8 @@ import (
"strings"
"time"
"opsxloki/models"
"opsxloki/repository"
"lokistack/models"
"lokistack/repository"
)
const alertCheckInterval = time.Minute
+28 -5
View File
@@ -2,11 +2,11 @@ package services
import (
"fmt"
"opsxloki/models"
"opsxloki/pkg/crypto"
"opsxloki/pkg/jwt"
"opsxloki/repository"
"opsxloki/utils"
"lokistack/models"
"lokistack/pkg/crypto"
"lokistack/pkg/jwt"
"lokistack/repository"
"lokistack/utils"
)
type AuthService struct{}
@@ -182,6 +182,29 @@ func (s *AuthService) ListUsers(page, pageSize int) ([]models.User, int64) {
return users, total
}
// BatchDeleteUsers 批量删除用户;逐个应用 DeleteUser 守卫(不能删自己/最后一个 admin),
// 返回成功数与失败明细(id + 原因),单个失败不阻断其余。
func (s *AuthService) BatchDeleteUsers(ids []uint, requesterID uint) (int, []map[string]interface{}) {
seen := make(map[uint]bool, len(ids))
deleted := 0
failures := make([]map[string]interface{}, 0)
for _, id := range ids {
if seen[id] {
continue
}
seen[id] = true
if err := s.DeleteUser(id, requesterID); err != nil {
failures = append(failures, map[string]interface{}{
"id": id,
"reason": err.Error(),
})
continue
}
deleted++
}
return deleted, failures
}
// DeleteUser 删除用户;不能删除自己,不能删除最后一个管理员
func (s *AuthService) DeleteUser(id, requesterID uint) error {
db := repository.GetDB()
+5 -5
View File
@@ -3,16 +3,16 @@ package services
import (
"context"
"fmt"
"opsxloki/pkg/crypto"
"lokistack/pkg/crypto"
"os"
"path/filepath"
"strings"
"time"
"opsxloki/config"
"opsxloki/models"
"opsxloki/repository"
"opsxloki/utils"
"lokistack/config"
"lokistack/models"
"lokistack/repository"
"lokistack/utils"
)
type ClusterService struct{}
+1 -1
View File
@@ -7,7 +7,7 @@ import (
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/tools/clientcmd"
"opsxloki/models"
"lokistack/models"
"sort"
"strings"
"time"
+1 -1
View File
@@ -9,7 +9,7 @@ import (
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"opsxloki/models"
"lokistack/models"
)
type ClusterDeploymentOption struct {
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"fmt"
"net/http"
"net/http/httptest"
"opsxloki/models"
"lokistack/models"
"testing"
)
+12 -5
View File
@@ -6,9 +6,9 @@ import (
"strings"
"time"
"opsxloki/models"
"opsxloki/repository"
"opsxloki/utils"
"lokistack/models"
"lokistack/repository"
"lokistack/utils"
)
type CollectorService struct{}
@@ -75,7 +75,14 @@ func (s *CollectorService) generateConfigSnapshot(c *models.Collector) string {
case "host-promtail":
return PromtailConfig(endpoint, c.LogPath, "host", c.Labels)
case "k8s-daemonset":
return PromtailHelmValues(endpoint, clusterName, c.Labels)
ns := c.Namespace
if ns == "" && c.LokiDeployment != nil {
ns = c.LokiDeployment.Namespace
}
if ns == "" {
ns = "log"
}
return PromtailDaemonsetYAML(ns, endpoint, clusterName, c.Labels, c.TargetNamespaces, c.AppLabelSource, c.AppInclude)
case "k8s-fluentbit-sidecar":
host, port := parseLokiURL(endpoint)
app := c.TargetDeployment
@@ -101,7 +108,7 @@ func (s *CollectorService) Create(req *CreateCollectorRequest) (*models.Collecto
return nil, fmt.Errorf("主机采集器需要选择目标主机")
}
if (req.Type == "k8s-daemonset" || req.Type == "k8s-fluentbit-sidecar") && (req.ClusterID == nil || *req.ClusterID == 0) {
return nil, fmt.Errorf("K8s 采集器需要选择目标集群")
return nil, fmt.Errorf("Kubernetes 采集器需要选择目标集群")
}
if req.Type == "k8s-fluentbit-sidecar" && req.TargetDeployment == "" {
return nil, fmt.Errorf("Sidecar 采集器需要填写目标 Deployment 名称")
+3 -3
View File
@@ -8,7 +8,7 @@ import (
"strings"
"time"
"opsxloki/models"
"lokistack/models"
)
// CollectorDeployer 采集器部署器,按 type 分发到三种安装/卸载流程
@@ -178,7 +178,7 @@ func (d *CollectorDeployer) uninstallHost(ctx context.Context) error {
return nil
}
// ===== k8s-daemonset:helm 安装 Promtail DaemonSet =====
// ===== k8s-daemonset:以原生清单安装 Promtail DaemonSet =====
func (d *CollectorDeployer) installDaemonset(ctx context.Context, endpoint string) error {
cluster := d.collector.Cluster
@@ -254,7 +254,7 @@ func (d *CollectorDeployer) uninstallDaemonset(ctx context.Context) error {
ns = "log"
}
// clusterrole / clusterrolebinding 用 loki-promtail;
// 旧版本曾用 opsxloki-promtail,这里一并清理以免升级后留下孤儿资源。
// 数组末两项是历史遗留命名,一并清理避免升级后留下孤儿资源。
for _, item := range [][2]string{
{"daemonset", "promtail"}, {"configmap", "promtail"}, {"serviceaccount", "promtail"},
{"clusterrole", "loki-promtail"}, {"clusterrolebinding", "loki-promtail"},
+3 -114
View File
@@ -83,120 +83,9 @@ scrape_configs:
return strings.Replace(conf, "__EXT_LABELS__", promtailExternalLabels(cluster, labelsJSON), 1)
}
// PromtailHelmValues DaemonSet 模式 Promtail Helm values(生产级)
// 包含:cri解析、kubernetes标签、批处理配置、资源限制
func PromtailHelmValues(lokiEndpoint, cluster, labelsJSON string) string {
values := fmt.Sprintf(`config:
clients:
- url: %s/loki/api/v1/push
batchwait: 1s
batchsize: 1048576
timeout: 10s
backoff_config:
min_period: 500ms
max_period: 5m
max_retries: 10
external_labels:
__EXT_LABELS__
positions:
filename: /run/promtail/positions.yaml
scrape_configs:
- job_name: kubernetes-pods
kubernetes_sd_configs:
- role: pod
pipeline_stages:
- cri: {}
- match:
selector: '{app!=""}'
stages:
- docker: {}
- match:
selector: '{app!=""}'
stages:
- labels:
app:
namespace:
pod:
container:
- limit:
rate: 1000
burst: 10000
drop: true
relabel_configs:
- source_labels: [__meta_kubernetes_pod_phase]
regex: (Succeeded|Failed)
action: drop
- source_labels: [__meta_kubernetes_pod_controller_name]
regex: ([0-9a-z-.]+?)(-[0-9a-f]{8,10})?
action: replace
target_label: __tmp_controller_name
- source_labels:
- __meta_kubernetes_pod_label_app_kubernetes_io_name
- __meta_kubernetes_pod_label_app
- __tmp_controller_name
- __meta_kubernetes_pod_name
regex: ^;*([^;]+)(;.*)?$
action: replace
target_label: app
- source_labels: [__meta_kubernetes_pod_annotation_kubernetes_io_config_hash]
regex: .+
action: replace
target_label: config_hash
- action: replace
source_labels: ['__meta_kubernetes_pod_name']
target_label: 'pod'
- action: replace
source_labels: ['__meta_kubernetes_namespace']
target_label: 'namespace'
- action: replace
source_labels: ['__meta_kubernetes_pod_container_name']
target_label: 'container'
- action: replace
source_labels: ['__meta_kubernetes_pod_node_name']
target_label: 'node_name'
- replacement: /var/log/pods/*$1/*.log
separator: /
source_labels: [__meta_kubernetes_pod_uid, __meta_kubernetes_pod_container_name]
target_label: __path__
daemonset:
enabled: true
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 100m
memory: 128Mi
extraVolumes:
- name: varlog
hostPath:
path: /var/log
- name: varlibdockercontainers
hostPath:
path: /var/lib/docker/containers
- name: promtail-run
hostPath:
path: /run/promtail
extraVolumeMounts:
- name: varlog
mountPath: /var/log
readOnly: true
- name: varlibdockercontainers
mountPath: /var/lib/docker/containers
readOnly: true
- name: promtail-run
mountPath: /run/promtail
`, lokiEndpoint)
return strings.Replace(values, "__EXT_LABELS__", promtailExternalLabels(cluster, labelsJSON), 1)
}
// PromtailDaemonsetYAML DaemonSet 模式纯 YAML 安装(SA/RBAC/ConfigMap/DaemonSet),
// 取代 helm 安装 —— 后端无需安装 helm 二进制。
// relabel 体系与主流 helm chart 对齐:namespace/pod/container/node_name + __path__ 按 pod uid 精确匹配。
// 经 Kubernetes API 直接提交,后端无需任何额外二进制。
// relabel 体系与主流 Promtail 部署惯例对齐:namespace/pod/container/node_name + __path__ 按 pod uid 精确匹配。
// sanitizePromLabel 把 K8s 标签名转成 Prometheus/Promtail 的 meta label 片段。
//
// k8s.kuboard.cn/name → k8s_kuboard_cn_name
@@ -365,7 +254,7 @@ data:
# 4) pod name ← 最后兜底,保证 app 永不为空
#
# 不使用 k8s.kuboard.cn/name 之类的平台私有标签:那只是 Kuboard 管理的集群才有,
# 换 Rancher/Lens/Helm/kubectl 部署的集群就不存在,会破坏通用性。
# 换其他方式部署的集群就不存在,会破坏通用性。
# 如集群确实有私有规范标签,可在「采集器 → 编辑」里配置 AppLabelSource,
# 它会作为最高优先级插到下面列表最前(其余仍兜底)。
- source_labels:
+9 -14
View File
@@ -9,10 +9,10 @@ import (
"sync"
"time"
"opsxloki/models"
"opsxloki/repository"
"opsxloki/seed"
"opsxloki/utils"
"lokistack/models"
"lokistack/repository"
"lokistack/seed"
"lokistack/utils"
)
// LokiVersionList 支持的 Loki 版本列表(按推荐程度排序)
@@ -569,19 +569,14 @@ func (s *DeploymentService) loadRelations(d *models.LokiDeployment) {
func (s *DeploymentService) getEndpoint(d *models.LokiDeployment) string {
switch d.Mode {
case "k8s-helm", "k8s-yaml":
case "k8s-yaml":
ns := d.Namespace
if ns == "" {
ns = "loki"
ns = "log"
}
// k8s-yaml manifest 固定创建名为 loki 的 Service;
// k8s-helm (grafana/loki) 以 release 名(=部署名,默认 loki)暴露 Service。
// k8s-yaml manifest 固定创建名为 loki 的 Service。
// 注意:不使用 loki-gateway,gateway 仅在 Distributed+gateway 启用时存在。
svc := "loki"
if d.Mode == "k8s-helm" && d.Name != "" {
svc = d.Name
}
return fmt.Sprintf("http://%s.%s.svc.cluster.local:3100", svc, ns)
return fmt.Sprintf("http://loki.%s.svc.cluster.local:3100", ns)
case "host-binary":
if d.Host != nil {
return fmt.Sprintf("http://%s:3100", d.Host.HostIP)
@@ -720,7 +715,7 @@ func (s *DeploymentService) recordGrafanaEndpoint(d *models.LokiDeployment) {
}
var endpoint string
switch d.Mode {
case "k8s-helm", "k8s-yaml":
case "k8s-yaml":
if d.Cluster == nil {
s.AddLog(d.ID, "warn", "Grafana 地址记录跳过:未关联集群", "")
return
+5 -1
View File
@@ -68,7 +68,11 @@ func (s *DeploymentService) GetLiveConfig(ctx context.Context, id uint) (*LiveLo
location = dockerConfigPath(d.ID)
output, _, err := client.Execute(ctx, "cat "+location)
if err != nil {
return nil, fmt.Errorf("无法读取主机上的 Loki 配置")
// 历史版本路径兜底(重部署一次后目录即迁移到新命名)
location = legacyDockerConfigDir(d.ID) + "/loki.yaml"
if output, _, err = client.Execute(ctx, "cat "+location); err != nil {
return nil, fmt.Errorf("无法读取主机上的 Loki 配置")
}
}
raw = output
default:
+1 -1
View File
@@ -9,7 +9,7 @@ import (
"testing"
"gopkg.in/yaml.v3"
"opsxloki/models"
"lokistack/models"
)
func TestRedactLokiYAML(t *testing.T) {
@@ -3,7 +3,7 @@ package services
import (
"testing"
"opsxloki/repository"
"lokistack/repository"
)
func TestParseRetentionYAML(t *testing.T) {
+4 -20
View File
@@ -5,10 +5,10 @@ import (
"fmt"
"time"
"opsxloki/models"
"opsxloki/pkg/crypto"
"opsxloki/repository"
"opsxloki/utils"
"lokistack/models"
"lokistack/pkg/crypto"
"lokistack/repository"
"lokistack/utils"
)
type HostService struct{}
@@ -190,19 +190,3 @@ func (s *HostService) Validate(id uint) {
s.Update(id, map[string]interface{}{"status": "active"})
}
}
func (s *HostService) ExecuteCommand(id uint, command string) (string, string, error) {
host, err := s.Get(id)
if err != nil {
return "", "", err
}
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
client, err := NewSSHClientWithAuth(HostSSHAuth(host))
if err != nil {
return "", "", err
}
defer client.Close()
return client.Execute(ctx, command)
}
+2 -2
View File
@@ -9,8 +9,8 @@ import (
"fmt"
"strings"
"opsxloki/models"
"opsxloki/repository"
"lokistack/models"
"lokistack/repository"
)
const (
+1 -1
View File
@@ -9,7 +9,7 @@ import (
appsv1 "k8s.io/api/apps/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"opsxloki/models"
"lokistack/models"
)
const ingressNamespace = "ingress-nginx"
+2 -2
View File
@@ -7,7 +7,7 @@ import (
"regexp"
"time"
"opsxloki/models"
"lokistack/models"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
@@ -54,7 +54,7 @@ func clusterRESTConfig(cluster *models.Cluster, timeout time.Duration) (*rest.Co
return nil, fmt.Errorf("构造 Kubernetes 客户端失败: %w", err)
}
result.Timeout = timeout
result.UserAgent = "LokiDeploy"
result.UserAgent = "lokistack"
return result, nil
}
+1 -1
View File
@@ -20,7 +20,7 @@ import (
kcScheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/restmapper"
"k8s.io/client-go/tools/remotecommand"
"opsxloki/models"
"lokistack/models"
)
func ensureNamespace(ctx context.Context, cluster *models.Cluster, namespace string) error {
+5 -5
View File
@@ -1,6 +1,6 @@
// Package services — LDAP 接入:配置存储(SystemSetting 表 JSON)、连接测试、
// 用户搜索预览、批量导入与登录验证共用同一套连接/搜索代码。
// 设计参照 opsx-web-lite(has_password 脱敏/空密码保留旧值/批量导入冲突策略)。
// 约定:has_password 脱敏、空密码保留旧值、批量导入冲突策略。
package services
import (
@@ -10,10 +10,10 @@ import (
"strings"
"time"
"opsxloki/models"
"opsxloki/pkg/crypto"
"opsxloki/repository"
"opsxloki/utils"
"lokistack/models"
"lokistack/pkg/crypto"
"lokistack/repository"
"lokistack/utils"
ldap "github.com/go-ldap/ldap/v3"
)
+4 -4
View File
@@ -3,9 +3,9 @@ package services
import (
"time"
"opsxloki/models"
"opsxloki/pkg/crypto"
"opsxloki/repository"
"lokistack/models"
"lokistack/pkg/crypto"
"lokistack/repository"
)
// LDAPImportUser 批量导入请求中的用户条目(数据来自预览接口)
@@ -16,7 +16,7 @@ type LDAPImportUser struct {
Email string `json:"email"`
}
// LDAPImportResult 导入结果(对齐 opsx-web-lite 契约)
// LDAPImportResult 导入结果(与前端展示契约对齐)
type LDAPImportResult struct {
SuccessCount int `json:"successCount"`
SkippedCount int `json:"skippedCount"`
+2 -2
View File
@@ -5,8 +5,8 @@ import (
"strings"
"time"
"opsxloki/models"
"opsxloki/repository"
"lokistack/models"
"lokistack/repository"
)
func ListLogPolicyGroups() []models.LogPolicyGroup {
+2 -2
View File
@@ -17,8 +17,8 @@ import (
"strings"
"time"
"opsxloki/models"
"opsxloki/repository"
"lokistack/models"
"lokistack/repository"
"k8s.io/client-go/rest"
)
+1 -1
View File
@@ -1,7 +1,7 @@
package services
import (
"opsxloki/models"
"lokistack/models"
"strings"
"testing"
)
+3 -3
View File
@@ -9,9 +9,9 @@ import (
"strings"
"time"
"opsxloki/models"
"opsxloki/pkg/crypto"
"opsxloki/repository"
"lokistack/models"
"lokistack/pkg/crypto"
"lokistack/repository"
)
// ==================== 数据源 CRUD ====================
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"fmt"
"text/template"
"opsxloki/pkg/crypto"
"lokistack/pkg/crypto"
)
const lokiConfigTemplate = `auth_enabled: {{ .AuthEnabled }}
+4 -4
View File
@@ -7,8 +7,8 @@ import (
"strconv"
"strings"
"opsxloki/models"
"opsxloki/pkg/crypto"
"lokistack/models"
"lokistack/pkg/crypto"
)
// LokiDeployer 负责 Loki 的 Kubernetes、主机 Docker 及历史部署模式。
@@ -169,7 +169,7 @@ WantedBy=multi-user.target
return nil
}
// ==================== Helm Values 生成 ====================
// ==================== 部署参数与版本校验 ====================
func (d *LokiDeployer) effectiveVersion() string {
if d.deployment.Version != "" {
@@ -226,7 +226,7 @@ func CleanupLoki(ctx context.Context, deployment *models.LokiDeployment) {
_ = deleteK8sResource(ctx, deployment.Cluster, item[0], ns, item[1])
}
}
// 历史 Helm release 不由此版本自动删除,避免误删旧数据。
// 仅清理本产品创建的资源;其他途径部署的遗留资源不自动删除,避免误删数据。
if deployment.Mode == "host-binary" && deployment.Host != nil {
host := deployment.Host
+28 -8
View File
@@ -7,13 +7,19 @@ import (
"strings"
"time"
"opsxloki/models"
"lokistack/models"
)
func dockerContainerName(id uint) string { return fmt.Sprintf("lokideploy-loki-%d", id) }
func dockerConfigDir(id uint) string { return fmt.Sprintf("/etc/lokideploy/loki-%d", id) }
func dockerContainerName(id uint) string { return fmt.Sprintf("lokistack-loki-%d", id) }
func dockerConfigDir(id uint) string { return fmt.Sprintf("/etc/lokistack/loki-%d", id) }
func dockerConfigPath(id uint) string { return dockerConfigDir(id) + "/loki.yaml" }
func dockerDataDir(id uint) string { return fmt.Sprintf("/var/lib/lokideploy/loki-%d", id) }
func dockerDataDir(id uint) string { return fmt.Sprintf("/var/lib/lokistack/loki-%d", id) }
// 历史命名的容器与目录 — 仅用于迁移与清理,
// 避免已部署实例在升级后留下孤儿容器/目录。
func legacyDockerContainerName(id uint) string { return fmt.Sprintf("lokideploy-loki-%d", id) }
func legacyDockerConfigDir(id uint) string { return fmt.Sprintf("/etc/lokideploy/loki-%d", id) }
func legacyDockerDataDir(id uint) string { return fmt.Sprintf("/var/lib/lokideploy/loki-%d", id) }
func dockerLokiImage(version string) (string, error) {
if version == "" {
@@ -39,6 +45,8 @@ func stopDockerLoki(ctx context.Context, deployment *models.LokiDeployment) erro
if _, stderr, err := client.Execute(ctx, "docker stop "+dockerContainerName(deployment.ID)); err != nil {
return fmt.Errorf("停止 Loki 容器失败: %v: %s", err, strings.TrimSpace(stderr))
}
// 历史命名的同名容器一并停掉(可能不存在,尽力而为)
client.Execute(ctx, "docker stop "+legacyDockerContainerName(deployment.ID)+" >/dev/null 2>&1 || true")
return nil
}
@@ -52,11 +60,13 @@ func deleteDockerLoki(ctx context.Context, deployment *models.LokiDeployment) er
}
defer client.Close()
name := dockerContainerName(deployment.ID)
legacyName := legacyDockerContainerName(deployment.ID)
if _, _, err := client.Execute(ctx, "docker info >/dev/null 2>&1"); err != nil {
return fmt.Errorf("目标主机 Docker 不可用,无法安全清理部署")
}
// 仅删除该部署的容器和配置;日志数据目录保留,以便人工恢复。
cmd := "if docker container inspect " + name + " >/dev/null 2>&1; then docker rm -f " + name + " >/dev/null || exit 1; fi; rm -rf -- " + dockerConfigDir(deployment.ID)
// 仅删除该部署的容器和配置(含历史命名);日志数据目录保留,以便人工恢复。
cmd := "for n in " + name + " " + legacyName + "; do if docker container inspect $n >/dev/null 2>&1; then docker rm -f $n >/dev/null || exit 1; fi; done; rm -rf -- " +
dockerConfigDir(deployment.ID) + " " + legacyDockerConfigDir(deployment.ID)
if _, stderr, err := client.Execute(ctx, cmd); err != nil {
return fmt.Errorf("清理 Loki 容器与配置失败: %v: %s", err, strings.TrimSpace(stderr))
}
@@ -94,6 +104,16 @@ func (d *LokiDeployer) deployDocker(ctx context.Context) error {
name := dockerContainerName(deployment.ID)
configDir := dockerConfigDir(deployment.ID)
dataDir := dockerDataDir(deployment.ID)
// 历史命名目录原子迁移到新命名;存在才迁,不覆盖已有数据
migrate := fmt.Sprintf(
"if [ -d %s ] && [ ! -d %s ]; then mkdir -p /etc/lokistack && mv %s %s; fi; "+
"if [ -d %s ] && [ ! -d %s ]; then mkdir -p /var/lib/lokistack && mv %s %s; fi",
legacyDockerConfigDir(deployment.ID), configDir, legacyDockerConfigDir(deployment.ID), configDir,
legacyDockerDataDir(deployment.ID), dataDir, legacyDockerDataDir(deployment.ID), dataDir,
)
if _, stderr, err := client.Execute(ctx, migrate); err != nil {
return fmt.Errorf("迁移历史部署目录失败(可手动执行: %s): %v: %s", migrate, err, strings.TrimSpace(stderr))
}
if _, _, err := client.Execute(ctx, "mkdir -p "+configDir+" "+dataDir+" && chmod 755 "+configDir+" && chown 10001:10001 "+dataDir); err != nil {
return fmt.Errorf("创建主机持久化目录失败: %w", err)
}
@@ -118,8 +138,8 @@ func (d *LokiDeployer) deployDocker(ctx context.Context) error {
if _, stderr, err := client.Execute(ctx, "docker pull "+image); err != nil {
return fmt.Errorf("拉取 Loki 镜像失败: %v: %s", err, strings.TrimSpace(stderr))
}
// 同一部署 ID 的重部署只替换容器,配置和日志数据目录保留。
client.Execute(ctx, "docker rm -f "+name+" >/dev/null 2>&1 || true")
// 同一部署 ID 的重部署只替换容器(含历史命名),配置和日志数据目录保留。
client.Execute(ctx, "docker rm -f "+name+" "+legacyDockerContainerName(deployment.ID)+" >/dev/null 2>&1 || true")
port := deployment.ListenPort
if port == 0 {
port = 3100
+2 -2
View File
@@ -4,7 +4,7 @@ import (
"strings"
"testing"
"opsxloki/models"
"lokistack/models"
)
func TestDockerLokiConfigUsesEnvironmentForStorageCredentials(t *testing.T) {
@@ -24,7 +24,7 @@ func TestDockerLokiConfigUsesEnvironmentForStorageCredentials(t *testing.T) {
t.Errorf("config missing %q", expected)
}
}
if got := dockerContainerName(deployment.ID); got != "lokideploy-loki-7" {
if got := dockerContainerName(deployment.ID); got != "lokistack-loki-7" {
t.Fatalf("unexpected container name: %s", got)
}
}
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"encoding/base64"
"fmt"
"opsxloki/pkg/crypto"
"lokistack/pkg/crypto"
)
func (d *LokiDeployer) generateManifest(ns string) string {
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"strings"
"testing"
"opsxloki/models"
"lokistack/models"
)
// TestObjectStoreConfigs 验证 OSS / COS / OBS 生成的 Loki 配置:
+5 -5
View File
@@ -1,6 +1,6 @@
package services
// 飞书/Lark 群机器人通知 — 卡片消息(参照 opsx-sec notifier 与运维告警脚本的卡片结构),
// 飞书/Lark 群机器人通知 — 卡片消息,
// webhook 存 SystemSetting(AES 加密),对外只回 hasWebhook。
import (
"bytes"
@@ -13,9 +13,9 @@ import (
"strings"
"time"
"opsxloki/models"
"opsxloki/pkg/crypto"
"opsxloki/repository"
"lokistack/models"
"lokistack/pkg/crypto"
"lokistack/repository"
)
const (
@@ -116,7 +116,7 @@ func SendLarkCard(title, color string, lines []string) error {
},
"elements": []interface{}{
map[string]interface{}{"tag": "div", "text": map[string]string{"tag": "lark_md", "content": content}},
map[string]interface{}{"tag": "note", "elements": []interface{}{map[string]string{"tag": "plain_text", "content": "opsx-loki 日志告警"}}},
map[string]interface{}{"tag": "note", "elements": []interface{}{map[string]string{"tag": "plain_text", "content": "LokiStack 日志告警"}}},
},
},
}
+4 -4
View File
@@ -12,9 +12,9 @@ import (
"net/http"
"time"
"opsxloki/models"
"opsxloki/pkg/crypto"
"opsxloki/repository"
"lokistack/models"
"lokistack/pkg/crypto"
"lokistack/repository"
)
const channelHTTPTimeout = 10 * time.Second
@@ -182,7 +182,7 @@ func SendLarkCardToChannel(channelID uint, title, color string, lines []string)
},
"elements": []interface{}{
map[string]interface{}{"tag": "div", "text": map[string]string{"tag": "lark_md", "content": content}},
map[string]interface{}{"tag": "note", "elements": []interface{}{map[string]string{"tag": "plain_text", "content": "opsx-loki 日志告警"}}},
map[string]interface{}{"tag": "note", "elements": []interface{}{map[string]string{"tag": "plain_text", "content": "LokiStack 日志告警"}}},
},
},
}
+3 -3
View File
@@ -14,9 +14,9 @@ import (
"strings"
"time"
"opsxloki/config"
"opsxloki/models"
"opsxloki/pkg/crypto"
"lokistack/config"
"lokistack/models"
"lokistack/pkg/crypto"
"golang.org/x/crypto/ssh"
)
+4 -4
View File
@@ -4,10 +4,10 @@ import (
"fmt"
"time"
"opsxloki/models"
"opsxloki/pkg/crypto"
"opsxloki/repository"
"opsxloki/utils"
"lokistack/models"
"lokistack/pkg/crypto"
"lokistack/repository"
"lokistack/utils"
)
type StorageService struct{}
+1 -1
View File
@@ -3,7 +3,7 @@ package services
import (
"testing"
"opsxloki/repository"
"lokistack/repository"
)
func TestStorageUpdateReplacesBothCredentials(t *testing.T) {