ci(engineering): Gitea Actions 门禁 + 前端测试设施 + 版本注入修复
- .gitea/workflows/ci.yml:后端 gofmt/vet/test + 前端 lint/test/build 双 job 门禁 - vitest + @testing-library + jsdom 落地,14 个命脉用例:useListQuery 乱序丢弃/卸载中止、 http.ts 401 单飞刷新+队列重放+失败登出+silent 直拒、审计防抖、StepDeploy 部署目标、query-utils - 版本注入断裂修复:main 补 buildTime/gitCommit 符号,build.sh/Dockerfile 注入 main.version, /health 返回真实版本,前端 package.json 版本对齐 2.0.0 - gofmt 清零(8 个存量未格式化文件)
This commit is contained in:
1 parent
2975452c21
commit
4f74fe6c3f
23 files changed
+1399
-77
No files matched your search
@@ -3,7 +3,7 @@ package dto
|
||||
import "lokistack/services"
|
||||
|
||||
type ImageRegistryRequest struct {
|
||||
Mode string `json:"mode" binding:"required,oneof=overseas cn custom"`
|
||||
CustomHost string `json:"customHost"`
|
||||
Mode string `json:"mode" binding:"required,oneof=overseas cn custom"`
|
||||
CustomHost string `json:"customHost"`
|
||||
Images map[string]services.ComponentImage `json:"images"`
|
||||
}
|
||||
+13
-13
@@ -2,19 +2,19 @@ package dto
|
||||
|
||||
// LDAP 域
|
||||
type LdapConfigRequest struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
UseSSL bool `json:"useSSL"`
|
||||
BindDN string `json:"bindDN"`
|
||||
BindPassword string `json:"bindPassword"`
|
||||
BaseDN string `json:"baseDN"`
|
||||
UserFilter string `json:"userFilter"`
|
||||
UsernameAttr string `json:"usernameAttr"`
|
||||
NameAttr string `json:"nameAttr"`
|
||||
EmailAttr string `json:"emailAttr"`
|
||||
TestUsername string `json:"testUsername"`
|
||||
TestPassword string `json:"testPassword"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
UseSSL bool `json:"useSSL"`
|
||||
BindDN string `json:"bindDN"`
|
||||
BindPassword string `json:"bindPassword"`
|
||||
BaseDN string `json:"baseDN"`
|
||||
UserFilter string `json:"userFilter"`
|
||||
UsernameAttr string `json:"usernameAttr"`
|
||||
NameAttr string `json:"nameAttr"`
|
||||
EmailAttr string `json:"emailAttr"`
|
||||
TestUsername string `json:"testUsername"`
|
||||
TestPassword string `json:"testPassword"`
|
||||
}
|
||||
|
||||
type LdapUsersRequest struct {
|
||||
|
||||
+9
-3
@@ -23,8 +23,13 @@ import (
|
||||
"lokistack/services"
|
||||
)
|
||||
|
||||
// version 由构建注入(-ldflags "-X main.version=..."),未注入时为 dev
|
||||
var version = "2.0.0"
|
||||
// version / buildTime / gitCommit 由构建注入(build.sh / Dockerfile 的 -ldflags -X main.*),
|
||||
// 未注入时为 dev,避免与前端版本号互相矛盾
|
||||
var (
|
||||
version = "dev"
|
||||
buildTime = "unknown"
|
||||
gitCommit = "unknown"
|
||||
)
|
||||
|
||||
// fileExists 判断文件是否存在(用于"是否首次部署"这类轻量判断)。
|
||||
func fileExists(path string) bool {
|
||||
@@ -97,7 +102,7 @@ func runCLI(args []string) {
|
||||
fmt.Println(string(b))
|
||||
|
||||
case "version":
|
||||
fmt.Println(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])
|
||||
@@ -127,6 +132,7 @@ func main() {
|
||||
}
|
||||
|
||||
// 指标注册要早于任何可能埋点的组件(middleware / alert engine)
|
||||
routers.Version = version
|
||||
services.InitMetrics(version)
|
||||
log.Println("Metrics enabled at /api/v1/metrics")
|
||||
|
||||
|
||||
@@ -67,13 +67,13 @@ func TestInferAuditFieldsFiveSegments(t *testing.T) {
|
||||
// 语义化动作不能被位置解析覆盖
|
||||
func TestInferAuditFieldsSemanticActions(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"/api/v1/deployments/1/deploy": "deploy",
|
||||
"/api/v1/deployments/1/stop": "stop",
|
||||
"/api/v1/collectors/2/install": "install",
|
||||
"/api/v1/collectors/2/uninstall": "uninstall",
|
||||
"/api/v1/users/3/reset-password": "reset-password",
|
||||
"/api/v1/hosts/2/validate": "validate",
|
||||
"/api/v1/storage/1/test": "test",
|
||||
"/api/v1/deployments/1/deploy": "deploy",
|
||||
"/api/v1/deployments/1/stop": "stop",
|
||||
"/api/v1/collectors/2/install": "install",
|
||||
"/api/v1/collectors/2/uninstall": "uninstall",
|
||||
"/api/v1/users/3/reset-password": "reset-password",
|
||||
"/api/v1/hosts/2/validate": "validate",
|
||||
"/api/v1/storage/1/test": "test",
|
||||
}
|
||||
for path, want := range cases {
|
||||
if _, action, _, _ := inferAuditFields(http.MethodPost, path); action != want {
|
||||
|
||||
@@ -13,18 +13,18 @@ import (
|
||||
// ipLimiter 内存滑动窗口 IP 限流(简化实现)。
|
||||
// 用于登录防爆破:超限返回 429。单实例部署下内存实现即可,无需外部存储。
|
||||
type ipLimiter struct {
|
||||
mu sync.Mutex
|
||||
window time.Duration
|
||||
max int
|
||||
hits map[string][]time.Time
|
||||
mu sync.Mutex
|
||||
window time.Duration
|
||||
max int
|
||||
hits map[string][]time.Time
|
||||
lastSweep time.Time
|
||||
}
|
||||
|
||||
func newIPLimiter(max int, window time.Duration) *ipLimiter {
|
||||
return &ipLimiter{
|
||||
window: window,
|
||||
max: max,
|
||||
hits: make(map[string][]time.Time),
|
||||
window: window,
|
||||
max: max,
|
||||
hits: make(map[string][]time.Time),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,10 +5,11 @@ import "time"
|
||||
// DailyVolumeSnapshot 每日日志量快照(平台自己留存的历史)。
|
||||
//
|
||||
// 为什么必须落库:
|
||||
// Loki 的 retention_period 是 24 小时(运维可改,但默认就该短),
|
||||
// 意味着「近 7 天每日日志量」直接查 Loki 时,只有最近 1 天有数据,
|
||||
// 其余 6 天是空的 —— 界面上看着像坏了,实际是数据已经不在了。
|
||||
// 想要 30 天 / 1 年趋势,只能在数据被删前每天抓一次存进平台库。
|
||||
//
|
||||
// Loki 的 retention_period 是 24 小时(运维可改,但默认就该短),
|
||||
// 意味着「近 7 天每日日志量」直接查 Loki 时,只有最近 1 天有数据,
|
||||
// 其余 6 天是空的 —— 界面上看着像坏了,实际是数据已经不在了。
|
||||
// 想要 30 天 / 1 年趋势,只能在数据被删前每天抓一次存进平台库。
|
||||
//
|
||||
// 粒度说明:
|
||||
// - 只按「数据源 + 命名空间」存。index/volume 系列接口只能按 selector 标签聚合,
|
||||
@@ -16,12 +17,12 @@ import "time"
|
||||
// 所以按 app 的历史量暂时无法落库,应用维度请看实时排行(有 20~30s 成本)。
|
||||
// - 每天一条,日期用**本地时区**的 YYYY-MM-DD,避免跨时区对不上。
|
||||
type DailyVolumeSnapshot struct {
|
||||
ID uint `json:"id"`
|
||||
Date string `json:"date"` // YYYY-MM-DD(本地时区)
|
||||
SourceID uint `json:"sourceId"` // 数据源
|
||||
Namespace string `json:"namespace"` // "" 表示该数据源全量
|
||||
Bytes int64 `json:"bytes"` // 日志体积
|
||||
Entries int64 `json:"entries"` // 日志条数
|
||||
ID uint `json:"id"`
|
||||
Date string `json:"date"` // YYYY-MM-DD(本地时区)
|
||||
SourceID uint `json:"sourceId"` // 数据源
|
||||
Namespace string `json:"namespace"` // "" 表示该数据源全量
|
||||
Bytes int64 `json:"bytes"` // 日志体积
|
||||
Entries int64 `json:"entries"` // 日志条数
|
||||
// Partial 该日期是否为"不完整的一天"(当天抓取时还没过完)。
|
||||
// 前端用它区分"真实低峰"和"只统计到一半"。
|
||||
Partial bool `json:"partial"`
|
||||
|
||||
+13
-13
@@ -34,19 +34,19 @@ func (s *StorageConfig) Sanitized() map[string]interface{} {
|
||||
plainSK := crypto.DecryptField(s.SecretKey)
|
||||
|
||||
return map[string]interface{}{
|
||||
"id": s.ID,
|
||||
"name": s.Name,
|
||||
"storageType": s.StorageType,
|
||||
"endpoint": s.Endpoint,
|
||||
"bucket": s.Bucket,
|
||||
"region": s.Region,
|
||||
"accessKey": plainAK,
|
||||
"secretKey": "",
|
||||
"pathPrefix": s.PathPrefix,
|
||||
"insecure": s.Insecure,
|
||||
"isDefault": s.IsDefault,
|
||||
"id": s.ID,
|
||||
"name": s.Name,
|
||||
"storageType": s.StorageType,
|
||||
"endpoint": s.Endpoint,
|
||||
"bucket": s.Bucket,
|
||||
"region": s.Region,
|
||||
"accessKey": plainAK,
|
||||
"secretKey": "",
|
||||
"pathPrefix": s.PathPrefix,
|
||||
"insecure": s.Insecure,
|
||||
"isDefault": s.IsDefault,
|
||||
"hasSecretKey": plainSK != "",
|
||||
"createdAt": s.CreatedAt,
|
||||
"updatedAt": s.UpdatedAt,
|
||||
"createdAt": s.CreatedAt,
|
||||
"updatedAt": s.UpdatedAt,
|
||||
}
|
||||
}
|
||||
@@ -18,12 +18,12 @@ type User struct {
|
||||
|
||||
func (u *User) Sanitized() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"id": u.ID,
|
||||
"username": u.Username,
|
||||
"nickname": u.Nickname,
|
||||
"role": u.Role,
|
||||
"id": u.ID,
|
||||
"username": u.Username,
|
||||
"nickname": u.Nickname,
|
||||
"role": u.Role,
|
||||
"authSource": u.AuthSource,
|
||||
"createdAt": u.CreatedAt,
|
||||
"updatedAt": u.UpdatedAt,
|
||||
"createdAt": u.CreatedAt,
|
||||
"updatedAt": u.UpdatedAt,
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,10 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Version 由 main 启动时注入(构建期 -ldflags -X main.version,再传到这里),
|
||||
// 保持 /api/v1/health 与关于页版本一致
|
||||
var Version = "dev"
|
||||
|
||||
func SetupRouter() *gin.Engine {
|
||||
// 用 gin.New() 而不是 gin.Default():Default 自带文本格式的 gin.Logger(),
|
||||
// 这里换成结构化 JSON 访问日志(middleware.AccessLog),便于 Promtail→Loki
|
||||
@@ -55,7 +59,7 @@ func SetupRouter() *gin.Engine {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "ok",
|
||||
"timestamp": time.Now().Unix(),
|
||||
"version": "2.0.0",
|
||||
"version": Version,
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -9,11 +9,14 @@ import (
|
||||
// 回归:接口的 **HTTP 方法与路径** 必须和前端调用一致。
|
||||
//
|
||||
// 线上事故:一次模糊匹配的 patch 把
|
||||
// logs.POST("/histogram", ...) → logs.GET("/histogram", ...)
|
||||
//
|
||||
// logs.POST("/histogram", ...) → logs.GET("/histogram", ...)
|
||||
//
|
||||
// 前端 logsApi.histogram 用的是 POST(body 里带 sourceId/query/start/end),
|
||||
// 于是所有直方图请求 404:
|
||||
// - 日志查询页:直方图不显示
|
||||
// - 日志分析页:直接报 "API endpoint not found",整页不可用
|
||||
//
|
||||
// 这类"方法写错"编译期完全查不出来,只能靠路由表断言。
|
||||
func TestCriticalRoutes(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
Reference in new issue
Block a user