From 085ac7540fc3dcdd125ba7cf8d12ce583846b1ae Mon Sep 17 00:00:00 2001 From: naiba Date: Thu, 2 Oct 2025 14:28:57 +0800 Subject: [PATCH 01/45] feat: upgrade frontend --- service/singleton/frontend-templates.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/singleton/frontend-templates.yaml b/service/singleton/frontend-templates.yaml index 0e19d77..690a281 100644 --- a/service/singleton/frontend-templates.yaml +++ b/service/singleton/frontend-templates.yaml @@ -2,7 +2,7 @@ name: "OfficialAdmin" repository: "https://github.com/nezhahq/admin-frontend" author: "nezhahq" - version: "v1.13.2" + version: "v1.14.0" is_admin: true is_official: true - path: "user-dist" From e1a03c9172fc95f3d346849508bc2abfc30abd4a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 2 Oct 2025 14:29:27 +0800 Subject: [PATCH 02/45] update contributors[no ci] --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d16e032..ddc8ca5 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ add your theme to [service/singleton/frontend-templates.yaml](service/singleton/ Kris Mmx233 rootmelo92118 -Moraxyc +Moraxyc Xu 1ridic 仓鼠 zhucaidan @@ -81,7 +81,7 @@ add your theme to [service/singleton/frontend-templates.yaml](service/singleton/ eya46 guoyongchang hiDandelion -I +IYUANWEIZE igophper lvyaoting quanljh From fece19a94093f33373bf3b0e746834397850548f Mon Sep 17 00:00:00 2001 From: naiba Date: Thu, 2 Oct 2025 15:30:21 +0800 Subject: [PATCH 03/45] feat: upgrade frontend --- service/singleton/frontend-templates.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/singleton/frontend-templates.yaml b/service/singleton/frontend-templates.yaml index 690a281..5607112 100644 --- a/service/singleton/frontend-templates.yaml +++ b/service/singleton/frontend-templates.yaml @@ -2,7 +2,7 @@ name: "OfficialAdmin" repository: "https://github.com/nezhahq/admin-frontend" author: "nezhahq" - version: "v1.14.0" + version: "v1.14.1" is_admin: true is_official: true - path: "user-dist" From f16f0892bc345f4e75ca2c89ecede5f3251143b8 Mon Sep 17 00:00:00 2001 From: naiba Date: Thu, 2 Oct 2025 17:19:26 +0800 Subject: [PATCH 04/45] =?UTF-8?q?=F0=9F=92=A9=20f*ck=20react?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit react 纯傻逼,js ts 就是纯傻逼,浪费人生 --- service/singleton/frontend-templates.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/singleton/frontend-templates.yaml b/service/singleton/frontend-templates.yaml index 5607112..ebdab1c 100644 --- a/service/singleton/frontend-templates.yaml +++ b/service/singleton/frontend-templates.yaml @@ -2,7 +2,7 @@ name: "OfficialAdmin" repository: "https://github.com/nezhahq/admin-frontend" author: "nezhahq" - version: "v1.14.1" + version: "v1.14.2" is_admin: true is_official: true - path: "user-dist" From 32f52067c037d0a10ded31fe5ff08262f32e9388 Mon Sep 17 00:00:00 2001 From: honeok Date: Fri, 3 Oct 2025 09:33:12 +0800 Subject: [PATCH 05/45] fix: embed tzdata to correct container timezone (#1126) Signed-off-by: honeok --- Dockerfile | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4cbce7e..f270900 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,13 @@ -FROM alpine AS certs -RUN apk update && apk add ca-certificates +FROM alpine AS depend +RUN apk add --update --no-cache ca-certificates tzdata FROM busybox:stable-musl ARG TARGETOS ARG TARGETARCH -COPY --from=certs /etc/ssl/certs /etc/ssl/certs +COPY --from=depend /etc/ssl/certs /etc/ssl/certs +COPY --from=depend /usr/share/zoneinfo /usr/share/zoneinfo COPY ./script/entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh From 2df709157e2caa5650ca48ed1954c318c6b87af6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 3 Oct 2025 09:33:27 +0800 Subject: [PATCH 06/45] update contributors[no ci] --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index ddc8ca5..ebdfaec 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,7 @@ add your theme to [service/singleton/frontend-templates.yaml](service/singleton/ eya46 guoyongchang hiDandelion +honeok IYUANWEIZE igophper lvyaoting From e1e0a812a6bdb119c124adfbc1ca42f89359c47b Mon Sep 17 00:00:00 2001 From: naiba Date: Thu, 9 Oct 2025 09:38:01 +0800 Subject: [PATCH 07/45] chore: upgrade frontend --- service/singleton/frontend-templates.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/singleton/frontend-templates.yaml b/service/singleton/frontend-templates.yaml index ebdab1c..902069b 100644 --- a/service/singleton/frontend-templates.yaml +++ b/service/singleton/frontend-templates.yaml @@ -2,7 +2,7 @@ name: "OfficialAdmin" repository: "https://github.com/nezhahq/admin-frontend" author: "nezhahq" - version: "v1.14.2" + version: "v1.14.3" is_admin: true is_official: true - path: "user-dist" From a9fd269ea943b75fc865e1eff160ed91cf90c085 Mon Sep 17 00:00:00 2001 From: naiba Date: Thu, 9 Oct 2025 09:50:55 +0800 Subject: [PATCH 08/45] chore: upgrade frontend --- service/singleton/frontend-templates.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/singleton/frontend-templates.yaml b/service/singleton/frontend-templates.yaml index 902069b..3c96fb3 100644 --- a/service/singleton/frontend-templates.yaml +++ b/service/singleton/frontend-templates.yaml @@ -2,7 +2,7 @@ name: "OfficialAdmin" repository: "https://github.com/nezhahq/admin-frontend" author: "nezhahq" - version: "v1.14.3" + version: "v1.14.4" is_admin: true is_official: true - path: "user-dist" From 1db4fe467951c4767dd983775c964e68d0374d98 Mon Sep 17 00:00:00 2001 From: naiba Date: Thu, 9 Oct 2025 18:32:30 +0800 Subject: [PATCH 09/45] chore: upgrade frontend --- service/singleton/frontend-templates.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/singleton/frontend-templates.yaml b/service/singleton/frontend-templates.yaml index 3c96fb3..68145a8 100644 --- a/service/singleton/frontend-templates.yaml +++ b/service/singleton/frontend-templates.yaml @@ -9,7 +9,7 @@ name: "Official" repository: "https://github.com/hamster1963/nezha-dash-v1" author: "hamster1963" - version: "v1.30.4" + version: "v1.32.0" is_official: true - path: "nezha-ascii-dist" name: "Nezha-ASCII" From 540c1cb1e7ee9ff12fadc5133a238d99ae1b149d Mon Sep 17 00:00:00 2001 From: naiba Date: Thu, 9 Oct 2025 21:36:59 +0800 Subject: [PATCH 10/45] feat: binding ip with session MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🛡️staying safe even your frontend was hacked --- .github/workflows/release.yml | 2 +- .github/workflows/test.yml | 2 +- cmd/dashboard/controller/jwt.go | 29 +++++++--- cmd/dashboard/controller/jwt_test.go | 79 ++++++++++++++++++++++++++++ go.mod | 5 +- pkg/ddns/ddns.go | 4 ++ pkg/ddns/ddns_test.go | 5 -- 7 files changed, 109 insertions(+), 17 deletions(-) create mode 100644 cmd/dashboard/controller/jwt_test.go diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 84639a8..d206721 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -48,7 +48,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: "1.24.x" + go-version: "1.25.x" - name: generate swagger docs run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f6e0989..2b5a6f2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,7 +27,7 @@ jobs: - uses: actions/setup-go@v5 with: - go-version: "1.24.x" + go-version: "1.25.x" - name: generate swagger docs run: | diff --git a/cmd/dashboard/controller/jwt.go b/cmd/dashboard/controller/jwt.go index 476b594..9054c40 100644 --- a/cmd/dashboard/controller/jwt.go +++ b/cmd/dashboard/controller/jwt.go @@ -50,10 +50,8 @@ func initParams() *jwt.GinJWTMiddleware { func payloadFunc() func(data any) jwt.MapClaims { return func(data any) jwt.MapClaims { - if v, ok := data.(string); ok { - return jwt.MapClaims{ - model.CtxKeyAuthorizedUser: v, - } + if v, ok := data.(map[string]interface{}); ok { + return v } return jwt.MapClaims{} } @@ -62,7 +60,15 @@ func payloadFunc() func(data any) jwt.MapClaims { func identityHandler() func(c *gin.Context) any { return func(c *gin.Context) any { claims := jwt.ExtractClaims(c) - userId := claims[model.CtxKeyAuthorizedUser].(string) + userId := claims["user_id"].(string) + tokenIP := claims["ip"].(string) + currentIP := c.GetString(model.CtxKeyRealIPStr) + + if tokenIP != currentIP { + // IP地址不匹配,token无效 + return nil + } + var user model.User if err := singleton.DB.First(&user, userId).Error; err != nil { return nil @@ -109,7 +115,12 @@ func authenticator() func(c *gin.Context) (any, error) { model.UnblockIP(singleton.DB, realip, model.BlockIDUnknownUser) model.UnblockIP(singleton.DB, realip, int64(user.ID)) - return utils.Itoa(user.ID), nil + + // 返回用户ID和IP地址的组合,用于在payloadFunc中设置JWT claims + return map[string]interface{}{ + "user_id": utils.Itoa(user.ID), + "ip": realip, + }, nil } } @@ -174,14 +185,16 @@ func fallbackAuthMiddleware(mw *jwt.GinJWTMiddleware) func(c *gin.Context) { return } + realIP := c.GetString(model.CtxKeyRealIPStr) + c.Set("JWT_PAYLOAD", claims) identity := mw.IdentityHandler(c) if identity != nil { - model.UnblockIP(singleton.DB, c.GetString(model.CtxKeyRealIPStr), model.BlockIDToken) + model.UnblockIP(singleton.DB, realIP, model.BlockIDToken) c.Set(mw.IdentityKey, identity) } else { - if err := model.BlockIP(singleton.DB, c.GetString(model.CtxKeyRealIPStr), model.WAFBlockReasonTypeBruteForceToken, model.BlockIDToken); err != nil { + if err := model.BlockIP(singleton.DB, realIP, model.WAFBlockReasonTypeBruteForceToken, model.BlockIDToken); err != nil { waf.ShowBlockPage(c, err) return } diff --git a/cmd/dashboard/controller/jwt_test.go b/cmd/dashboard/controller/jwt_test.go new file mode 100644 index 0000000..f2b58ea --- /dev/null +++ b/cmd/dashboard/controller/jwt_test.go @@ -0,0 +1,79 @@ +package controller + +import ( + "testing" + "time" + + jwt "github.com/appleboy/gin-jwt/v2" + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" +) + +func TestPayloadFunc(t *testing.T) { + payloadFn := payloadFunc() + + // 测试包含IP的格式 + t.Run("format with IP", func(t *testing.T) { + data := map[string]interface{}{ + "user_id": "123", + "ip": "192.168.1.1", + } + claims := payloadFn(data) + assert.Equal(t, "123", claims["user_id"]) + assert.Equal(t, "192.168.1.1", claims["ip"]) + }) + + // 测试不包含IP的格式 + t.Run("format without IP", func(t *testing.T) { + data := map[string]interface{}{ + "user_id": "123", + } + claims := payloadFn(data) + assert.Equal(t, "123", claims["user_id"]) + assert.Nil(t, claims["ip"]) + }) + + // 测试无效数据格式 + t.Run("invalid data format", func(t *testing.T) { + claims := payloadFn("123") // 字符串类型不再支持 + assert.Empty(t, claims) + }) + + // 测试空的map + t.Run("empty map", func(t *testing.T) { + data := map[string]interface{}{} + claims := payloadFn(data) + assert.Empty(t, claims) + }) +} + +func TestIPBinding(t *testing.T) { + // 创建测试用的gin context + gin.SetMode(gin.TestMode) + + t.Run("IP mismatch should invalidate token", func(t *testing.T) { + // 模拟JWT claims包含IP绑定 + claims := jwt.MapClaims{ + "user_id": "123", + "ip": "192.168.1.1", + "exp": float64(time.Now().Add(time.Hour).Unix()), + } + + // 这里需要实际的数据库和用户设置来完全测试 + // 但可以测试claims的基本结构 + assert.Equal(t, "123", claims["user_id"]) + assert.Equal(t, "192.168.1.1", claims["ip"]) + }) + + t.Run("no IP in token should deny access", func(t *testing.T) { + // 没有IP绑定的token应该被拒绝 + claims := jwt.MapClaims{ + "user_id": "123", + "exp": float64(time.Now().Add(time.Hour).Unix()), + } + + // 验证token结构 + assert.Equal(t, "123", claims["user_id"]) + assert.Nil(t, claims["ip"]) + }) +} diff --git a/go.mod b/go.mod index 60041b6..48d186f 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/nezhahq/nezha -go 1.24.0 +go 1.25 require ( github.com/appleboy/gin-jwt/v2 v2.10.3 @@ -26,8 +26,10 @@ require ( github.com/oschwald/maxminddb-golang v1.13.1 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/robfig/cron/v3 v3.0.1 + github.com/stretchr/testify v1.10.0 github.com/swaggo/files v1.0.1 github.com/swaggo/gin-swagger v1.6.0 + github.com/swaggo/swag v1.16.4 github.com/tidwall/gjson v1.18.0 golang.org/x/crypto v0.37.0 golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 @@ -74,7 +76,6 @@ require ( github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/swaggo/swag v1.16.4 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect diff --git a/pkg/ddns/ddns.go b/pkg/ddns/ddns.go index 99b1d4d..1dc5c2e 100644 --- a/pkg/ddns/ddns.go +++ b/pkg/ddns/ddns.go @@ -114,6 +114,10 @@ func (provider *Provider) splitDomainSOA(ctx context.Context, domain string) (pr if soa, ok := r.Answer[0].(*dns.SOA); ok { zone := soa.Hdr.Name prefix := libdns.RelativeName(domain, zone) + // Convert "@" to empty string for zone apex + if prefix == "@" { + prefix = "" + } return prefix, zone, nil } } diff --git a/pkg/ddns/ddns_test.go b/pkg/ddns/ddns_test.go index c802d2b..70d7cb9 100644 --- a/pkg/ddns/ddns_test.go +++ b/pkg/ddns/ddns_test.go @@ -2,7 +2,6 @@ package ddns import ( "context" - "os" "testing" ) @@ -13,10 +12,6 @@ type testSt struct { } func TestSplitDomainSOA(t *testing.T) { - if ci := os.Getenv("CI"); ci != "" { // skip if test on CI - return - } - cases := []testSt{ { domain: "www.example.co.uk", From cd69af3a0a192256fd7f2d204495ea72c9f8d641 Mon Sep 17 00:00:00 2001 From: naiba Date: Thu, 9 Oct 2025 22:00:08 +0800 Subject: [PATCH 11/45] fix: oauth2 login --- cmd/dashboard/controller/oauth2.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cmd/dashboard/controller/oauth2.go b/cmd/dashboard/controller/oauth2.go index 49bd900..ebe7e50 100644 --- a/cmd/dashboard/controller/oauth2.go +++ b/cmd/dashboard/controller/oauth2.go @@ -177,7 +177,10 @@ func oauth2callback(jwtConfig *jwt.GinJWTMiddleware) func(c *gin.Context) (any, } } - tokenString, _, err := jwtConfig.TokenGenerator(fmt.Sprintf("%d", bind.UserID)) + tokenString, _, err := jwtConfig.TokenGenerator(map[string]interface{}{ + "user_id": fmt.Sprintf("%d", bind.UserID), + "ip": realip, + }) if err != nil { return nil, err } From 245103615fd7dce61db4f9a97a9859c6d7dc857f Mon Sep 17 00:00:00 2001 From: naiba Date: Thu, 9 Oct 2025 22:01:35 +0800 Subject: [PATCH 12/45] chore: back comp --- cmd/dashboard/controller/jwt.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/cmd/dashboard/controller/jwt.go b/cmd/dashboard/controller/jwt.go index 9054c40..8bcfe48 100644 --- a/cmd/dashboard/controller/jwt.go +++ b/cmd/dashboard/controller/jwt.go @@ -60,8 +60,17 @@ func payloadFunc() func(data any) jwt.MapClaims { func identityHandler() func(c *gin.Context) any { return func(c *gin.Context) any { claims := jwt.ExtractClaims(c) - userId := claims["user_id"].(string) - tokenIP := claims["ip"].(string) + + userId, ok := claims["user_id"].(string) + if !ok { + return nil + } + + tokenIP, ok := claims["ip"].(string) + if !ok { + return nil + } + currentIP := c.GetString(model.CtxKeyRealIPStr) if tokenIP != currentIP { From 9d1ee6f54326974659260b4793fbdbaae4ecfc0d Mon Sep 17 00:00:00 2001 From: naiba Date: Fri, 10 Oct 2025 09:53:15 +0800 Subject: [PATCH 13/45] improve: not block ip when ip changed --- cmd/dashboard/controller/jwt.go | 6 ++++-- model/common.go | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/cmd/dashboard/controller/jwt.go b/cmd/dashboard/controller/jwt.go index 8bcfe48..17702c2 100644 --- a/cmd/dashboard/controller/jwt.go +++ b/cmd/dashboard/controller/jwt.go @@ -75,6 +75,7 @@ func identityHandler() func(c *gin.Context) any { if tokenIP != currentIP { // IP地址不匹配,token无效 + c.Set(model.CtxKeyIsIPMismatch, true) return nil } @@ -203,8 +204,9 @@ func fallbackAuthMiddleware(mw *jwt.GinJWTMiddleware) func(c *gin.Context) { model.UnblockIP(singleton.DB, realIP, model.BlockIDToken) c.Set(mw.IdentityKey, identity) } else { - if err := model.BlockIP(singleton.DB, realIP, model.WAFBlockReasonTypeBruteForceToken, model.BlockIDToken); err != nil { - waf.ShowBlockPage(c, err) + isIpMismatch := c.GetBool(model.CtxKeyIsIPMismatch) + if !isIpMismatch { + waf.ShowBlockPage(c, model.BlockIP(singleton.DB, realIP, model.WAFBlockReasonTypeBruteForceToken, model.BlockIDToken)) return } } diff --git a/model/common.go b/model/common.go index 12c3052..321f815 100644 --- a/model/common.go +++ b/model/common.go @@ -15,6 +15,7 @@ import ( const ( CtxKeyAuthorizedUser = "ckau" CtxKeyRealIPStr = "ckri" + CtxKeyIsIPMismatch = "ckipm" ) const ( From 16d5c6450f353a071a6a8b80ef50ac1fcba1f7cd Mon Sep 17 00:00:00 2001 From: UUBulb <35923940+uubulb@users.noreply.github.com> Date: Thu, 4 Dec 2025 21:51:33 +0800 Subject: [PATCH 14/45] fix: use server ids in db query (#1146) --- cmd/dashboard/controller/server.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/cmd/dashboard/controller/server.go b/cmd/dashboard/controller/server.go index 8256255..dec44bc 100644 --- a/cmd/dashboard/controller/server.go +++ b/cmd/dashboard/controller/server.go @@ -342,7 +342,7 @@ func batchMoveServer(c *gin.Context) (any, error) { } err := singleton.DB.Transaction(func(tx *gorm.DB) error { - if err := tx.Model(&model.Server{}).Where("id in (?)", moveForm.ToUser).Update("user_id", moveForm.ToUser).Error; err != nil { + if err := tx.Model(&model.Server{}).Where("id in (?)", moveForm.Ids).Update("user_id", moveForm.ToUser).Error; err != nil { return err } return nil @@ -357,13 +357,12 @@ func batchMoveServer(c *gin.Context) (any, error) { idsMap[id] = true } - singleton.ServerShared.Range(func(_ uint64, s *model.Server) bool { + for _, s := range singleton.ServerShared.Range { if s == nil || !idsMap[s.ID] { - return true + continue } s.UserID = moveForm.ToUser - return true - }) + } return nil, nil } From ae4eb2d0527e9d76ac989e5bb4b529969f02d0a2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 4 Dec 2025 21:51:57 +0800 Subject: [PATCH 15/45] update contributors[no ci] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ebdfaec..1d28e05 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,7 @@ add your theme to [service/singleton/frontend-templates.yaml](service/singleton/ Leon KorenKrita Ian Li -GreenTeodoro839 +YiPing Zhang Esdese :D Coming From 4372de483b80789082fe983653e0b5c57a941af9 Mon Sep 17 00:00:00 2001 From: UUBulb <35923940+uubulb@users.noreply.github.com> Date: Thu, 4 Dec 2025 21:52:12 +0800 Subject: [PATCH 16/45] fix: add a default error message for waf page (#1145) --- cmd/dashboard/controller/waf/waf.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cmd/dashboard/controller/waf/waf.go b/cmd/dashboard/controller/waf/waf.go index bcf59dd..3dbdc27 100644 --- a/cmd/dashboard/controller/waf/waf.go +++ b/cmd/dashboard/controller/waf/waf.go @@ -50,8 +50,14 @@ func Waf(c *gin.Context) { } func ShowBlockPage(c *gin.Context, err error) { + var errMsg string + if err != nil { + errMsg = err.Error() + } else { + errMsg = "you were blocked by nezha WAF" + } c.Writer.WriteHeader(http.StatusForbidden) c.Header("Content-Type", "text/html; charset=utf-8") - c.Writer.WriteString(strings.Replace(errorPageTemplate, "{error}", err.Error(), 1)) + c.Writer.WriteString(strings.Replace(errorPageTemplate, "{error}", errMsg, 1)) c.Abort() } From 4e68c2f874ecac07eaf0a72f1ed86cf487aa626b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Jan 2026 10:31:21 +0800 Subject: [PATCH 17/45] chore(deps): bump golang.org/x/crypto from 0.37.0 to 0.45.0 (#1138) Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.37.0 to 0.45.0. - [Commits](https://github.com/golang/crypto/compare/v0.37.0...v0.45.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-version: 0.45.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 16 ++++++++-------- go.sum | 28 ++++++++++++++-------------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/go.mod b/go.mod index 48d186f..a1278bc 100644 --- a/go.mod +++ b/go.mod @@ -29,13 +29,12 @@ require ( github.com/stretchr/testify v1.10.0 github.com/swaggo/files v1.0.1 github.com/swaggo/gin-swagger v1.6.0 - github.com/swaggo/swag v1.16.4 github.com/tidwall/gjson v1.18.0 - golang.org/x/crypto v0.37.0 + golang.org/x/crypto v0.45.0 golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 - golang.org/x/net v0.39.0 + golang.org/x/net v0.47.0 golang.org/x/oauth2 v0.29.0 - golang.org/x/sync v0.13.0 + golang.org/x/sync v0.18.0 google.golang.org/grpc v1.72.0 google.golang.org/protobuf v1.36.6 gorm.io/driver/sqlite v1.5.7 @@ -76,6 +75,7 @@ require ( github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/swaggo/swag v1.16.4 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect @@ -83,11 +83,11 @@ require ( github.com/ugorji/go/codec v1.2.12 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect golang.org/x/arch v0.16.0 // indirect - golang.org/x/mod v0.24.0 // indirect - golang.org/x/sys v0.32.0 // indirect - golang.org/x/text v0.24.0 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.11.0 // indirect - golang.org/x/tools v0.32.0 // indirect + golang.org/x/tools v0.38.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 6866275..407e96c 100644 --- a/go.sum +++ b/go.sum @@ -190,25 +190,25 @@ golang.org/x/arch v0.16.0 h1:foMtLTdyOmIniqWCHjY6+JxuC54XP1fDwx4N0ASyW+U= golang.org/x/arch v0.16.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= -golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= -golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= -golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= -golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -216,8 +216,8 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= -golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -225,15 +225,15 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= -golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= -golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 h1:h6p3mQqrmT1XkHVTfzLdNz1u7IhINeZkz67/xTbOuWs= google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= From 6b07370c1b4987c8e922e629f15b9082e1d0ccfd Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Thu, 1 Jan 2026 03:33:59 +0100 Subject: [PATCH 18/45] Added translation using Weblate (Ukrainian) (#1128) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added translation using Weblate (Japanese) Co-authored-by: Zilong Liu <2821624044@qq.com> Co-authored-by: Руслан Пузич --- .../translations/ja_JP/LC_MESSAGES/nezha.mo | Bin 0 -> 458 bytes .../translations/ja_JP/LC_MESSAGES/nezha.po | 312 +++++++++++++++++ .../translations/uk_UA/LC_MESSAGES/nezha.mo | Bin 0 -> 537 bytes .../translations/uk_UA/LC_MESSAGES/nezha.po | 313 ++++++++++++++++++ 4 files changed, 625 insertions(+) create mode 100644 pkg/i18n/translations/ja_JP/LC_MESSAGES/nezha.mo create mode 100644 pkg/i18n/translations/ja_JP/LC_MESSAGES/nezha.po create mode 100644 pkg/i18n/translations/uk_UA/LC_MESSAGES/nezha.mo create mode 100644 pkg/i18n/translations/uk_UA/LC_MESSAGES/nezha.po diff --git a/pkg/i18n/translations/ja_JP/LC_MESSAGES/nezha.mo b/pkg/i18n/translations/ja_JP/LC_MESSAGES/nezha.mo new file mode 100644 index 0000000000000000000000000000000000000000..91a1420fab454f36dab55a0a5f87fbbab754c379 GIT binary patch literal 458 zcmYk2%TB{E5Jf?-%91q;FIa$JnpOg-xGf@(6oQhbN>g4tw;7UDsU6vFd3*&Mevfa# zArfVzf@hR;XN9)LFXAGynhq literal 0 HcmV?d00001 diff --git a/pkg/i18n/translations/ja_JP/LC_MESSAGES/nezha.po b/pkg/i18n/translations/ja_JP/LC_MESSAGES/nezha.po new file mode 100644 index 0000000..6293fc2 --- /dev/null +++ b/pkg/i18n/translations/ja_JP/LC_MESSAGES/nezha.po @@ -0,0 +1,312 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-01-30 21:58+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: ja\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: cmd/dashboard/controller/alertrule.go:104 +#, c-format +msgid "alert id %d does not exist" +msgstr "" + +#: cmd/dashboard/controller/alertrule.go:108 +#: cmd/dashboard/controller/alertrule.go:156 +#: cmd/dashboard/controller/alertrule.go:176 +#: cmd/dashboard/controller/controller.go:226 +#: cmd/dashboard/controller/cron.go:58 cmd/dashboard/controller/cron.go:124 +#: cmd/dashboard/controller/cron.go:136 cmd/dashboard/controller/cron.go:195 +#: cmd/dashboard/controller/cron.go:224 cmd/dashboard/controller/ddns.go:131 +#: cmd/dashboard/controller/ddns.go:192 cmd/dashboard/controller/fm.go:43 +#: cmd/dashboard/controller/nat.go:59 cmd/dashboard/controller/nat.go:111 +#: cmd/dashboard/controller/nat.go:122 cmd/dashboard/controller/nat.go:162 +#: cmd/dashboard/controller/notification.go:112 +#: cmd/dashboard/controller/notification.go:166 +#: cmd/dashboard/controller/notification_group.go:76 +#: cmd/dashboard/controller/notification_group.go:152 +#: cmd/dashboard/controller/notification_group.go:164 +#: cmd/dashboard/controller/notification_group.go:233 +#: cmd/dashboard/controller/server.go:66 cmd/dashboard/controller/server.go:78 +#: cmd/dashboard/controller/server.go:137 +#: cmd/dashboard/controller/server.go:201 +#: cmd/dashboard/controller/server_group.go:75 +#: cmd/dashboard/controller/server_group.go:150 +#: cmd/dashboard/controller/server_group.go:229 +#: cmd/dashboard/controller/service.go:271 +#: cmd/dashboard/controller/service.go:342 +#: cmd/dashboard/controller/service.go:369 +#: cmd/dashboard/controller/terminal.go:41 +msgid "permission denied" +msgstr "" + +#: cmd/dashboard/controller/alertrule.go:184 +msgid "duration need to be at least 3" +msgstr "" + +#: cmd/dashboard/controller/alertrule.go:188 +msgid "cycle_interval need to be at least 1" +msgstr "" + +#: cmd/dashboard/controller/alertrule.go:191 +msgid "cycle_start is not set" +msgstr "" + +#: cmd/dashboard/controller/alertrule.go:194 +msgid "cycle_start is a future value" +msgstr "" + +#: cmd/dashboard/controller/alertrule.go:199 +msgid "need to configure at least a single rule" +msgstr "" + +#: cmd/dashboard/controller/controller.go:220 +#: cmd/dashboard/controller/oauth2.go:153 +#: cmd/dashboard/controller/server_group.go:162 +#: cmd/dashboard/controller/service.go:97 cmd/dashboard/controller/user.go:27 +#: cmd/dashboard/controller/user.go:63 +msgid "unauthorized" +msgstr "" + +#: cmd/dashboard/controller/controller.go:243 +msgid "database error" +msgstr "" + +#: cmd/dashboard/controller/cron.go:75 cmd/dashboard/controller/cron.go:149 +msgid "scheduled tasks cannot be triggered by alarms" +msgstr "" + +#: cmd/dashboard/controller/cron.go:132 cmd/dashboard/controller/cron.go:190 +#, c-format +msgid "task id %d does not exist" +msgstr "" + +#: cmd/dashboard/controller/ddns.go:57 cmd/dashboard/controller/ddns.go:122 +msgid "the retry count must be an integer between 1 and 10" +msgstr "" + +#: cmd/dashboard/controller/ddns.go:81 cmd/dashboard/controller/ddns.go:154 +msgid "error parsing %s: %v" +msgstr "" + +#: cmd/dashboard/controller/ddns.go:127 cmd/dashboard/controller/nat.go:118 +#, c-format +msgid "profile id %d does not exist" +msgstr "" + +#: cmd/dashboard/controller/fm.go:39 cmd/dashboard/controller/terminal.go:37 +msgid "server not found or not connected" +msgstr "" + +#: cmd/dashboard/controller/notification.go:69 +#: cmd/dashboard/controller/notification.go:131 +msgid "a test message" +msgstr "" + +#: cmd/dashboard/controller/notification.go:108 +#, c-format +msgid "notification id %d does not exist" +msgstr "" + +#: cmd/dashboard/controller/notification_group.go:94 +#: cmd/dashboard/controller/notification_group.go:175 +msgid "have invalid notification id" +msgstr "" + +#: cmd/dashboard/controller/notification_group.go:160 +#: cmd/dashboard/controller/server_group.go:158 +#, c-format +msgid "group id %d does not exist" +msgstr "" + +#: cmd/dashboard/controller/oauth2.go:42 cmd/dashboard/controller/oauth2.go:83 +msgid "provider is required" +msgstr "" + +#: cmd/dashboard/controller/oauth2.go:52 cmd/dashboard/controller/oauth2.go:87 +#: cmd/dashboard/controller/oauth2.go:132 +msgid "provider not found" +msgstr "" + +#: cmd/dashboard/controller/oauth2.go:100 +msgid "operation not permitted" +msgstr "" + +#: cmd/dashboard/controller/oauth2.go:138 +msgid "code is required" +msgstr "" + +#: cmd/dashboard/controller/oauth2.go:175 +msgid "oauth2 user not binded yet" +msgstr "" + +#: cmd/dashboard/controller/oauth2.go:217 +#: cmd/dashboard/controller/oauth2.go:223 +#: cmd/dashboard/controller/oauth2.go:228 +msgid "invalid state key" +msgstr "" + +#: cmd/dashboard/controller/server.go:74 +#, c-format +msgid "server id %d does not exist" +msgstr "" + +#: cmd/dashboard/controller/server.go:250 +msgid "operation timeout" +msgstr "" + +#: cmd/dashboard/controller/server.go:257 +msgid "get server config failed: %v" +msgstr "" + +#: cmd/dashboard/controller/server.go:261 +msgid "get server config failed" +msgstr "" + +#: cmd/dashboard/controller/server_group.go:92 +#: cmd/dashboard/controller/server_group.go:172 +msgid "have invalid server id" +msgstr "" + +#: cmd/dashboard/controller/service.go:90 +#: cmd/dashboard/controller/service.go:165 +msgid "server not found" +msgstr "" + +#: cmd/dashboard/controller/service.go:267 +#, c-format +msgid "service id %d does not exist" +msgstr "" + +#: cmd/dashboard/controller/user.go:68 +msgid "incorrect password" +msgstr "" + +#: cmd/dashboard/controller/user.go:82 +msgid "you don't have any oauth2 bindings" +msgstr "" + +#: cmd/dashboard/controller/user.go:131 +msgid "password length must be greater than 6" +msgstr "" + +#: cmd/dashboard/controller/user.go:134 +msgid "username can't be empty" +msgstr "" + +#: cmd/dashboard/controller/user.go:137 +msgid "invalid role" +msgstr "" + +#: cmd/dashboard/controller/user.go:176 +msgid "can't delete yourself" +msgstr "" + +#: service/rpc/io_stream.go:128 +msgid "timeout: no connection established" +msgstr "" + +#: service/rpc/io_stream.go:131 +msgid "timeout: user connection not established" +msgstr "" + +#: service/rpc/io_stream.go:134 +msgid "timeout: agent connection not established" +msgstr "" + +#: service/rpc/nezha.go:71 +msgid "Scheduled Task Executed Successfully" +msgstr "" + +#: service/rpc/nezha.go:75 +msgid "Scheduled Task Executed Failed" +msgstr "" + +#: service/rpc/nezha.go:274 +msgid "IP Changed" +msgstr "" + +#: service/singleton/alertsentinel.go:169 +msgid "Incident" +msgstr "" + +#: service/singleton/alertsentinel.go:179 +msgid "Resolved" +msgstr "" + +#: service/singleton/crontask.go:54 +msgid "Tasks failed to register: [" +msgstr "" + +#: service/singleton/crontask.go:61 +msgid "" +"] These tasks will not execute properly. Fix them in the admin dashboard." +msgstr "" + +#: service/singleton/crontask.go:144 service/singleton/crontask.go:169 +#, c-format +msgid "[Task failed] %s: server %s is offline and cannot execute the task" +msgstr "" + +#: service/singleton/servicesentinel.go:468 +#, c-format +msgid "[Latency] %s %2f > %2f, Reporter: %s" +msgstr "" + +#: service/singleton/servicesentinel.go:475 +#, c-format +msgid "[Latency] %s %2f < %2f, Reporter: %s" +msgstr "" + +#: service/singleton/servicesentinel.go:501 +#, c-format +msgid "[%s] %s Reporter: %s, Error: %s" +msgstr "" + +#: service/singleton/servicesentinel.go:544 +#, c-format +msgid "[TLS] Fetch cert info failed, Reporter: %s, Error: %s" +msgstr "" + +#: service/singleton/servicesentinel.go:584 +#, c-format +msgid "The TLS certificate will expire within seven days. Expiration time: %s" +msgstr "" + +#: service/singleton/servicesentinel.go:597 +#, c-format +msgid "" +"TLS certificate changed, old: issuer %s, expires at %s; new: issuer %s, " +"expires at %s" +msgstr "" + +#: service/singleton/servicesentinel.go:633 +msgid "No Data" +msgstr "" + +#: service/singleton/servicesentinel.go:635 +msgid "Good" +msgstr "" + +#: service/singleton/servicesentinel.go:637 +msgid "Low Availability" +msgstr "" + +#: service/singleton/servicesentinel.go:639 +msgid "Down" +msgstr "" + +#: service/singleton/user.go:60 +msgid "user id not specified" +msgstr "" diff --git a/pkg/i18n/translations/uk_UA/LC_MESSAGES/nezha.mo b/pkg/i18n/translations/uk_UA/LC_MESSAGES/nezha.mo new file mode 100644 index 0000000000000000000000000000000000000000..cc620ad16bbb64e556995e6d16eb7994e5be6f27 GIT binary patch literal 537 zcmYjN!A{#i5Cy@>N6s9kQb8cPu}wirH*ty~P8%ssqL4u0wuuMZB5_! z$zitCBA2<4a4^J|OdL8BER?n^Hff%)^D;GTsyH~~5u4y+Ua44DSmbaMg%cK!ST}_4 zBOdpUZb!q&83<$9R10ZV!Ya+7HyaFK7{(E_qd4pj&cbeY5?x+)h$_=kq{yZy9{wMt zompEL?z)*W7L&$LoKrJwD4n`RZNj*+8n?b#B1|?{LMLvybk~hJluIY>$I;ra-|(CZ zB`r#t^0^=z7N4wJtY`}@SY$#Qw83oJW6d?Ma&w28MRK8%T&5hF^V~Y)RjI{_^^|@v z9AxqKHNoc=)M+61d@l%m*x!dL^*#i?4`0CpV$;799KV;^!AX@KywTde2d}TSggD+I bX?)Tgx11mB8%osmX0PkDz-hy$<81r|R4|#M literal 0 HcmV?d00001 diff --git a/pkg/i18n/translations/uk_UA/LC_MESSAGES/nezha.po b/pkg/i18n/translations/uk_UA/LC_MESSAGES/nezha.po new file mode 100644 index 0000000..3541ee5 --- /dev/null +++ b/pkg/i18n/translations/uk_UA/LC_MESSAGES/nezha.po @@ -0,0 +1,313 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-01-30 21:58+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: uk\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" + +#: cmd/dashboard/controller/alertrule.go:104 +#, c-format +msgid "alert id %d does not exist" +msgstr "" + +#: cmd/dashboard/controller/alertrule.go:108 +#: cmd/dashboard/controller/alertrule.go:156 +#: cmd/dashboard/controller/alertrule.go:176 +#: cmd/dashboard/controller/controller.go:226 +#: cmd/dashboard/controller/cron.go:58 cmd/dashboard/controller/cron.go:124 +#: cmd/dashboard/controller/cron.go:136 cmd/dashboard/controller/cron.go:195 +#: cmd/dashboard/controller/cron.go:224 cmd/dashboard/controller/ddns.go:131 +#: cmd/dashboard/controller/ddns.go:192 cmd/dashboard/controller/fm.go:43 +#: cmd/dashboard/controller/nat.go:59 cmd/dashboard/controller/nat.go:111 +#: cmd/dashboard/controller/nat.go:122 cmd/dashboard/controller/nat.go:162 +#: cmd/dashboard/controller/notification.go:112 +#: cmd/dashboard/controller/notification.go:166 +#: cmd/dashboard/controller/notification_group.go:76 +#: cmd/dashboard/controller/notification_group.go:152 +#: cmd/dashboard/controller/notification_group.go:164 +#: cmd/dashboard/controller/notification_group.go:233 +#: cmd/dashboard/controller/server.go:66 cmd/dashboard/controller/server.go:78 +#: cmd/dashboard/controller/server.go:137 +#: cmd/dashboard/controller/server.go:201 +#: cmd/dashboard/controller/server_group.go:75 +#: cmd/dashboard/controller/server_group.go:150 +#: cmd/dashboard/controller/server_group.go:229 +#: cmd/dashboard/controller/service.go:271 +#: cmd/dashboard/controller/service.go:342 +#: cmd/dashboard/controller/service.go:369 +#: cmd/dashboard/controller/terminal.go:41 +msgid "permission denied" +msgstr "" + +#: cmd/dashboard/controller/alertrule.go:184 +msgid "duration need to be at least 3" +msgstr "" + +#: cmd/dashboard/controller/alertrule.go:188 +msgid "cycle_interval need to be at least 1" +msgstr "" + +#: cmd/dashboard/controller/alertrule.go:191 +msgid "cycle_start is not set" +msgstr "" + +#: cmd/dashboard/controller/alertrule.go:194 +msgid "cycle_start is a future value" +msgstr "" + +#: cmd/dashboard/controller/alertrule.go:199 +msgid "need to configure at least a single rule" +msgstr "" + +#: cmd/dashboard/controller/controller.go:220 +#: cmd/dashboard/controller/oauth2.go:153 +#: cmd/dashboard/controller/server_group.go:162 +#: cmd/dashboard/controller/service.go:97 cmd/dashboard/controller/user.go:27 +#: cmd/dashboard/controller/user.go:63 +msgid "unauthorized" +msgstr "" + +#: cmd/dashboard/controller/controller.go:243 +msgid "database error" +msgstr "" + +#: cmd/dashboard/controller/cron.go:75 cmd/dashboard/controller/cron.go:149 +msgid "scheduled tasks cannot be triggered by alarms" +msgstr "" + +#: cmd/dashboard/controller/cron.go:132 cmd/dashboard/controller/cron.go:190 +#, c-format +msgid "task id %d does not exist" +msgstr "" + +#: cmd/dashboard/controller/ddns.go:57 cmd/dashboard/controller/ddns.go:122 +msgid "the retry count must be an integer between 1 and 10" +msgstr "" + +#: cmd/dashboard/controller/ddns.go:81 cmd/dashboard/controller/ddns.go:154 +msgid "error parsing %s: %v" +msgstr "" + +#: cmd/dashboard/controller/ddns.go:127 cmd/dashboard/controller/nat.go:118 +#, c-format +msgid "profile id %d does not exist" +msgstr "" + +#: cmd/dashboard/controller/fm.go:39 cmd/dashboard/controller/terminal.go:37 +msgid "server not found or not connected" +msgstr "" + +#: cmd/dashboard/controller/notification.go:69 +#: cmd/dashboard/controller/notification.go:131 +msgid "a test message" +msgstr "" + +#: cmd/dashboard/controller/notification.go:108 +#, c-format +msgid "notification id %d does not exist" +msgstr "" + +#: cmd/dashboard/controller/notification_group.go:94 +#: cmd/dashboard/controller/notification_group.go:175 +msgid "have invalid notification id" +msgstr "" + +#: cmd/dashboard/controller/notification_group.go:160 +#: cmd/dashboard/controller/server_group.go:158 +#, c-format +msgid "group id %d does not exist" +msgstr "" + +#: cmd/dashboard/controller/oauth2.go:42 cmd/dashboard/controller/oauth2.go:83 +msgid "provider is required" +msgstr "" + +#: cmd/dashboard/controller/oauth2.go:52 cmd/dashboard/controller/oauth2.go:87 +#: cmd/dashboard/controller/oauth2.go:132 +msgid "provider not found" +msgstr "" + +#: cmd/dashboard/controller/oauth2.go:100 +msgid "operation not permitted" +msgstr "" + +#: cmd/dashboard/controller/oauth2.go:138 +msgid "code is required" +msgstr "" + +#: cmd/dashboard/controller/oauth2.go:175 +msgid "oauth2 user not binded yet" +msgstr "" + +#: cmd/dashboard/controller/oauth2.go:217 +#: cmd/dashboard/controller/oauth2.go:223 +#: cmd/dashboard/controller/oauth2.go:228 +msgid "invalid state key" +msgstr "" + +#: cmd/dashboard/controller/server.go:74 +#, c-format +msgid "server id %d does not exist" +msgstr "" + +#: cmd/dashboard/controller/server.go:250 +msgid "operation timeout" +msgstr "" + +#: cmd/dashboard/controller/server.go:257 +msgid "get server config failed: %v" +msgstr "" + +#: cmd/dashboard/controller/server.go:261 +msgid "get server config failed" +msgstr "" + +#: cmd/dashboard/controller/server_group.go:92 +#: cmd/dashboard/controller/server_group.go:172 +msgid "have invalid server id" +msgstr "" + +#: cmd/dashboard/controller/service.go:90 +#: cmd/dashboard/controller/service.go:165 +msgid "server not found" +msgstr "" + +#: cmd/dashboard/controller/service.go:267 +#, c-format +msgid "service id %d does not exist" +msgstr "" + +#: cmd/dashboard/controller/user.go:68 +msgid "incorrect password" +msgstr "" + +#: cmd/dashboard/controller/user.go:82 +msgid "you don't have any oauth2 bindings" +msgstr "" + +#: cmd/dashboard/controller/user.go:131 +msgid "password length must be greater than 6" +msgstr "" + +#: cmd/dashboard/controller/user.go:134 +msgid "username can't be empty" +msgstr "" + +#: cmd/dashboard/controller/user.go:137 +msgid "invalid role" +msgstr "" + +#: cmd/dashboard/controller/user.go:176 +msgid "can't delete yourself" +msgstr "" + +#: service/rpc/io_stream.go:128 +msgid "timeout: no connection established" +msgstr "" + +#: service/rpc/io_stream.go:131 +msgid "timeout: user connection not established" +msgstr "" + +#: service/rpc/io_stream.go:134 +msgid "timeout: agent connection not established" +msgstr "" + +#: service/rpc/nezha.go:71 +msgid "Scheduled Task Executed Successfully" +msgstr "" + +#: service/rpc/nezha.go:75 +msgid "Scheduled Task Executed Failed" +msgstr "" + +#: service/rpc/nezha.go:274 +msgid "IP Changed" +msgstr "" + +#: service/singleton/alertsentinel.go:169 +msgid "Incident" +msgstr "" + +#: service/singleton/alertsentinel.go:179 +msgid "Resolved" +msgstr "" + +#: service/singleton/crontask.go:54 +msgid "Tasks failed to register: [" +msgstr "" + +#: service/singleton/crontask.go:61 +msgid "" +"] These tasks will not execute properly. Fix them in the admin dashboard." +msgstr "" + +#: service/singleton/crontask.go:144 service/singleton/crontask.go:169 +#, c-format +msgid "[Task failed] %s: server %s is offline and cannot execute the task" +msgstr "" + +#: service/singleton/servicesentinel.go:468 +#, c-format +msgid "[Latency] %s %2f > %2f, Reporter: %s" +msgstr "" + +#: service/singleton/servicesentinel.go:475 +#, c-format +msgid "[Latency] %s %2f < %2f, Reporter: %s" +msgstr "" + +#: service/singleton/servicesentinel.go:501 +#, c-format +msgid "[%s] %s Reporter: %s, Error: %s" +msgstr "" + +#: service/singleton/servicesentinel.go:544 +#, c-format +msgid "[TLS] Fetch cert info failed, Reporter: %s, Error: %s" +msgstr "" + +#: service/singleton/servicesentinel.go:584 +#, c-format +msgid "The TLS certificate will expire within seven days. Expiration time: %s" +msgstr "" + +#: service/singleton/servicesentinel.go:597 +#, c-format +msgid "" +"TLS certificate changed, old: issuer %s, expires at %s; new: issuer %s, " +"expires at %s" +msgstr "" + +#: service/singleton/servicesentinel.go:633 +msgid "No Data" +msgstr "" + +#: service/singleton/servicesentinel.go:635 +msgid "Good" +msgstr "" + +#: service/singleton/servicesentinel.go:637 +msgid "Low Availability" +msgstr "" + +#: service/singleton/servicesentinel.go:639 +msgid "Down" +msgstr "" + +#: service/singleton/user.go:60 +msgid "user id not specified" +msgstr "" From 7f8ca5cda9908d56bad0e4bf62c4c98b0aa4933c Mon Sep 17 00:00:00 2001 From: naiba Date: Thu, 1 Jan 2026 10:35:15 +0800 Subject: [PATCH 19/45] chore: disable javascript scan --- .github/workflows/codeql-analysis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index ddce9b5..656eecc 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -32,7 +32,7 @@ jobs: strategy: fail-fast: false matrix: - language: [ 'go', 'javascript' ] + language: [ 'go' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # Learn more about CodeQL language support at https://git.io/codeql-language-support From 554db7cd64bbb7f7e8861107589c36733b802f39 Mon Sep 17 00:00:00 2001 From: naiba Date: Thu, 1 Jan 2026 10:56:52 +0800 Subject: [PATCH 20/45] chore: upgrade frontend --- service/singleton/frontend-templates.yaml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/service/singleton/frontend-templates.yaml b/service/singleton/frontend-templates.yaml index 68145a8..898c259 100644 --- a/service/singleton/frontend-templates.yaml +++ b/service/singleton/frontend-templates.yaml @@ -2,22 +2,22 @@ name: "OfficialAdmin" repository: "https://github.com/nezhahq/admin-frontend" author: "nezhahq" - version: "v1.14.4" + version: "v1.14.5" is_admin: true is_official: true - path: "user-dist" name: "Official" repository: "https://github.com/hamster1963/nezha-dash-v1" author: "hamster1963" - version: "v1.32.0" + version: "v1.33.0" is_official: true +- path: "nazhua-dist" + name: "Nazhua" + repository: "https://github.com/hi2shark/nazhua" + author: "hi2hi" + version: "v0.9.0" - path: "nezha-ascii-dist" name: "Nezha-ASCII" repository: "https://github.com/hamster1963/nezha-ascii" author: "hamster1963" version: "v1.1.0" -- path: "nazhua-dist" - name: "Nazhua" - repository: "https://github.com/hi2shark/nazhua" - author: "hi2hi" - version: "v0.7.0" From 1eee2329eb3bcbd641ebfbcbfcafea330efbfff5 Mon Sep 17 00:00:00 2001 From: naiba Date: Thu, 1 Jan 2026 11:51:25 +0800 Subject: [PATCH 21/45] fix: immutable release --- .github/workflows/release.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d206721..78551c6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -107,10 +107,10 @@ jobs: done - name: Release - uses: ncipollo/release-action@v1 + uses: softprops/action-gh-release@v2 with: - artifacts: "assets/*/*/*.zip" - generateReleaseNotes: true + files: "assets/*/*/*.zip" + generate_release_notes: true - name: Purge jsdelivr cache run: | From 7b92f335f26ac85a362dbf950828392e1656107a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 1 Jan 2026 11:51:51 +0800 Subject: [PATCH 22/45] update contributors[no ci] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1d28e05..0178e73 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ add your theme to [service/singleton/frontend-templates.yaml](service/singleton/ ## Contributors -naiba +奶爸 UUBulb Akkia 卖女孩的小火柴 From 7c6bc3d5c7c6fa9340eee12314122ad1fae5625a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A5=B6=E7=88=B8?= Date: Fri, 2 Jan 2026 22:04:47 +0800 Subject: [PATCH 23/45] =?UTF-8?q?chore:=20=E5=9E=83=E5=9C=BE=E4=BD=AC?= =?UTF-8?q?=E8=AE=BA=E5=9D=9B=E4=B8=8B=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 0178e73..57f0bed 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,10 @@

Supports monitoring system status, HTTP (SSL certificate change, upcoming expiration, expired), TCP, Ping and supports push alerts, run scheduled tasks and web terminal.

-\>> Official Forum | 官方论坛: [有点意思](https://lajilao.com/c/nezha)
\>> Telegram Channel: [哪吒监控(中文通知频道)](https://t.me/nezhanews)
+\>> Telegram Group: [Nezha Monitoring Global (English Only)](https://t.me/nezhamonitoring_global), [哪吒监控(中文群组)](https://t.me/nezhamonitoring) \>> [Use Cases | 我们的用户](https://www.google.com/search?q=%22%E5%93%AA%E5%90%92%E7%9B%91%E6%8E%A7+Nezha+Monitoring%22) (Google)
-\>> Telegram Group: [Nezha Monitoring Global (English Only)](https://t.me/nezhamonitoring_global), [哪吒监控(中文群组)](https://t.me/nezhamonitoring) ## User Guide From 6067a41038ba61e1239a0a35b2bda61e14ee60c1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 2 Jan 2026 22:05:02 +0800 Subject: [PATCH 24/45] update contributors[no ci] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 57f0bed..cb239f4 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ add your theme to [service/singleton/frontend-templates.yaml](service/singleton/ Sean lyj fscarmen -no-name-now +windingFlame JSker9 Hsukqi Lee Darc Z. From 302d278644095f9213d6090edd46cfe3d95612d3 Mon Sep 17 00:00:00 2001 From: UUBulb <35923940+uubulb@users.noreply.github.com> Date: Sat, 10 Jan 2026 17:29:10 +0800 Subject: [PATCH 25/45] feat(notification): add option to convert metric units in request body (#1156) * feat(notification): add option to convert metric units in request body * ignore gosec * rename fields --- cmd/dashboard/controller/notification.go | 4 ++ model/notification.go | 52 ++++++++++++++++++------ model/notification_api.go | 17 ++++---- model/notification_test.go | 40 ++++++++++++++---- pkg/utils/bytes.go | 40 ++++++++++++++++++ 5 files changed, 124 insertions(+), 29 deletions(-) create mode 100644 pkg/utils/bytes.go diff --git a/cmd/dashboard/controller/notification.go b/cmd/dashboard/controller/notification.go index f5f5aa1..774ecf2 100644 --- a/cmd/dashboard/controller/notification.go +++ b/cmd/dashboard/controller/notification.go @@ -59,6 +59,8 @@ func createNotification(c *gin.Context) (uint64, error) { n.URL = nf.URL verifyTLS := nf.VerifyTLS n.VerifyTLS = &verifyTLS + formatMetricUnits := nf.FormatMetricUnits + n.FormatMetricUnits = &formatMetricUnits ns := model.NotificationServerBundle{ Notification: &n, @@ -120,6 +122,8 @@ func updateNotification(c *gin.Context) (any, error) { n.URL = nf.URL verifyTLS := nf.VerifyTLS n.VerifyTLS = &verifyTLS + formatMetricUnits := nf.FormatMetricUnits + n.FormatMetricUnits = &formatMetricUnits ns := model.NotificationServerBundle{ Notification: &n, diff --git a/model/notification.go b/model/notification.go index e6f570d..db32e6b 100644 --- a/model/notification.go +++ b/model/notification.go @@ -33,13 +33,14 @@ type NotificationServerBundle struct { type Notification struct { Common - Name string `json:"name"` - URL string `json:"url"` - RequestMethod uint8 `json:"request_method"` - RequestType uint8 `json:"request_type"` - RequestHeader string `json:"request_header" gorm:"type:longtext"` - RequestBody string `json:"request_body" gorm:"type:longtext"` - VerifyTLS *bool `json:"verify_tls,omitempty"` + Name string `json:"name"` + URL string `json:"url"` + RequestMethod uint8 `json:"request_method"` + RequestType uint8 `json:"request_type"` + RequestHeader string `json:"request_header" gorm:"type:longtext"` + RequestBody string `json:"request_body" gorm:"type:longtext"` + VerifyTLS *bool `json:"verify_tls,omitempty"` + FormatMetricUnits *bool `json:"format_metric_units,omitempty"` } func (ns *NotificationServerBundle) reqURL(message string) string { @@ -172,10 +173,19 @@ func (ns *NotificationServerBundle) replaceParamsInString(str string, message st replacements = append(replacements, "#SERVER.NAME#", mod(ns.Server.Name), "#SERVER.ID#", mod(fmt.Sprintf("%d", ns.Server.ID)), - "#SERVER.CPU#", mod(fmt.Sprintf("%f", ns.Server.State.CPU)), - "#SERVER.MEM#", mod(fmt.Sprintf("%d", ns.Server.State.MemUsed)), - "#SERVER.SWAP#", mod(fmt.Sprintf("%d", ns.Server.State.SwapUsed)), - "#SERVER.DISK#", mod(fmt.Sprintf("%d", ns.Server.State.DiskUsed)), + + // Converted metrics + "#SERVER.CPU#", mod(ns.formatUsage(false, ns.Server.State.CPU)), + "#SERVER.MEM#", mod(ns.formatUsage(true, float64(ns.Server.State.MemUsed)/float64(ns.Server.Host.MemTotal))), + "#SERVER.SWAP#", mod(ns.formatUsage(true, float64(ns.Server.State.SwapUsed)/float64(ns.Server.Host.SwapTotal))), + "#SERVER.DISK#", mod(ns.formatUsage(true, float64(ns.Server.State.DiskUsed)/float64(ns.Server.Host.DiskTotal))), + "#SERVER.SPEEDIN#", mod(fmt.Sprintf("%s/s", ns.formatSize(ns.Server.State.NetInSpeed))), + "#SERVER.SPEEDOUT#", mod(fmt.Sprintf("%s/s", ns.formatSize(ns.Server.State.NetOutSpeed))), + "#SERVER.TRANSFERIN#", mod(ns.formatSize(ns.Server.State.NetInTransfer)), + "#SERVER.TRANSFEROUT#", mod(ns.formatSize(ns.Server.State.NetOutTransfer)), + + // Raw metrics + "#SERVER.CPUUSED#", mod(fmt.Sprintf("%f", ns.Server.State.CPU)), "#SERVER.MEMUSED#", mod(fmt.Sprintf("%d", ns.Server.State.MemUsed)), "#SERVER.SWAPUSED#", mod(fmt.Sprintf("%d", ns.Server.State.SwapUsed)), "#SERVER.DISKUSED#", mod(fmt.Sprintf("%d", ns.Server.State.DiskUsed)), @@ -184,8 +194,6 @@ func (ns *NotificationServerBundle) replaceParamsInString(str string, message st "#SERVER.DISKTOTAL#", mod(fmt.Sprintf("%d", ns.Server.Host.DiskTotal)), "#SERVER.NETINSPEED#", mod(fmt.Sprintf("%d", ns.Server.State.NetInSpeed)), "#SERVER.NETOUTSPEED#", mod(fmt.Sprintf("%d", ns.Server.State.NetOutSpeed)), - "#SERVER.TRANSFERIN#", mod(fmt.Sprintf("%d", ns.Server.State.NetInTransfer)), - "#SERVER.TRANSFEROUT#", mod(fmt.Sprintf("%d", ns.Server.State.NetOutTransfer)), "#SERVER.NETINTRANSFER#", mod(fmt.Sprintf("%d", ns.Server.State.NetInTransfer)), "#SERVER.NETOUTTRANSFER#", mod(fmt.Sprintf("%d", ns.Server.State.NetOutTransfer)), "#SERVER.LOAD1#", mod(fmt.Sprintf("%f", ns.Server.State.Load1)), @@ -219,3 +227,21 @@ func (ns *NotificationServerBundle) replaceParamsInString(str string, message st replacer := strings.NewReplacer(replacements...) return replacer.Replace(str) } + +func (ns *NotificationServerBundle) formatUsage(toPercentage bool, usage float64) string { + if ns.Notification.FormatMetricUnits != nil && *ns.Notification.FormatMetricUnits { + if toPercentage { + usage = usage * 100 + } + + return fmt.Sprintf("%.2f %%", usage) + } + return fmt.Sprintf("%f", usage) +} + +func (ns *NotificationServerBundle) formatSize(size uint64) string { + if ns.Notification.FormatMetricUnits != nil && *ns.Notification.FormatMetricUnits { + return utils.Bytes(size) + } + return fmt.Sprintf("%d", size) +} diff --git a/model/notification_api.go b/model/notification_api.go index 5bee8d2..a30971e 100644 --- a/model/notification_api.go +++ b/model/notification_api.go @@ -1,12 +1,13 @@ package model type NotificationForm struct { - Name string `json:"name,omitempty" minLength:"1"` - URL string `json:"url,omitempty"` - RequestMethod uint8 `json:"request_method,omitempty"` - RequestType uint8 `json:"request_type,omitempty"` - RequestHeader string `json:"request_header,omitempty"` - RequestBody string `json:"request_body,omitempty"` - VerifyTLS bool `json:"verify_tls,omitempty" validate:"optional"` - SkipCheck bool `json:"skip_check,omitempty" validate:"optional"` + Name string `json:"name,omitempty" minLength:"1"` + URL string `json:"url,omitempty"` + RequestMethod uint8 `json:"request_method,omitempty"` + RequestType uint8 `json:"request_type,omitempty"` + RequestHeader string `json:"request_header,omitempty"` + RequestBody string `json:"request_body,omitempty"` + VerifyTLS bool `json:"verify_tls,omitempty" validate:"optional"` + SkipCheck bool `json:"skip_check,omitempty" validate:"optional"` + FormatMetricUnits bool `json:"format_metric_units,omitempty" validate:"optional"` } diff --git a/model/notification_test.go b/model/notification_test.go index 480a319..5b7c197 100644 --- a/model/notification_test.go +++ b/model/notification_test.go @@ -27,12 +27,14 @@ type testSt struct { } func execCase(t *testing.T, item testSt) { + trueBool := true n := Notification{ - URL: item.url, - RequestMethod: item.reqMethod, - RequestType: item.reqType, - RequestBody: item.body, - RequestHeader: item.header, + URL: item.url, + RequestMethod: item.reqMethod, + RequestType: item.reqType, + RequestBody: item.body, + RequestHeader: item.header, + FormatMetricUnits: &trueBool, } server := Server{ Common: Common{}, @@ -45,7 +47,7 @@ func execCase(t *testing.T, item testSt) { CPU: nil, MemTotal: 0, DiskTotal: 0, - SwapTotal: 0, + SwapTotal: 8888, Arch: "", Virtualization: "", BootTime: 0, @@ -184,7 +186,29 @@ func TestNotification(t *testing.T) { }, { url: "https://example.com/?m=#NEZHA#", - body: `{"Server":"#SERVER.NAME#","ServerIP":"#SERVER.IP#","ServerSWAP":#SERVER.SWAP#}`, + body: `{"Server":"#SERVER.NAME#","ServerIP":"#SERVER.IP#","ServerSWAP":"#SERVER.SWAP#"}`, + reqMethod: NotificationRequestMethodPOST, + header: `{"asd":"dsa11"}`, + reqType: NotificationRequestTypeJSON, + expectURL: "https://example.com/?m=" + msg, + expectMethod: http.MethodPost, + expectContentType: reqTypeJSON, + expectBody: `{"Server":"ServerName","ServerIP":"1.1.1.1","ServerSWAP":"100.00 %"}`, + expectHeader: map[string]string{"asd": "dsa11"}, + }, + { + url: "https://example.com/?m=#NEZHA#", + body: `{"#NEZHA#":"#NEZHA#","Server":"#SERVER.NAME#","ServerIP":"#SERVER.IP#","ServerSWAP":"#SERVER.SWAP#"}`, + reqMethod: NotificationRequestMethodPOST, + reqType: NotificationRequestTypeForm, + expectURL: "https://example.com/?m=" + msg, + expectMethod: http.MethodPost, + expectContentType: reqTypeForm, + expectBody: "%23NEZHA%23=" + msg + "&Server=ServerName&ServerIP=1.1.1.1&ServerSWAP=100.00+%25", + }, + { + url: "https://example.com/?m=#NEZHA#", + body: `{"Server":"#SERVER.NAME#","ServerIP":"#SERVER.IP#","ServerSWAP":#SERVER.SWAPUSED#}`, reqMethod: NotificationRequestMethodPOST, header: `{"asd":"dsa11"}`, reqType: NotificationRequestTypeJSON, @@ -196,7 +220,7 @@ func TestNotification(t *testing.T) { }, { url: "https://example.com/?m=#NEZHA#", - body: `{"#NEZHA#":"#NEZHA#","Server":"#SERVER.NAME#","ServerIP":"#SERVER.IP#","ServerSWAP":"#SERVER.SWAP#"}`, + body: `{"#NEZHA#":"#NEZHA#","Server":"#SERVER.NAME#","ServerIP":"#SERVER.IP#","ServerSWAP":"#SERVER.SWAPUSED#"}`, reqMethod: NotificationRequestMethodPOST, reqType: NotificationRequestTypeForm, expectURL: "https://example.com/?m=" + msg, diff --git a/pkg/utils/bytes.go b/pkg/utils/bytes.go new file mode 100644 index 0000000..2de2701 --- /dev/null +++ b/pkg/utils/bytes.go @@ -0,0 +1,40 @@ +package utils + +import ( + "fmt" + "math" +) + +// https://github.com/dustin/go-humanize/blob/master/bytes.go + +func logn(n, b float64) float64 { + return math.Log(n) / math.Log(b) +} + +func countDigits(n int64) int { + digits := 0 + for n != 0 { + n /= 10 + digits += 1 + } + return digits +} + +func humanateBytes(s uint64, base float64, minDigits int, sizes []string) string { + if s < 10 { + return fmt.Sprintf("%d B", s) + } + e := math.Floor(logn(float64(s), base)) + suffix := sizes[min(len(sizes)-1, int(e))] // #nosec G602 + rounding := math.Pow10(minDigits - 1) + val := math.Floor(float64(s)/math.Pow(base, e)*rounding+0.5) / rounding + ff := "%%.%df %%s" + digits := max(minDigits-countDigits(int64(val)), 0) + f := fmt.Sprintf(ff, digits) + return fmt.Sprintf(f, val, suffix) +} + +func Bytes(s uint64) string { + sizes := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB"} + return humanateBytes(s, 1024, 2, sizes) +} From 439192b4c852e7227d67be3b19b1401973cf0462 Mon Sep 17 00:00:00 2001 From: naiba Date: Sat, 10 Jan 2026 17:38:15 +0800 Subject: [PATCH 26/45] chore: upgrade frontend --- service/singleton/frontend-templates.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/singleton/frontend-templates.yaml b/service/singleton/frontend-templates.yaml index 898c259..c7099b8 100644 --- a/service/singleton/frontend-templates.yaml +++ b/service/singleton/frontend-templates.yaml @@ -2,7 +2,7 @@ name: "OfficialAdmin" repository: "https://github.com/nezhahq/admin-frontend" author: "nezhahq" - version: "v1.14.5" + version: "v1.14.6" is_admin: true is_official: true - path: "user-dist" From 4c4758207d70f92a95bd414ce3175914754dfa4b Mon Sep 17 00:00:00 2001 From: naiba Date: Sat, 10 Jan 2026 20:23:28 +0800 Subject: [PATCH 27/45] chore: upgrade frontend --- service/singleton/frontend-templates.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/singleton/frontend-templates.yaml b/service/singleton/frontend-templates.yaml index c7099b8..d257fbd 100644 --- a/service/singleton/frontend-templates.yaml +++ b/service/singleton/frontend-templates.yaml @@ -2,7 +2,7 @@ name: "OfficialAdmin" repository: "https://github.com/nezhahq/admin-frontend" author: "nezhahq" - version: "v1.14.6" + version: "v1.14.7" is_admin: true is_official: true - path: "user-dist" From e61772e85878d2fc85be3e5b2042946aaa0c454f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A5=B6=E7=88=B8?= Date: Sun, 15 Feb 2026 13:13:33 +0800 Subject: [PATCH 28/45] feat(v2.0.0): tsdb (#1162) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: tsdb * fix(ci): remove --parseGoList=false from swag init to fix dependency resolution * fix(ci): fix swag init directory and temporary remove s390x support due to cgo issues * fix(ci): fix swag init output directory to cmd/dashboard/docs * fix(ci): set GOTOOLCHAIN=auto for gosec * feat: add system storage maintenance for SQLite and TSDB * shit * feat: add s390x support and improve service monitoring * ci: upgrade goreleaser-cross image to v1.25 * ci: add libzstd-dev:s390x for cross-compilation * ci: build libzstd for s390x from source * ci: add libzstd_linux_s390x.go for gozstd linking * ci: use vendor mode for s390x gozstd build * ci: clone zstd source for s390x build * refactor(tsdb): rename MaxDiskUsageGB to MinFreeDiskSpaceGB and optimize queries - Rename config to accurately reflect VictoriaMetrics behavior: minimum free disk space threshold - Add QueryServiceHistoryByServerID for batch query optimization - Fix hasStatus to avoid false status counting when only delay data exists - Fix service aggregation boundary: use successCount*2 >= count - Fix serviceID parsing with strconv.ParseUint error handling - Add TagFiltersCacheSize for better query performance * feat(api): add server metrics endpoint and simplify service history response - Add /server/:id/metrics API for querying TSDB server metrics - Simplify getServiceHistory by removing redundant data conversion - Change AvgDelay type from float32 to float64 - Remove generated swagger docs (to be regenerated) - Update TSDB query, writer and tests * chore: 临时禁用不支持前端 * ci: cache zstd build for s390x to speed up CI * fix(tsdb): fix race conditions, data correctness and optimize performance - Fix TOCTOU race between IsClosed() and write/query by holding RLock - Fix delay=0 excluded from stats by using hasDelay flag instead of value > 0 - Fix fmt.Sscanf -> strconv.ParseUint for server_id parsing with error logging - Fix buffer unbounded growth by flushing inside lock when over maxSize - Split makeMetricRow into makeServerMetricRow/makeServiceMetricRow - Extract InitGlobalSettings() from Open() for VictoriaMetrics globals - Remove redundant instance/GetInstance/SetInstance singleton - Add error logging for silently skipped block decode errors - Optimize WriteBatch* to build all rows in single write call - Optimize downsample to use linear scan instead of map for sorted data - Optimize query slice reuse across block iterations * 服务添加DisplayIndex (#1166) * 服务添加DisplayIndex * 根据ai建议修改 --------- Co-authored-by: huYang <306061454@qq.com> * fix(tsdb): restore SQLite fallback and monthly status reload on restart - Restore ServiceHistory model and SQLite write fallback when TSDB is disabled - Reload monthlyStatus (30-day) and serviceStatusToday from TSDB/SQLite on startup - Add SQLite fallback query for /service/:id/history and /server/:id/service - Remove breaking GET /service/:id endpoint, keep /service/:id/history only - Add QueryServiceDailyStats to TSDB for per-day aggregation - Add tests for monthly status and today stats loading from both TSDB and SQLite - Migrate ServiceHistory table only when TSDB is disabled * ci: exclude false-positive gosec rules G117, G703, G704 * feat(api): expose tsdb_enabled in setting response * ci: restore G115 exclusion accidentally dropped in previous commit * fix: update version numbers for OfficialAdmin and Official templates * chore: upgrade frontend * chore: upgrade frontend --------- Co-authored-by: 胡说丷刂 <34758853+laosan-xx@users.noreply.github.com> Co-authored-by: huYang <306061454@qq.com> --- .github/workflows/release.yml | 49 +- .github/workflows/test.yml | 6 +- .goreleaser.yml | 1 + cmd/dashboard/controller/controller.go | 5 +- cmd/dashboard/controller/server.go | 87 +++ cmd/dashboard/controller/service.go | 346 ++++++++--- cmd/dashboard/controller/setting.go | 16 + cmd/dashboard/main.go | 19 +- go.mod | 36 +- go.sum | 101 ++-- model/config.go | 22 + model/service.go | 1 + model/service_api.go | 3 +- model/service_history.go | 2 +- model/service_history_api.go | 58 +- model/setting_api.go | 5 +- pkg/tsdb/config.go | 67 +++ pkg/tsdb/maintenance.go | 17 + pkg/tsdb/query.go | 664 ++++++++++++++++++++++ pkg/tsdb/tsdb.go | 117 ++++ pkg/tsdb/tsdb_test.go | 622 ++++++++++++++++++++ pkg/tsdb/writer.go | 301 ++++++++++ service/rpc/nezha.go | 39 ++ service/singleton/frontend-templates.yaml | 4 +- service/singleton/servicesentinel.go | 242 +++++--- service/singleton/servicesentinel_test.go | 336 +++++++++++ service/singleton/singleton.go | 36 +- service/singleton/tsdb.go | 73 +++ 28 files changed, 3054 insertions(+), 221 deletions(-) create mode 100644 pkg/tsdb/config.go create mode 100644 pkg/tsdb/maintenance.go create mode 100644 pkg/tsdb/query.go create mode 100644 pkg/tsdb/tsdb.go create mode 100644 pkg/tsdb/tsdb_test.go create mode 100644 pkg/tsdb/writer.go create mode 100644 service/singleton/servicesentinel_test.go create mode 100644 service/singleton/tsdb.go diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 78551c6..1ecd3b9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,17 +15,18 @@ jobs: goos: [linux, windows] goarch: [amd64] include: - - goos: linux - goarch: s390x - goos: linux goarch: arm64 + - goos: linux + goarch: s390x name: Build artifacts runs-on: ubuntu-latest container: - image: goreleaser/goreleaser-cross:v1.24 + image: goreleaser/goreleaser-cross:v1.25 steps: - - run: | + - name: Install dependencies + run: | apt update && apt install unzip curl -y wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/bin/yq chmod +x /usr/bin/yq @@ -53,7 +54,45 @@ jobs: - name: generate swagger docs run: | go install github.com/swaggo/swag/cmd/swag@latest - swag init --pd -d . -g ./cmd/dashboard/main.go -o ./cmd/dashboard/docs --parseGoList=false + swag init --pd -d cmd/dashboard -g main.go -o cmd/dashboard/docs + go mod tidy + + - name: Cache zstd for s390x + if: matrix.goarch == 's390x' + id: cache-zstd + uses: actions/cache@v4 + with: + path: /tmp/zstd-s390x + key: zstd-s390x-v1.5.7 + + - name: Build zstd for s390x + if: matrix.goarch == 's390x' + run: | + # gozstd doesn't ship prebuilt libzstd for s390x, build it manually + go mod vendor + + if [ -f /tmp/zstd-s390x/libzstd.a ]; then + echo "Using cached libzstd.a" + else + # Clone zstd source and build for s390x + ZSTD_VERSION=v1.5.7 + git clone --depth 1 --branch ${ZSTD_VERSION} https://github.com/facebook/zstd /tmp/zstd + cd /tmp/zstd/lib + CC=s390x-linux-gnu-gcc \ + CXX=s390x-linux-gnu-g++ \ + AR=s390x-linux-gnu-ar \ + ZSTD_LEGACY_SUPPORT=0 \ + make clean libzstd.a + mkdir -p /tmp/zstd-s390x + cp libzstd.a /tmp/zstd-s390x/libzstd.a + fi + + # Copy to vendor directory + GOZSTD_VENDOR=${GITHUB_WORKSPACE}/vendor/github.com/valyala/gozstd + cp /tmp/zstd-s390x/libzstd.a ${GOZSTD_VENDOR}/libzstd_linux_s390x.a + + # Create the Go file to link the library + printf '//go:build linux && s390x\n\npackage gozstd\n\n/*\n#cgo LDFLAGS: ${SRCDIR}/libzstd_linux_s390x.a\n*/\nimport "C"\n' > ${GOZSTD_VENDOR}/libzstd_linux_s390x.go - name: Build with tag if: contains(github.ref, 'refs/tags/') diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2b5a6f2..a668c60 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,7 +34,7 @@ jobs: go install github.com/swaggo/swag/cmd/swag@latest touch ./cmd/dashboard/user-dist/a touch ./cmd/dashboard/admin-dist/a - swag init --pd -d . -g ./cmd/dashboard/main.go -o ./cmd/dashboard/docs --parseGoList=false + swag init --pd -d cmd/dashboard -g main.go -o cmd/dashboard/docs - name: Unit test run: | @@ -46,5 +46,7 @@ jobs: - name: Run Gosec Security Scanner if: runner.os == 'Linux' uses: securego/gosec@master + env: + GOTOOLCHAIN: auto with: - args: --exclude=G104,G402,G115,G203 ./... + args: --exclude=G104,G115,G117,G203,G402,G703,G704 ./... diff --git a/.goreleaser.yml b/.goreleaser.yml index 55e342e..5562b94 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -52,6 +52,7 @@ builds: flags: - -trimpath - -buildvcs=false + - -mod=vendor tags: - go_json goos: diff --git a/cmd/dashboard/controller/controller.go b/cmd/dashboard/controller/controller.go index 7683a89..4e08988 100644 --- a/cmd/dashboard/controller/controller.go +++ b/cmd/dashboard/controller/controller.go @@ -73,8 +73,10 @@ func routers(r *gin.Engine, frontendDist fs.FS) { optionalAuth.GET("/server-group", commonHandler(listServerGroup)) optionalAuth.GET("/service", commonHandler(showService)) - optionalAuth.GET("/service/:id", commonHandler(listServiceHistory)) optionalAuth.GET("/service/server", commonHandler(listServerWithServices)) + optionalAuth.GET("/service/:id/history", commonHandler(getServiceHistory)) + optionalAuth.GET("/server/:id/service", commonHandler(listServerServices)) + optionalAuth.GET("/server/:id/metrics", commonHandler(getServerMetrics)) auth := api.Group("", authMw) @@ -150,6 +152,7 @@ func routers(r *gin.Engine, frontendDist fs.FS) { auth.POST("/online-user/batch-block", adminHandler(batchBlockOnlineUser)) auth.PATCH("/setting", adminHandler(updateConfig)) + auth.POST("/maintenance", adminHandler(runMaintenance)) r.NoRoute(fallbackToFrontend(frontendDist)) } diff --git a/cmd/dashboard/controller/server.go b/cmd/dashboard/controller/server.go index dec44bc..880db1f 100644 --- a/cmd/dashboard/controller/server.go +++ b/cmd/dashboard/controller/server.go @@ -12,6 +12,7 @@ import ( "gorm.io/gorm" "github.com/nezhahq/nezha/model" + "github.com/nezhahq/nezha/pkg/tsdb" pb "github.com/nezhahq/nezha/proto" "github.com/nezhahq/nezha/service/singleton" ) @@ -366,3 +367,89 @@ func batchMoveServer(c *gin.Context) (any, error) { return nil, nil } + +var serverMetricMap = map[string]tsdb.MetricType{ + "cpu": tsdb.MetricServerCPU, + "memory": tsdb.MetricServerMemory, + "swap": tsdb.MetricServerSwap, + "disk": tsdb.MetricServerDisk, + "net_in_speed": tsdb.MetricServerNetInSpeed, + "net_out_speed": tsdb.MetricServerNetOutSpeed, + "net_in_transfer": tsdb.MetricServerNetInTransfer, + "net_out_transfer": tsdb.MetricServerNetOutTransfer, + "load1": tsdb.MetricServerLoad1, + "load5": tsdb.MetricServerLoad5, + "load15": tsdb.MetricServerLoad15, + "tcp_conn": tsdb.MetricServerTCPConn, + "udp_conn": tsdb.MetricServerUDPConn, + "process_count": tsdb.MetricServerProcessCount, + "temperature": tsdb.MetricServerTemperature, + "uptime": tsdb.MetricServerUptime, + "gpu": tsdb.MetricServerGPU, +} + +// Get server metrics history +// @Summary Get server metrics history +// @Security BearerAuth +// @Schemes +// @Description Get server metrics history for a specific server +// @Tags common +// @param id path uint true "Server ID" +// @param metric query string true "Metric name: cpu, memory, swap, disk, net_in_speed, net_out_speed, net_in_transfer, net_out_transfer, load1, load5, load15, tcp_conn, udp_conn, process_count, temperature, uptime, gpu" +// @param period query string false "Time period: 1d, 7d, 30d (default: 1d)" +// @Produce json +// @Success 200 {object} model.CommonResponse[model.ServerMetricsResponse] +// @Router /server/{id}/metrics [get] +func getServerMetrics(c *gin.Context) (*model.ServerMetricsResponse, error) { + idStr := c.Param("id") + serverID, err := strconv.ParseUint(idStr, 10, 64) + if err != nil { + return nil, err + } + + server, ok := singleton.ServerShared.Get(serverID) + if !ok { + return nil, singleton.Localizer.ErrorT("server not found") + } + + _, isMember := c.Get(model.CtxKeyAuthorizedUser) + if server.HideForGuest && !isMember { + return nil, singleton.Localizer.ErrorT("unauthorized") + } + + metricName := c.Query("metric") + metricType, ok := serverMetricMap[metricName] + if !ok { + return nil, singleton.Localizer.ErrorT("invalid metric name") + } + + periodStr := c.DefaultQuery("period", "1d") + period, err := tsdb.ParseQueryPeriod(periodStr) + if err != nil { + return nil, err + } + + if !isMember && period != tsdb.Period1Day { + return nil, singleton.Localizer.ErrorT("unauthorized: only 1d data available for guests") + } + + response := &model.ServerMetricsResponse{ + ServerID: serverID, + ServerName: server.Name, + Metric: metricName, + DataPoints: make([]model.ServerMetricsDataPoint, 0), + } + + if !singleton.TSDBEnabled() { + return response, nil + } + + points, err := singleton.TSDBShared.QueryServerMetrics(serverID, metricType, period) + if err != nil { + return nil, err + } + + response.DataPoints = points + + return response, nil +} diff --git a/cmd/dashboard/controller/service.go b/cmd/dashboard/controller/service.go index a15e2ca..3f97a73 100644 --- a/cmd/dashboard/controller/service.go +++ b/cmd/dashboard/controller/service.go @@ -9,11 +9,11 @@ import ( "github.com/gin-gonic/gin" "github.com/jinzhu/copier" + "gorm.io/gorm" "github.com/nezhahq/nezha/model" - "github.com/nezhahq/nezha/pkg/utils" + "github.com/nezhahq/nezha/pkg/tsdb" "github.com/nezhahq/nezha/service/singleton" - "gorm.io/gorm" ) // Show service @@ -55,7 +55,7 @@ func showService(c *gin.Context) (*model.ServiceResponse, error) { // @Param id query uint false "Resource ID" // @Produce json // @Success 200 {object} model.CommonResponse[[]model.Service] -// @Router /service [get] +// @Router /service/list [get] func listService(c *gin.Context) ([]*model.Service, error) { var ss []*model.Service ssl := singleton.ServiceSentinelShared.GetSortedList() @@ -66,96 +66,321 @@ func listService(c *gin.Context) ([]*model.Service, error) { return ss, nil } -// List service histories by server id +// Get service history +// @Summary Get service history by service ID +// @Security BearerAuth +// @Schemes +// @Description Get service monitoring history for a specific service +// @Tags common +// @param id path uint true "Service ID" +// @param period query string false "Time period: 1d, 7d, 30d (default: 1d)" +// @Produce json +// @Success 200 {object} model.CommonResponse[model.ServiceHistoryResponse] +// @Router /service/{id}/history [get] +func getServiceHistory(c *gin.Context) (*model.ServiceHistoryResponse, error) { + idStr := c.Param("id") + serviceID, err := strconv.ParseUint(idStr, 10, 64) + if err != nil { + return nil, err + } + + // 检查服务是否存在 + service, ok := singleton.ServiceSentinelShared.Get(serviceID) + if !ok || service == nil { + return nil, singleton.Localizer.ErrorT("service not found") + } + + // 解析时间范围 + periodStr := c.DefaultQuery("period", "1d") + period, err := tsdb.ParseQueryPeriod(periodStr) + if err != nil { + return nil, err + } + + // 权限检查:未登录用户只能查看 1d 数据 + _, isMember := c.Get(model.CtxKeyAuthorizedUser) + if !isMember && period != tsdb.Period1Day { + return nil, singleton.Localizer.ErrorT("unauthorized: only 1d data available for guests") + } + + response := &model.ServiceHistoryResponse{ + ServiceID: serviceID, + ServiceName: service.Name, + Servers: make([]model.ServerServiceStats, 0), + } + + if !singleton.TSDBEnabled() { + return queryServiceHistoryFromDB(serviceID, period, response) + } + + result, err := singleton.TSDBShared.QueryServiceHistory(serviceID, period) + if err != nil { + return nil, err + } + + serverMap := singleton.ServerShared.GetList() + + for i := range result.Servers { + if server, ok := serverMap[result.Servers[i].ServerID]; ok { + result.Servers[i].ServerName = server.Name + } + } + response.Servers = result.Servers + + return response, nil +} + +func queryServiceHistoryFromDB(serviceID uint64, period tsdb.QueryPeriod, response *model.ServiceHistoryResponse) (*model.ServiceHistoryResponse, error) { + since := time.Now().Add(-period.Duration()) + + var histories []model.ServiceHistory + if err := singleton.DB.Where("service_id = ? AND server_id != 0 AND created_at >= ?", serviceID, since). + Order("server_id, created_at").Find(&histories).Error; err != nil { + return nil, err + } + + serverMap := singleton.ServerShared.GetList() + grouped := make(map[uint64][]model.ServiceHistory) + for _, h := range histories { + grouped[h.ServerID] = append(grouped[h.ServerID], h) + } + + for serverID, records := range grouped { + stats := model.ServerServiceStats{ + ServerID: serverID, + } + if server, ok := serverMap[serverID]; ok { + stats.ServerName = server.Name + } + + var totalDelay float64 + var totalUp, totalDown uint64 + dps := make([]model.DataPoint, 0, len(records)) + for _, r := range records { + status := uint8(1) + if r.Down > 0 && r.Up == 0 { + status = 0 + } + dps = append(dps, model.DataPoint{ + Timestamp: r.CreatedAt.Unix() * 1000, + Delay: r.AvgDelay, + Status: status, + }) + totalDelay += r.AvgDelay + totalUp += r.Up + totalDown += r.Down + } + + var avgDelay float64 + if len(records) > 0 { + avgDelay = totalDelay / float64(len(records)) + } + var upPercent float32 + if totalUp+totalDown > 0 { + upPercent = float32(totalUp) / float32(totalUp+totalDown) * 100 + } + stats.Stats = model.ServiceHistorySummary{ + AvgDelay: avgDelay, + UpPercent: upPercent, + TotalUp: totalUp, + TotalDown: totalDown, + DataPoints: dps, + } + response.Servers = append(response.Servers, stats) + } + + return response, nil +} + +// List server services // @Summary List service histories by server id // @Security BearerAuth // @Schemes -// @Description List service histories by server id +// @Description List service histories for a specific server // @Tags common // @param id path uint true "Server ID" +// @param period query string false "Time period: 1d, 7d, 30d (default: 1d)" // @Produce json // @Success 200 {object} model.CommonResponse[[]model.ServiceInfos] -// @Router /service/{id} [get] -func listServiceHistory(c *gin.Context) ([]*model.ServiceInfos, error) { +// @Router /server/{id}/service [get] +func listServerServices(c *gin.Context) ([]*model.ServiceInfos, error) { idStr := c.Param("id") - id, err := strconv.ParseUint(idStr, 10, 64) + serverID, err := strconv.ParseUint(idStr, 10, 64) if err != nil { return nil, err } m := singleton.ServerShared.GetList() - server, ok := m[id] + server, ok := m[serverID] if !ok || server == nil { return nil, singleton.Localizer.ErrorT("server not found") } _, isMember := c.Get(model.CtxKeyAuthorizedUser) - authorized := isMember // TODO || isViewPasswordVerfied + authorized := isMember if server.HideForGuest && !authorized { return nil, singleton.Localizer.ErrorT("unauthorized") } - var serviceHistories []*model.ServiceHistory - if err := singleton.DB.Model(&model.ServiceHistory{}).Select("service_id, created_at, server_id, avg_delay"). - Where("server_id = ?", id).Where("created_at >= ?", time.Now().Add(-24*time.Hour)).Order("service_id, created_at"). - Scan(&serviceHistories).Error; err != nil { + // 解析时间范围 + periodStr := c.DefaultQuery("period", "1d") + period, err := tsdb.ParseQueryPeriod(periodStr) + if err != nil { return nil, err } - var sortedServiceIDs []uint64 - resultMap := make(map[uint64]*model.ServiceInfos) - for _, history := range serviceHistories { - infos, ok := resultMap[history.ServiceID] - service, _ := singleton.ServiceSentinelShared.Get(history.ServiceID) - if !ok { - infos = &model.ServiceInfos{ - ServiceID: history.ServiceID, - ServerID: history.ServerID, - ServiceName: service.Name, - ServerName: m[history.ServerID].Name, + // 权限检查:未登录用户只能查看 1d 数据 + if !isMember && period != tsdb.Period1Day { + return nil, singleton.Localizer.ErrorT("unauthorized: only 1d data available for guests") + } + + services := singleton.ServiceSentinelShared.GetSortedList() + + var result []*model.ServiceInfos + + if !singleton.TSDBEnabled() { + return queryServerServicesFromDB(serverID, server.Name, period, services) + } + + historyResults, err := singleton.TSDBShared.QueryServiceHistoryByServerID(serverID, period) + if err != nil { + return nil, err + } + + for _, service := range services { + if service.Cover == model.ServiceCoverAll { + if service.SkipServers[serverID] { + continue + } + } else { + if !service.SkipServers[serverID] { + continue } - resultMap[history.ServiceID] = infos - sortedServiceIDs = append(sortedServiceIDs, history.ServiceID) } - infos.CreatedAt = append(infos.CreatedAt, history.CreatedAt.Truncate(time.Minute).Unix()*1000) - infos.AvgDelay = append(infos.AvgDelay, history.AvgDelay) + + historyResult, ok := historyResults[service.ID] + if !ok || len(historyResult.Servers) == 0 { + continue + } + + serverStats := historyResult.Servers[0] + + infos := &model.ServiceInfos{ + ServiceID: service.ID, + ServerID: serverID, + ServiceName: service.Name, + ServerName: server.Name, + DisplayIndex: service.DisplayIndex, + CreatedAt: make([]int64, len(serverStats.Stats.DataPoints)), + AvgDelay: make([]float64, len(serverStats.Stats.DataPoints)), + } + + for i, dp := range serverStats.Stats.DataPoints { + infos.CreatedAt[i] = dp.Timestamp + infos.AvgDelay[i] = dp.Delay + } + + result = append(result, infos) } - ret := make([]*model.ServiceInfos, 0, len(sortedServiceIDs)) - for _, id := range sortedServiceIDs { - ret = append(ret, resultMap[id]) + return result, nil +} + +func queryServerServicesFromDB(serverID uint64, serverName string, period tsdb.QueryPeriod, services []*model.Service) ([]*model.ServiceInfos, error) { + since := time.Now().Add(-period.Duration()) + + var histories []model.ServiceHistory + if err := singleton.DB.Where("server_id = ? AND created_at >= ?", serverID, since). + Order("service_id, created_at").Find(&histories).Error; err != nil { + return nil, err } - return ret, nil + grouped := make(map[uint64][]model.ServiceHistory) + for _, h := range histories { + grouped[h.ServiceID] = append(grouped[h.ServiceID], h) + } + + var result []*model.ServiceInfos + for _, service := range services { + if service.Cover == model.ServiceCoverAll { + if service.SkipServers[serverID] { + continue + } + } else { + if !service.SkipServers[serverID] { + continue + } + } + + records, ok := grouped[service.ID] + if !ok { + continue + } + + infos := &model.ServiceInfos{ + ServiceID: service.ID, + ServerID: serverID, + ServiceName: service.Name, + ServerName: serverName, + DisplayIndex: service.DisplayIndex, + CreatedAt: make([]int64, 0, len(records)), + AvgDelay: make([]float64, 0, len(records)), + } + + for _, r := range records { + infos.CreatedAt = append(infos.CreatedAt, r.CreatedAt.Truncate(time.Minute).Unix()*1000) + infos.AvgDelay = append(infos.AvgDelay, r.AvgDelay) + } + + result = append(result, infos) + } + + return result, nil } // List server with service // @Summary List server with service // @Security BearerAuth // @Schemes -// @Description List server with service +// @Description List servers that have service monitoring data // @Tags common // @Produce json // @Success 200 {object} model.CommonResponse[[]uint64] // @Router /service/server [get] func listServerWithServices(c *gin.Context) ([]uint64, error) { - var serverIdsWithService []uint64 - if err := singleton.DB.Model(&model.ServiceHistory{}). - Select("distinct(server_id)"). - Where("server_id != 0"). - Find(&serverIdsWithService).Error; err != nil { - return nil, newGormError("%v", err) + // 从内存中获取有服务监控配置的服务器列表 + services := singleton.ServiceSentinelShared.GetList() + serverMap := singleton.ServerShared.GetList() + + serverIDSet := make(map[uint64]bool) + + for _, service := range services { + if service.Cover == model.ServiceCoverAll { + // 除了跳过的服务器,其他都包含 + for serverID := range serverMap { + if !service.SkipServers[serverID] { + serverIDSet[serverID] = true + } + } + } else { + // 只包含指定的服务器 + for serverID, enabled := range service.SkipServers { + if enabled { + serverIDSet[serverID] = true + } + } + } } _, isMember := c.Get(model.CtxKeyAuthorizedUser) - authorized := isMember // TODO || isViewPasswordVerfied + authorized := isMember var ret []uint64 - for _, id := range serverIdsWithService { - server, ok := singleton.ServerShared.Get(id) + for id := range serverIDSet { + server, ok := serverMap[id] if !ok || server == nil { - return nil, singleton.Localizer.ErrorT("server not found") + continue } if !server.HideForGuest || authorized { ret = append(ret, id) @@ -191,6 +416,7 @@ func createService(c *gin.Context) (uint64, error) { m.Type = mf.Type m.SkipServers = mf.SkipServers m.Cover = mf.Cover + m.DisplayIndex = mf.DisplayIndex m.Notify = mf.Notify m.NotificationGroupID = mf.NotificationGroupID m.Duration = mf.Duration @@ -210,21 +436,6 @@ func createService(c *gin.Context) (uint64, error) { return 0, newGormError("%v", err) } - var skipServers []uint64 - for k := range m.SkipServers { - skipServers = append(skipServers, k) - } - - var err error - if m.Cover == 0 { - err = singleton.DB.Unscoped().Delete(&model.ServiceHistory{}, "service_id = ? and server_id in (?)", m.ID, skipServers).Error - } else { - err = singleton.DB.Unscoped().Delete(&model.ServiceHistory{}, "service_id = ? and server_id not in (?)", m.ID, skipServers).Error - } - if err != nil { - return 0, err - } - if err := singleton.ServiceSentinelShared.Update(&m); err != nil { return 0, err } @@ -269,6 +480,7 @@ func updateService(c *gin.Context) (any, error) { m.Type = mf.Type m.SkipServers = mf.SkipServers m.Cover = mf.Cover + m.DisplayIndex = mf.DisplayIndex m.Notify = mf.Notify m.NotificationGroupID = mf.NotificationGroupID m.Duration = mf.Duration @@ -288,17 +500,6 @@ func updateService(c *gin.Context) (any, error) { return nil, newGormError("%v", err) } - skipServers := utils.MapKeysToSlice(mf.SkipServers) - - if m.Cover == model.ServiceCoverAll { - err = singleton.DB.Unscoped().Delete(&model.ServiceHistory{}, "service_id = ? and server_id in (?)", m.ID, skipServers).Error - } else { - err = singleton.DB.Unscoped().Delete(&model.ServiceHistory{}, "service_id = ? and server_id not in (?) and server_id > 0", m.ID, skipServers).Error - } - if err != nil { - return nil, err - } - if err := singleton.ServiceSentinelShared.Update(&m); err != nil { return nil, err } @@ -329,10 +530,7 @@ func batchDeleteService(c *gin.Context) (any, error) { } err := singleton.DB.Transaction(func(tx *gorm.DB) error { - if err := tx.Unscoped().Delete(&model.Service{}, "id in (?)", ids).Error; err != nil { - return err - } - return tx.Unscoped().Delete(&model.ServiceHistory{}, "service_id in (?)", ids).Error + return tx.Unscoped().Delete(&model.Service{}, "id in (?)", ids).Error }) if err != nil { return nil, err diff --git a/cmd/dashboard/controller/setting.go b/cmd/dashboard/controller/setting.go index 427b901..4fd8906 100644 --- a/cmd/dashboard/controller/setting.go +++ b/cmd/dashboard/controller/setting.go @@ -39,6 +39,7 @@ func listConfig(c *gin.Context) (*model.SettingResponse, error) { }, Version: singleton.Version, FrontendTemplates: singleton.FrontendTemplates, + TSDBEnabled: singleton.TSDBEnabled(), } if !authorized || !isAdmin { @@ -54,6 +55,7 @@ func listConfig(c *gin.Context) (*model.SettingResponse, error) { ConfigDashboard: configDashboard, Oauth2Providers: config.Oauth2Providers, }, + TSDBEnabled: singleton.TSDBEnabled(), } } @@ -113,3 +115,17 @@ func updateConfig(c *gin.Context) (any, error) { singleton.OnUpdateLang(singleton.Conf.Language) return nil, nil } + +// Perform maintenance +// @Summary Perform maintenance +// @Security BearerAuth +// @Schemes +// @Description Perform system maintenance (SQLite VACUUM and TSDB maintenance) +// @Tags admin required +// @Produce json +// @Success 200 {object} model.CommonResponse[any] +// @Router /maintenance [post] +func runMaintenance(c *gin.Context) (any, error) { + singleton.PerformMaintenance() + return nil, nil +} diff --git a/cmd/dashboard/main.go b/cmd/dashboard/main.go index 0ae23de..63752a2 100644 --- a/cmd/dashboard/main.go +++ b/cmd/dashboard/main.go @@ -11,6 +11,7 @@ import ( "net" "net/http" "os" + "runtime/debug" "strings" "time" _ "time/tzdata" @@ -65,8 +66,8 @@ func initSystem(bus chan<- *model.Service) error { return err } - // 每天的3:30 对 监控记录 和 流量记录 进行清理 - if _, err := singleton.CronShared.AddFunc("0 30 3 * * *", singleton.CleanServiceHistory); err != nil { + // 每天的3:30 对流量记录进行清理 + if _, err := singleton.CronShared.AddFunc("0 30 3 * * *", singleton.CleanMonitorHistory); err != nil { return err } @@ -109,12 +110,19 @@ func main() { os.Exit(0) } - serviceSentinelDispatchBus := make(chan *model.Service) // 用于传递服务监控任务信息的channel - // 初始化 dao 包 + serviceSentinelDispatchBus := make(chan *model.Service) if err := utils.FirstError(singleton.InitFrontendTemplates, func() error { return singleton.InitConfigFromPath(dashboardCliParam.ConfigFile) }, singleton.InitTimezoneAndCache, + func() error { + if singleton.Conf.Memory.GoMemLimitMB > 0 { + debug.SetMemoryLimit(singleton.Conf.Memory.GoMemLimitMB * 1024 * 1024) + log.Printf("NEZHA>> Go memory limit set to %d MB", singleton.Conf.Memory.GoMemLimitMB) + } + return nil + }, func() error { return singleton.InitDBFromPath(dashboardCliParam.DatabaseLocation) }, + singleton.InitTSDB, func() error { return initSystem(serviceSentinelDispatchBus) }); err != nil { log.Fatal(err) } @@ -124,7 +132,7 @@ func main() { log.Fatal(err) } - singleton.CleanServiceHistory() + singleton.CleanMonitorHistory() rpc.DispatchKeepalive() go rpc.DispatchTask(serviceSentinelDispatchBus) go singleton.AlertSentinelStart() @@ -172,6 +180,7 @@ func main() { }, func(c context.Context) error { log.Println("NEZHA>> Graceful::START") singleton.RecordTransferHourlyUsage() + singleton.CloseTSDB() log.Println("NEZHA>> Graceful::END") var err error if muxServerHTTPS != nil { diff --git a/go.mod b/go.mod index a1278bc..d604d2a 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,9 @@ module github.com/nezhahq/nezha -go 1.25 +go 1.25.6 require ( + github.com/VictoriaMetrics/VictoriaMetrics v1.134.0 github.com/appleboy/gin-jwt/v2 v2.10.3 github.com/dustinkirkland/golang-petname v0.0.0-20240428194347-eebcea082ee0 github.com/gin-contrib/pprof v1.5.3 @@ -15,7 +16,7 @@ require ( github.com/knadh/koanf/maps v0.1.2 github.com/knadh/koanf/providers/env v1.1.0 github.com/knadh/koanf/providers/file v1.2.0 - github.com/knadh/koanf/v2 v2.2.0 + github.com/knadh/koanf/v2 v2.3.0 github.com/leonelquinteros/gotext v1.7.1 github.com/libdns/cloudflare v0.2.1 github.com/libdns/he v1.1.1 @@ -26,17 +27,18 @@ require ( github.com/oschwald/maxminddb-golang v1.13.1 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/robfig/cron/v3 v3.0.1 - github.com/stretchr/testify v1.10.0 + github.com/stretchr/testify v1.11.1 github.com/swaggo/files v1.0.1 github.com/swaggo/gin-swagger v1.6.0 + github.com/swaggo/swag v1.16.4 github.com/tidwall/gjson v1.18.0 golang.org/x/crypto v0.45.0 - golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 + golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 golang.org/x/net v0.47.0 - golang.org/x/oauth2 v0.29.0 + golang.org/x/oauth2 v0.32.0 golang.org/x/sync v0.18.0 - google.golang.org/grpc v1.72.0 - google.golang.org/protobuf v1.36.6 + google.golang.org/grpc v1.76.0 + google.golang.org/protobuf v1.36.10 gorm.io/driver/sqlite v1.5.7 gorm.io/gorm v1.26.0 sigs.k8s.io/yaml v1.4.0 @@ -44,8 +46,13 @@ require ( require ( github.com/KyleBanks/depth v1.2.1 // indirect + github.com/VictoriaMetrics/easyproto v1.1.3 // indirect + github.com/VictoriaMetrics/fastcache v1.13.2 // indirect + github.com/VictoriaMetrics/metrics v1.40.2 // indirect + github.com/VictoriaMetrics/metricsql v0.84.8 // indirect github.com/bytedance/sonic v1.13.2 // indirect github.com/bytedance/sonic/loader v0.2.4 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudwego/base64x v0.1.5 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect @@ -59,10 +66,12 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.26.0 // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect + github.com/golang/snappy v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mailru/easyjson v0.9.0 // indirect @@ -71,23 +80,28 @@ require ( github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/swaggo/swag v1.16.4 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fastrand v1.1.0 // indirect + github.com/valyala/gozstd v1.24.0 // indirect + github.com/valyala/histogram v1.2.0 // indirect + github.com/valyala/quicktemplate v1.8.0 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect golang.org/x/arch v0.16.0 // indirect golang.org/x/mod v0.29.0 // indirect golang.org/x/sys v0.38.0 // indirect golang.org/x/text v0.31.0 // indirect - golang.org/x/time v0.11.0 // indirect + golang.org/x/time v0.14.0 // indirect golang.org/x/tools v0.38.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251007200510-49b9836ed3ff // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 407e96c..22350d6 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,17 @@ github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/VictoriaMetrics/VictoriaMetrics v1.134.0 h1:0FgGM0rVRcTzd9qtO1gHlgLC/kBA1gsi+iwSYXAa/rQ= +github.com/VictoriaMetrics/VictoriaMetrics v1.134.0/go.mod h1:vUnt83zBB65TkVq7zSjSuJFUZFwnmV+hdF2KmkUbY0U= +github.com/VictoriaMetrics/easyproto v1.1.3 h1:gRSA3ZQs7n4+5I+SniDWD59jde1jVq4JmgQ9HUUyvk4= +github.com/VictoriaMetrics/easyproto v1.1.3/go.mod h1:QlGlzaJnDfFd8Lk6Ci/fuLxfTo3/GThPs2KH23mv710= +github.com/VictoriaMetrics/fastcache v1.13.2 h1:2XTB49aLSuCex7e9P5rqrfQcMkzGjh5Vq3GMFa8YpCA= +github.com/VictoriaMetrics/fastcache v1.13.2/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU= +github.com/VictoriaMetrics/metrics v1.40.2 h1:OVSjKcQEx6JAwGeu8/KQm9Su5qJ72TMEW4xYn5vw3Ac= +github.com/VictoriaMetrics/metrics v1.40.2/go.mod h1:XE4uudAAIRaJE614Tl5HMrtoEU6+GDZO4QTnNSsZRuA= +github.com/VictoriaMetrics/metricsql v0.84.8 h1:5JXrvPJiYkYNqJVT7+hMZmpAwRHd3txBdlVIw4rJ1VM= +github.com/VictoriaMetrics/metricsql v0.84.8/go.mod h1:d4EisFO6ONP/HIGDYTAtwrejJBBeKGQYiRl095bS4QQ= +github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8= +github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= github.com/appleboy/gin-jwt/v2 v2.10.3 h1:KNcPC+XPRNpuoBh+j+rgs5bQxN+SwG/0tHbIqpRoBGc= github.com/appleboy/gin-jwt/v2 v2.10.3/go.mod h1:LDUaQ8mF2W6LyXIbd5wqlV2SFebuyYs4RDwqMNgpsp8= github.com/appleboy/gofight/v2 v2.1.2 h1:VOy3jow4vIK8BRQJoC/I9muxyYlJ2yb9ht2hZoS3rf4= @@ -9,6 +21,8 @@ github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1 github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= @@ -30,8 +44,8 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= @@ -58,9 +72,11 @@ github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXe github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -78,6 +94,8 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= @@ -87,8 +105,8 @@ github.com/knadh/koanf/providers/env v1.1.0 h1:U2VXPY0f+CsNDkvdsG8GcsnK4ah85WwWy github.com/knadh/koanf/providers/env v1.1.0/go.mod h1:QhHHHZ87h9JxJAn2czdEl6pdkNnDh/JS1Vtsyt65hTY= github.com/knadh/koanf/providers/file v1.2.0 h1:hrUJ6Y9YOA49aNu/RSYzOTFlqzXSCpmYIDXI7OJU6+U= github.com/knadh/koanf/providers/file v1.2.0/go.mod h1:bp1PM5f83Q+TOUu10J/0ApLBd9uIzg+n9UgthfY+nRA= -github.com/knadh/koanf/v2 v2.2.0 h1:FZFwd9bUjpb8DyCWARUBy5ovuhDs1lI87dOEn2K8UVU= -github.com/knadh/koanf/v2 v2.2.0/go.mod h1:PSFru3ufQgTsI7IF+95rf9s8XA1+aHxKuO/W+dPoHEY= +github.com/knadh/koanf/v2 v2.3.0 h1:Qg076dDRFHvqnKG97ZEsi9TAg2/nFTa9hCdcSa1lvlM= +github.com/knadh/koanf/v2 v2.3.0/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -119,8 +137,9 @@ github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/nezhahq/libdns-tencentcloud v0.0.0-20250501081622-bd293105845a h1:wCB9wDZi2JlTfMtE09s5VjSaQpk4EXegvja4wEzx2vk= github.com/nezhahq/libdns-tencentcloud v0.0.0-20250501081622-bd293105845a/go.mod h1:CUbNGv2k24auuhwa7MMVXl45fniBMm2eVi57FlWLcIs= github.com/ory/graceful v0.1.3 h1:FaeXcHZh168WzS+bqruqWEw/HgXWLdNv2nJ+fbhxbhc= @@ -138,8 +157,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= -github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -149,8 +168,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M= @@ -171,29 +190,39 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G8= +github.com/valyala/fastrand v1.1.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ= +github.com/valyala/gozstd v1.24.0 h1:M/9L3h7bVwbj2gZwrmuoaxzwVrmBUvos2jG9cZtuhlc= +github.com/valyala/gozstd v1.24.0/go.mod h1:y5Ew47GLlP37EkTB+B4s7r6A5rdaeB7ftbl9zoYiIPQ= +github.com/valyala/histogram v1.2.0 h1:wyYGAZZt3CpwUiIb9AU/Zbllg1llXyrtApRS815OLoQ= +github.com/valyala/histogram v1.2.0/go.mod h1:Hb4kBwb4UxsaNbbbh+RRz8ZR6pdodR57tzWUS3BUzXY= +github.com/valyala/quicktemplate v1.8.0 h1:zU0tjbIqTRgKQzFY1L42zq0qR3eh4WoQQdIdqCysW5k= +github.com/valyala/quicktemplate v1.8.0/go.mod h1:qIqW8/igXt8fdrUln5kOSb+KWMaJ4Y8QUsfd1k6L2jM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= -go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= -go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= -go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= -go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= -go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= -go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= -go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= -go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= -go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= golang.org/x/arch v0.16.0 h1:foMtLTdyOmIniqWCHjY6+JxuC54XP1fDwx4N0ASyW+U= golang.org/x/arch v0.16.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= -golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= -golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= +golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 h1:TQwNpfvNkxAVlItJf6Cr5JTsVZoC/Sj7K3OZv2Pc14A= +golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= @@ -203,8 +232,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= -golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= +golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= @@ -227,20 +256,22 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= -golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= -golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 h1:h6p3mQqrmT1XkHVTfzLdNz1u7IhINeZkz67/xTbOuWs= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM= -google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251007200510-49b9836ed3ff h1:A90eA31Wq6HOMIQlLfzFwzqGKBTuaVztYu/g8sn+8Zc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251007200510-49b9836ed3ff/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= +google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/model/config.go b/model/config.go index 380cce8..9e54931 100644 --- a/model/config.go +++ b/model/config.go @@ -70,6 +70,12 @@ type Config struct { // HTTPS 配置 HTTPS HTTPSConf `koanf:"https" json:"https"` + // TSDB 配置 + TSDB TSDBConf `koanf:"tsdb" json:"tsdb"` + + // 内存配置 + Memory MemoryConf `koanf:"memory" json:"memory"` + k *koanf.Koanf `json:"-"` filePath string `json:"-"` } @@ -81,6 +87,22 @@ type HTTPSConf struct { TLSKeyPath string `koanf:"tls_key_path" json:"tls_key_path,omitempty"` } +// TSDBConf TSDB 配置 +type TSDBConf struct { + DataPath string `koanf:"data_path" json:"data_path,omitempty"` + RetentionDays uint16 `koanf:"retention_days" json:"retention_days,omitempty"` + MinFreeDiskSpaceGB float64 `koanf:"min_free_disk_space_gb" json:"min_free_disk_space_gb,omitempty"` + MaxMemoryMB int64 `koanf:"max_memory_mb" json:"max_memory_mb,omitempty"` + WriteBufferSize int `koanf:"write_buffer_size" json:"write_buffer_size,omitempty"` + WriteBufferFlushInterval int `koanf:"write_buffer_flush_interval" json:"write_buffer_flush_interval,omitempty"` +} + +// MemoryConf 内存配置 +type MemoryConf struct { + // GoMemLimitMB Go 运行时内存限制(MB),0 表示不限制 + GoMemLimitMB int64 `koanf:"go_mem_limit_mb" json:"go_mem_limit_mb,omitempty"` +} + // Read 读取配置文件并应用 func (c *Config) Read(path string, frontendTemplates []FrontendTemplate) error { c.k = koanf.New(".") diff --git a/model/service.go b/model/service.go index 0be9f38..72153bc 100644 --- a/model/service.go +++ b/model/service.go @@ -53,6 +53,7 @@ type Service struct { Target string `json:"target"` SkipServersRaw string `json:"-"` Duration uint64 `json:"duration"` + DisplayIndex int `json:"display_index"` // 展示排序,越大越靠前 Notify bool `json:"notify,omitempty"` NotificationGroupID uint64 `json:"notification_group_id"` // 当前服务监控所属的通知组 ID Cover uint8 `json:"cover"` diff --git a/model/service_api.go b/model/service_api.go index ad877f2..b314fc1 100644 --- a/model/service_api.go +++ b/model/service_api.go @@ -7,6 +7,7 @@ type ServiceForm struct { Target string `json:"target,omitempty"` Type uint8 `json:"type,omitempty"` Cover uint8 `json:"cover,omitempty"` + DisplayIndex int `json:"display_index,omitempty" default:"0"` // 展示排序,越大越靠前 Notify bool `json:"notify,omitempty" validate:"optional"` Duration uint64 `json:"duration,omitempty"` MinLatency float32 `json:"min_latency,omitempty" default:"0.0"` @@ -26,7 +27,7 @@ type ServiceResponseItem struct { CurrentDown uint64 `json:"current_down"` TotalUp uint64 `json:"total_up"` TotalDown uint64 `json:"total_down"` - Delay *[30]float32 `json:"delay,omitempty"` + Delay *[30]float64 `json:"delay,omitempty"` Up *[30]uint64 `json:"up,omitempty"` Down *[30]uint64 `json:"down,omitempty"` } diff --git a/model/service_history.go b/model/service_history.go index a607c94..769ab53 100644 --- a/model/service_history.go +++ b/model/service_history.go @@ -10,7 +10,7 @@ type ServiceHistory struct { UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at,omitempty"` ServiceID uint64 `gorm:"index:idx_server_id_created_at_service_id_avg_delay" json:"service_id,omitempty"` ServerID uint64 `gorm:"index:idx_server_id_created_at_service_id_avg_delay" json:"server_id,omitempty"` - AvgDelay float32 `gorm:"index:idx_server_id_created_at_service_id_avg_delay" json:"avg_delay,omitempty"` // 平均延迟,毫秒 + AvgDelay float64 `gorm:"index:idx_server_id_created_at_service_id_avg_delay" json:"avg_delay,omitempty"` // 平均延迟,毫秒 Up uint64 `json:"up,omitempty"` // 检查状态良好计数 Down uint64 `json:"down,omitempty"` // 检查状态异常计数 Data string `json:"data,omitempty"` diff --git a/model/service_history_api.go b/model/service_history_api.go index 88337a7..4b003c0 100644 --- a/model/service_history_api.go +++ b/model/service_history_api.go @@ -1,10 +1,56 @@ package model +// ServiceInfos 服务监控信息(兼容旧API) type ServiceInfos struct { - ServiceID uint64 `json:"monitor_id"` - ServerID uint64 `json:"server_id"` - ServiceName string `json:"monitor_name"` - ServerName string `json:"server_name"` - CreatedAt []int64 `json:"created_at"` - AvgDelay []float32 `json:"avg_delay"` + ServiceID uint64 `json:"monitor_id"` + ServerID uint64 `json:"server_id"` + ServiceName string `json:"monitor_name"` + ServerName string `json:"server_name"` + DisplayIndex int `json:"display_index"` // 展示排序,越大越靠前 + CreatedAt []int64 `json:"created_at"` + AvgDelay []float64 `json:"avg_delay"` +} + +// DataPoint 数据点 +type DataPoint struct { + Timestamp int64 `json:"ts"` + Delay float64 `json:"delay"` + Status uint8 `json:"status"` // 1=成功, 0=失败 +} + +// ServiceHistorySummary 服务历史统计摘要 +type ServiceHistorySummary struct { + AvgDelay float64 `json:"avg_delay"` + UpPercent float32 `json:"up_percent"` + TotalUp uint64 `json:"total_up"` + TotalDown uint64 `json:"total_down"` + DataPoints []DataPoint `json:"data_points,omitempty"` +} + +// ServerServiceStats 某服务器对某服务的统计 +type ServerServiceStats struct { + ServerID uint64 `json:"server_id"` + ServerName string `json:"server_name,omitempty"` + Stats ServiceHistorySummary `json:"stats"` +} + +// ServiceHistoryResponse 服务历史查询响应 +type ServiceHistoryResponse struct { + ServiceID uint64 `json:"service_id"` + ServiceName string `json:"service_name,omitempty"` + Servers []ServerServiceStats `json:"servers"` +} + +// ServerMetricsDataPoint 服务器指标数据点 +type ServerMetricsDataPoint struct { + Timestamp int64 `json:"ts"` + Value float64 `json:"value"` +} + +// ServerMetricsResponse 服务器指标历史查询响应 +type ServerMetricsResponse struct { + ServerID uint64 `json:"server_id"` + ServerName string `json:"server_name,omitempty"` + Metric string `json:"metric"` + DataPoints []ServerMetricsDataPoint `json:"data_points"` } diff --git a/model/setting_api.go b/model/setting_api.go index 5cef3af..d9ef7d9 100644 --- a/model/setting_api.go +++ b/model/setting_api.go @@ -10,8 +10,8 @@ type SettingForm struct { InstallHost string `json:"install_host,omitempty" validate:"optional"` CustomCode string `json:"custom_code,omitempty" validate:"optional"` CustomCodeDashboard string `json:"custom_code_dashboard,omitempty" validate:"optional"` - WebRealIPHeader string `json:"web_real_ip_header,omitempty" validate:"optional"` // 前端真实IP - AgentRealIPHeader string `json:"agent_real_ip_header,omitempty" validate:"optional"` // Agent真实IP + WebRealIPHeader string `json:"web_real_ip_header,omitempty" validate:"optional"` // 前端真实IP + AgentRealIPHeader string `json:"agent_real_ip_header,omitempty" validate:"optional"` // Agent真实IP UserTemplate string `json:"user_template,omitempty" validate:"optional"` AgentTLS bool `json:"tls,omitempty" validate:"optional"` @@ -42,4 +42,5 @@ type SettingResponse struct { Version string `json:"version,omitempty"` FrontendTemplates []FrontendTemplate `json:"frontend_templates,omitempty"` + TSDBEnabled bool `json:"tsdb_enabled"` } diff --git a/pkg/tsdb/config.go b/pkg/tsdb/config.go new file mode 100644 index 0000000..1d5d267 --- /dev/null +++ b/pkg/tsdb/config.go @@ -0,0 +1,67 @@ +package tsdb + +import "time" + +// Config TSDB 配置选项 +type Config struct { + // DataPath 数据存储路径,为空则不启用 TSDB + DataPath string `koanf:"data_path" json:"data_path,omitempty"` + // RetentionDays 数据保留天数,默认 30 天 + RetentionDays uint16 `koanf:"retention_days" json:"retention_days,omitempty"` + // MinFreeDiskSpaceGB 最小磁盘剩余空间(GB),默认 1GB + // 当磁盘剩余空间低于此值时,TSDB 将停止接收新数据以防止磁盘耗尽 + MinFreeDiskSpaceGB float64 `koanf:"min_free_disk_space_gb" json:"min_free_disk_space_gb,omitempty"` + // MaxMemoryMB 最大内存使用量(MB),默认 256MB,用于限制 VictoriaMetrics 缓存 + MaxMemoryMB int64 `koanf:"max_memory_mb" json:"max_memory_mb,omitempty"` + // DedupInterval 去重间隔,默认 30 秒 + DedupInterval time.Duration `koanf:"dedup_interval" json:"dedup_interval,omitempty"` + // WriteBufferSize 写入缓冲区大小,默认 512,达到此数量后批量写入 + WriteBufferSize int `koanf:"write_buffer_size" json:"write_buffer_size,omitempty"` + // WriteBufferFlushInterval 写入缓冲区刷新间隔,默认 5 秒 + WriteBufferFlushInterval time.Duration `koanf:"write_buffer_flush_interval" json:"write_buffer_flush_interval,omitempty"` +} + +// DefaultConfig 返回默认配置(不设置 DataPath,需要显式配置才启用) +func DefaultConfig() *Config { + return &Config{ + DataPath: "", + RetentionDays: 30, + MinFreeDiskSpaceGB: 1, + MaxMemoryMB: 256, + DedupInterval: 30 * time.Second, + WriteBufferSize: 512, + WriteBufferFlushInterval: 5 * time.Second, + } +} + +// Validate 验证配置有效性并填充默认值 +func (c *Config) Validate() { + if c.RetentionDays == 0 { + c.RetentionDays = 30 + } + if c.MinFreeDiskSpaceGB <= 0 { + c.MinFreeDiskSpaceGB = 1 + } + if c.MaxMemoryMB <= 0 { + c.MaxMemoryMB = 256 + } + if c.DedupInterval <= 0 { + c.DedupInterval = 30 * time.Second + } + if c.WriteBufferSize <= 0 { + c.WriteBufferSize = 512 + } + if c.WriteBufferFlushInterval <= 0 { + c.WriteBufferFlushInterval = 5 * time.Second + } +} + +// Enabled 检查是否启用 TSDB +func (c *Config) Enabled() bool { + return c.DataPath != "" +} + +// MinFreeDiskSpaceBytes 返回最小磁盘剩余空间(字节) +func (c *Config) MinFreeDiskSpaceBytes() int64 { + return int64(c.MinFreeDiskSpaceGB * 1024 * 1024 * 1024) +} diff --git a/pkg/tsdb/maintenance.go b/pkg/tsdb/maintenance.go new file mode 100644 index 0000000..fe9c73e --- /dev/null +++ b/pkg/tsdb/maintenance.go @@ -0,0 +1,17 @@ +package tsdb + +import ( + "log" +) + +func (db *TSDB) Maintenance() { + db.mu.RLock() + defer db.mu.RUnlock() + if db.closed { + return + } + + log.Println("NEZHA>> TSDB starting maintenance (flush)...") + db.storage.DebugFlush() + log.Println("NEZHA>> TSDB maintenance completed") +} diff --git a/pkg/tsdb/query.go b/pkg/tsdb/query.go new file mode 100644 index 0000000..e5b7379 --- /dev/null +++ b/pkg/tsdb/query.go @@ -0,0 +1,664 @@ +package tsdb + +import ( + "fmt" + "log" + "sort" + "strconv" + "time" + + "github.com/VictoriaMetrics/VictoriaMetrics/lib/storage" + + "github.com/nezhahq/nezha/model" +) + +// QueryPeriod 查询时间段 +type QueryPeriod string + +const ( + Period1Day QueryPeriod = "1d" + Period7Days QueryPeriod = "7d" + Period30Days QueryPeriod = "30d" +) + +// ParseQueryPeriod 解析查询时间段 +func ParseQueryPeriod(s string) (QueryPeriod, error) { + switch s { + case "1d", "": + return Period1Day, nil + case "7d": + return Period7Days, nil + case "30d": + return Period30Days, nil + default: + return "", fmt.Errorf("invalid period: %s, expected 1d, 7d, or 30d", s) + } +} + +// Duration 返回时间段的时长 +func (p QueryPeriod) Duration() time.Duration { + switch p { + case Period7Days: + return 7 * 24 * time.Hour + case Period30Days: + return 30 * 24 * time.Hour + default: + return 24 * time.Hour + } +} + +// DownsampleInterval 返回降采样间隔 +// 1d: 5分钟一个点 (288个点) +// 7d: 30分钟一个点 (336个点) +// 30d: 2小时一个点 (360个点) +func (p QueryPeriod) DownsampleInterval() time.Duration { + switch p { + case Period7Days: + return 30 * time.Minute + case Period30Days: + return 2 * time.Hour + default: + return 5 * time.Minute + } +} + +// Type aliases for model types used in tsdb package +type ( + DataPoint = model.DataPoint + ServiceHistorySummary = model.ServiceHistorySummary + ServerServiceStats = model.ServerServiceStats + ServiceHistoryResult = model.ServiceHistoryResponse + MetricDataPoint = model.ServerMetricsDataPoint +) + +type rawDataPoint struct { + timestamp int64 + value float64 + status float64 + hasDelay bool + hasStatus bool +} + +func (db *TSDB) QueryServiceHistory(serviceID uint64, period QueryPeriod) (*ServiceHistoryResult, error) { + db.mu.RLock() + defer db.mu.RUnlock() + if db.closed { + return nil, fmt.Errorf("TSDB is closed") + } + + now := time.Now() + tr := storage.TimeRange{ + MinTimestamp: now.Add(-period.Duration()).UnixMilli(), + MaxTimestamp: now.UnixMilli(), + } + + serviceIDStr := strconv.FormatUint(serviceID, 10) + + delayData, err := db.queryMetricByServiceID(MetricServiceDelay, serviceIDStr, tr) + if err != nil { + return nil, fmt.Errorf("failed to query delay data: %w", err) + } + + statusData, err := db.queryMetricByServiceID(MetricServiceStatus, serviceIDStr, tr) + if err != nil { + return nil, fmt.Errorf("failed to query status data: %w", err) + } + + result := &ServiceHistoryResult{ + ServiceID: serviceID, + Servers: make([]ServerServiceStats, 0), + } + + serverDataMap := make(map[uint64]map[int64]*rawDataPoint) + + for serverID, points := range delayData { + if serverDataMap[serverID] == nil { + serverDataMap[serverID] = make(map[int64]*rawDataPoint) + } + for _, p := range points { + serverDataMap[serverID][p.timestamp] = &rawDataPoint{ + timestamp: p.timestamp, + value: p.value, + hasDelay: true, + } + } + } + + for serverID, points := range statusData { + if serverDataMap[serverID] == nil { + serverDataMap[serverID] = make(map[int64]*rawDataPoint) + } + for _, p := range points { + if existing, ok := serverDataMap[serverID][p.timestamp]; ok { + existing.status = p.value + existing.hasStatus = true + } else { + serverDataMap[serverID][p.timestamp] = &rawDataPoint{ + timestamp: p.timestamp, + status: p.value, + hasStatus: true, + } + } + } + } + + for serverID, pointsMap := range serverDataMap { + points := make([]rawDataPoint, 0, len(pointsMap)) + for _, p := range pointsMap { + points = append(points, *p) + } + stats := calculateStats(points, period.DownsampleInterval()) + result.Servers = append(result.Servers, ServerServiceStats{ + ServerID: serverID, + Stats: stats, + }) + } + + sort.Slice(result.Servers, func(i, j int) bool { + return result.Servers[i].ServerID < result.Servers[j].ServerID + }) + + return result, nil +} + +type DailyServiceStats struct { + Up uint64 + Down uint64 + Delay float64 +} + +func (db *TSDB) QueryServiceDailyStats(serviceID uint64, today time.Time, days int) ([]DailyServiceStats, error) { + db.mu.RLock() + defer db.mu.RUnlock() + if db.closed { + return nil, fmt.Errorf("TSDB is closed") + } + + stats := make([]DailyServiceStats, days) + serviceIDStr := strconv.FormatUint(serviceID, 10) + + start := today.AddDate(0, 0, -(days - 1)) + tr := storage.TimeRange{ + MinTimestamp: start.UnixMilli(), + MaxTimestamp: today.UnixMilli(), + } + + statusData, err := db.queryMetricByServiceID(MetricServiceStatus, serviceIDStr, tr) + if err != nil { + return nil, err + } + delayData, err := db.queryMetricByServiceID(MetricServiceDelay, serviceIDStr, tr) + if err != nil { + return nil, err + } + + for _, points := range statusData { + for _, p := range points { + ts := time.UnixMilli(p.timestamp) + dayIndex := (days - 1) - int(today.Sub(ts).Hours())/24 + if dayIndex < 0 || dayIndex >= days { + continue + } + if p.value >= 0.5 { + stats[dayIndex].Up++ + } else { + stats[dayIndex].Down++ + } + } + } + + delayCount := make([]int, days) + for _, points := range delayData { + for _, p := range points { + ts := time.UnixMilli(p.timestamp) + dayIndex := (days - 1) - int(today.Sub(ts).Hours())/24 + if dayIndex < 0 || dayIndex >= days { + continue + } + stats[dayIndex].Delay = (stats[dayIndex].Delay*float64(delayCount[dayIndex]) + p.value) / float64(delayCount[dayIndex]+1) + delayCount[dayIndex]++ + } + } + + return stats, nil +} + +type metricPoint struct { + timestamp int64 + value float64 +} + +func (db *TSDB) queryMetricByServiceID(metric MetricType, serviceID string, tr storage.TimeRange) (map[uint64][]metricPoint, error) { + tfs := storage.NewTagFilters() + if err := tfs.Add(nil, []byte(metric), false, false); err != nil { + return nil, err + } + if err := tfs.Add([]byte("service_id"), []byte(serviceID), false, false); err != nil { + return nil, err + } + + deadline := uint64(time.Now().Add(30 * time.Second).Unix()) + + var search storage.Search + search.Init(nil, db.storage, []*storage.TagFilters{tfs}, tr, 100000, deadline) + defer search.MustClose() + + result := make(map[uint64][]metricPoint) + var timestamps []int64 + var values []float64 + + for search.NextMetricBlock() { + mbr := search.MetricBlockRef + var block storage.Block + mbr.BlockRef.MustReadBlock(&block) + + mn := storage.GetMetricName() + if err := mn.Unmarshal(mbr.MetricName); err != nil { + log.Printf("NEZHA>> TSDB: failed to unmarshal metric name: %v", err) + storage.PutMetricName(mn) + continue + } + + serverIDBytes := mn.GetTagValue("server_id") + if len(serverIDBytes) == 0 { + storage.PutMetricName(mn) + continue + } + + serverID, err := strconv.ParseUint(string(serverIDBytes), 10, 64) + if err != nil { + log.Printf("NEZHA>> TSDB: failed to parse server_id %q: %v", string(serverIDBytes), err) + storage.PutMetricName(mn) + continue + } + storage.PutMetricName(mn) + + if err := block.UnmarshalData(); err != nil { + log.Printf("NEZHA>> TSDB: failed to unmarshal block data: %v", err) + continue + } + + timestamps = timestamps[:0] + values = values[:0] + timestamps, values = block.AppendRowsWithTimeRangeFilter(timestamps, values, tr) + + for i := range timestamps { + result[serverID] = append(result[serverID], metricPoint{ + timestamp: timestamps[i], + value: values[i], + }) + } + } + + if err := search.Error(); err != nil { + return nil, err + } + + return result, nil +} + +func calculateStats(points []rawDataPoint, downsampleInterval time.Duration) ServiceHistorySummary { + if len(points) == 0 { + return ServiceHistorySummary{} + } + + sort.Slice(points, func(i, j int) bool { + return points[i].timestamp < points[j].timestamp + }) + + var totalDelay float64 + var delayCount int + var totalUp, totalDown uint64 + + for _, p := range points { + if p.hasDelay { + totalDelay += p.value + delayCount++ + } + if p.hasStatus { + if p.status >= 0.5 { + totalUp++ + } else { + totalDown++ + } + } + } + + summary := ServiceHistorySummary{ + TotalUp: totalUp, + TotalDown: totalDown, + } + + if delayCount > 0 { + summary.AvgDelay = totalDelay / float64(delayCount) + } + + if totalUp+totalDown > 0 { + summary.UpPercent = float32(totalUp) / float32(totalUp+totalDown) * 100 + } + + summary.DataPoints = downsample(points, downsampleInterval) + + return summary +} + +func downsample(points []rawDataPoint, interval time.Duration) []DataPoint { + if len(points) == 0 { + return nil + } + + intervalMs := interval.Milliseconds() + result := make([]DataPoint, 0) + + // points 已排序,线性扫描分桶 + bucketStart := (points[0].timestamp / intervalMs) * intervalMs + var totalDelay float64 + var delayCount, upCount, statusCount int + + flushBucket := func() { + var avgDelay float64 + if delayCount > 0 { + avgDelay = totalDelay / float64(delayCount) + } + var status uint8 + if statusCount > 0 && upCount > statusCount/2 { + status = 1 + } + result = append(result, DataPoint{ + Timestamp: bucketStart, + Delay: avgDelay, + Status: status, + }) + } + + for _, p := range points { + key := (p.timestamp / intervalMs) * intervalMs + if key != bucketStart { + flushBucket() + bucketStart = key + totalDelay = 0 + delayCount = 0 + upCount = 0 + statusCount = 0 + } + if p.hasDelay { + totalDelay += p.value + delayCount++ + } + if p.hasStatus { + statusCount++ + if p.status >= 0.5 { + upCount++ + } + } + } + flushBucket() + + return result +} + +func downsampleMetrics(points []rawDataPoint, interval time.Duration, useLastValue bool) []MetricDataPoint { + if len(points) == 0 { + return nil + } + + sort.Slice(points, func(i, j int) bool { + return points[i].timestamp < points[j].timestamp + }) + + intervalMs := interval.Milliseconds() + result := make([]MetricDataPoint, 0) + + bucketStart := (points[0].timestamp / intervalMs) * intervalMs + var total float64 + var count int + var last rawDataPoint + + flushBucket := func() { + var value float64 + if useLastValue { + value = last.value + } else if count > 0 { + value = total / float64(count) + } + result = append(result, MetricDataPoint{ + Timestamp: bucketStart, + Value: value, + }) + } + + for _, p := range points { + key := (p.timestamp / intervalMs) * intervalMs + if key != bucketStart { + flushBucket() + bucketStart = key + total = 0 + count = 0 + } + total += p.value + count++ + last = p + } + flushBucket() + + return result +} + +// isCumulativeMetric 判断指标是否为累积型(单调递增) +func isCumulativeMetric(metric MetricType) bool { + switch metric { + case MetricServerNetInTransfer, MetricServerNetOutTransfer, MetricServerUptime: + return true + default: + return false + } +} + +func (db *TSDB) QueryServerMetrics(serverID uint64, metric MetricType, period QueryPeriod) ([]MetricDataPoint, error) { + db.mu.RLock() + defer db.mu.RUnlock() + if db.closed { + return nil, fmt.Errorf("TSDB is closed") + } + + now := time.Now() + tr := storage.TimeRange{ + MinTimestamp: now.Add(-period.Duration()).UnixMilli(), + MaxTimestamp: now.UnixMilli(), + } + + serverIDStr := strconv.FormatUint(serverID, 10) + + tfs := storage.NewTagFilters() + if err := tfs.Add(nil, []byte(metric), false, false); err != nil { + return nil, err + } + if err := tfs.Add([]byte("server_id"), []byte(serverIDStr), false, false); err != nil { + return nil, err + } + + deadline := uint64(time.Now().Add(30 * time.Second).Unix()) + + var search storage.Search + search.Init(nil, db.storage, []*storage.TagFilters{tfs}, tr, 100000, deadline) + defer search.MustClose() + + var points []rawDataPoint + var timestamps []int64 + var values []float64 + + for search.NextMetricBlock() { + mbr := search.MetricBlockRef + var block storage.Block + mbr.BlockRef.MustReadBlock(&block) + + if err := block.UnmarshalData(); err != nil { + log.Printf("NEZHA>> TSDB: failed to unmarshal block data: %v", err) + continue + } + + timestamps = timestamps[:0] + values = values[:0] + timestamps, values = block.AppendRowsWithTimeRangeFilter(timestamps, values, tr) + + for i := range timestamps { + points = append(points, rawDataPoint{ + timestamp: timestamps[i], + value: values[i], + }) + } + } + + if err := search.Error(); err != nil { + return nil, err + } + + return downsampleMetrics(points, period.DownsampleInterval(), isCumulativeMetric(metric)), nil +} + +func (db *TSDB) QueryServiceHistoryByServerID(serverID uint64, period QueryPeriod) (map[uint64]*ServiceHistoryResult, error) { + db.mu.RLock() + defer db.mu.RUnlock() + if db.closed { + return nil, fmt.Errorf("TSDB is closed") + } + + now := time.Now() + tr := storage.TimeRange{ + MinTimestamp: now.Add(-period.Duration()).UnixMilli(), + MaxTimestamp: now.UnixMilli(), + } + + serverIDStr := strconv.FormatUint(serverID, 10) + + delayData, err := db.queryMetricByServerID(MetricServiceDelay, serverIDStr, tr) + if err != nil { + return nil, fmt.Errorf("failed to query delay data: %w", err) + } + + statusData, err := db.queryMetricByServerID(MetricServiceStatus, serverIDStr, tr) + if err != nil { + return nil, fmt.Errorf("failed to query status data: %w", err) + } + + serviceDataMap := make(map[uint64]map[int64]*rawDataPoint) + + for serviceID, points := range delayData { + if serviceDataMap[serviceID] == nil { + serviceDataMap[serviceID] = make(map[int64]*rawDataPoint) + } + for _, p := range points { + serviceDataMap[serviceID][p.timestamp] = &rawDataPoint{ + timestamp: p.timestamp, + value: p.value, + hasDelay: true, + } + } + } + + for serviceID, points := range statusData { + if serviceDataMap[serviceID] == nil { + serviceDataMap[serviceID] = make(map[int64]*rawDataPoint) + } + for _, p := range points { + if existing, ok := serviceDataMap[serviceID][p.timestamp]; ok { + existing.status = p.value + existing.hasStatus = true + } else { + serviceDataMap[serviceID][p.timestamp] = &rawDataPoint{ + timestamp: p.timestamp, + status: p.value, + hasStatus: true, + } + } + } + } + + results := make(map[uint64]*ServiceHistoryResult) + + for serviceID, pointsMap := range serviceDataMap { + points := make([]rawDataPoint, 0, len(pointsMap)) + for _, p := range pointsMap { + points = append(points, *p) + } + stats := calculateStats(points, period.DownsampleInterval()) + results[serviceID] = &ServiceHistoryResult{ + ServiceID: serviceID, + Servers: []ServerServiceStats{{ + ServerID: serverID, + Stats: stats, + }}, + } + } + + return results, nil +} + +func (db *TSDB) queryMetricByServerID(metric MetricType, serverID string, tr storage.TimeRange) (map[uint64][]metricPoint, error) { + tfs := storage.NewTagFilters() + if err := tfs.Add(nil, []byte(metric), false, false); err != nil { + return nil, err + } + if err := tfs.Add([]byte("server_id"), []byte(serverID), false, false); err != nil { + return nil, err + } + + deadline := uint64(time.Now().Add(30 * time.Second).Unix()) + + var search storage.Search + search.Init(nil, db.storage, []*storage.TagFilters{tfs}, tr, 100000, deadline) + defer search.MustClose() + + result := make(map[uint64][]metricPoint) + var timestamps []int64 + var values []float64 + + for search.NextMetricBlock() { + mbr := search.MetricBlockRef + var block storage.Block + mbr.BlockRef.MustReadBlock(&block) + + mn := storage.GetMetricName() + if err := mn.Unmarshal(mbr.MetricName); err != nil { + log.Printf("NEZHA>> TSDB: failed to unmarshal metric name: %v", err) + storage.PutMetricName(mn) + continue + } + + serviceIDBytes := mn.GetTagValue("service_id") + if len(serviceIDBytes) == 0 { + storage.PutMetricName(mn) + continue + } + + serviceID, err := strconv.ParseUint(string(serviceIDBytes), 10, 64) + if err != nil { + log.Printf("NEZHA>> TSDB: failed to parse service_id %q: %v", string(serviceIDBytes), err) + storage.PutMetricName(mn) + continue + } + storage.PutMetricName(mn) + + if err := block.UnmarshalData(); err != nil { + log.Printf("NEZHA>> TSDB: failed to unmarshal block data: %v", err) + continue + } + + timestamps = timestamps[:0] + values = values[:0] + timestamps, values = block.AppendRowsWithTimeRangeFilter(timestamps, values, tr) + + for i := range timestamps { + result[serviceID] = append(result[serviceID], metricPoint{ + timestamp: timestamps[i], + value: values[i], + }) + } + } + + if err := search.Error(); err != nil { + return nil, err + } + + return result, nil +} diff --git a/pkg/tsdb/tsdb.go b/pkg/tsdb/tsdb.go new file mode 100644 index 0000000..d3627b6 --- /dev/null +++ b/pkg/tsdb/tsdb.go @@ -0,0 +1,117 @@ +package tsdb + +import ( + "fmt" + "log" + "path/filepath" + "sync" + "time" + + "github.com/VictoriaMetrics/VictoriaMetrics/lib/storage" +) + +// TSDB 封装 VictoriaMetrics 存储 +type TSDB struct { + storage *storage.Storage + config *Config + mu sync.RWMutex + closed bool + + writer *bufferedWriter +} + +// InitGlobalSettings 初始化 VictoriaMetrics 包级别的全局设置。 +// 这些设置是进程级别的,应在 Open() 之前调用且只调用一次。 +func InitGlobalSettings(config *Config) { + memBytes := int(config.MaxMemoryMB * 1024 * 1024) + storage.SetTSIDCacheSize(memBytes * 35 / 100) + storage.SetMetricNameCacheSize(memBytes * 10 / 100) + storage.SetTagFiltersCacheSize(memBytes * 5 / 100) + storage.SetMetadataStorageSize(memBytes * 1 / 100) + + storage.SetDedupInterval(config.DedupInterval) + storage.SetFreeDiskSpaceLimit(config.MinFreeDiskSpaceBytes()) + storage.SetDataFlushInterval(5 * time.Second) +} + +// Open 打开或创建 TSDB 存储 +func Open(config *Config) (*TSDB, error) { + if config == nil { + config = DefaultConfig() + } + + config.Validate() + + dataPath := config.DataPath + if !filepath.IsAbs(dataPath) { + absPath, err := filepath.Abs(dataPath) + if err != nil { + return nil, fmt.Errorf("failed to get absolute path: %w", err) + } + dataPath = absPath + } + + InitGlobalSettings(config) + + opts := storage.OpenOptions{ + Retention: time.Duration(config.RetentionDays) * 24 * time.Hour, + } + + stor := storage.MustOpenStorage(dataPath, opts) + + db := &TSDB{ + storage: stor, + config: config, + } + + db.writer = newBufferedWriter(db, config.WriteBufferSize, config.WriteBufferFlushInterval) + + log.Printf("NEZHA>> TSDB opened at %s, retention: %d days, min free disk: %.1f GB, max memory: %d MB", + dataPath, config.RetentionDays, config.MinFreeDiskSpaceGB, config.MaxMemoryMB) + + return db, nil +} + +// Close 关闭 TSDB 存储 +func (db *TSDB) Close() error { + db.mu.Lock() + defer db.mu.Unlock() + + if db.closed { + return nil + } + + if db.writer != nil { + db.writer.stop() + } + + db.storage.MustClose() + db.closed = true + log.Println("NEZHA>> TSDB closed") + return nil +} + +// Storage 返回底层存储对象(用于高级查询) +func (db *TSDB) Storage() *storage.Storage { + return db.storage +} + +// Config 返回配置 +func (db *TSDB) Config() *Config { + return db.config +} + +// IsClosed 检查是否已关闭 +func (db *TSDB) IsClosed() bool { + db.mu.RLock() + defer db.mu.RUnlock() + return db.closed +} + +// Flush 强制刷盘(主要用于测试) +func (db *TSDB) Flush() { + if db.writer != nil { + db.writer.flush() + } + db.storage.DebugFlush() +} diff --git a/pkg/tsdb/tsdb_test.go b/pkg/tsdb/tsdb_test.go new file mode 100644 index 0000000..fe2ab4d --- /dev/null +++ b/pkg/tsdb/tsdb_test.go @@ -0,0 +1,622 @@ +package tsdb + +import ( + "os" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestConfig_Defaults(t *testing.T) { + config := DefaultConfig() + + assert.Equal(t, "", config.DataPath) // 默认为空,不启用 TSDB + assert.Equal(t, uint16(30), config.RetentionDays) + assert.Equal(t, float64(1), config.MinFreeDiskSpaceGB) + assert.Equal(t, 30*time.Second, config.DedupInterval) + assert.False(t, config.Enabled()) +} + +func TestConfig_Enabled(t *testing.T) { + config := &Config{DataPath: ""} + assert.False(t, config.Enabled()) + + config.DataPath = "data/tsdb" + assert.True(t, config.Enabled()) +} + +func TestConfig_MinFreeDiskSpaceBytes(t *testing.T) { + config := &Config{MinFreeDiskSpaceGB: 5} + expected := int64(5 * 1024 * 1024 * 1024) + assert.Equal(t, expected, config.MinFreeDiskSpaceBytes()) +} + +func TestTSDB_OpenClose(t *testing.T) { + tempDir, err := os.MkdirTemp("", "tsdb_test") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + config := &Config{ + DataPath: filepath.Join(tempDir, "tsdb"), + RetentionDays: 1, + MinFreeDiskSpaceGB: 1, + DedupInterval: time.Second, + } + + db, err := Open(config) + require.NoError(t, err) + require.NotNil(t, db) + + assert.False(t, db.IsClosed()) + assert.NotNil(t, db.Storage()) + assert.Equal(t, config, db.Config()) + + err = db.Close() + require.NoError(t, err) + assert.True(t, db.IsClosed()) + + // 重复关闭应该安全 + err = db.Close() + require.NoError(t, err) +} + +func TestTSDB_WriteServerMetrics(t *testing.T) { + tempDir, err := os.MkdirTemp("", "tsdb_test") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + config := &Config{ + DataPath: filepath.Join(tempDir, "tsdb"), + RetentionDays: 1, + MinFreeDiskSpaceGB: 1, + DedupInterval: time.Second, + } + + db, err := Open(config) + require.NoError(t, err) + defer db.Close() + + metrics := &ServerMetrics{ + ServerID: 1, + Timestamp: time.Now(), + CPU: 50.5, + MemUsed: 1024 * 1024 * 1024, + SwapUsed: 512 * 1024 * 1024, + DiskUsed: 10 * 1024 * 1024 * 1024, + NetInSpeed: 1000000, + NetOutSpeed: 500000, + NetInTransfer: 1000000000, + NetOutTransfer: 500000000, + Load1: 1.5, + Load5: 1.2, + Load15: 1.0, + TCPConnCount: 100, + UDPConnCount: 50, + ProcessCount: 200, + Temperature: 65.5, + Uptime: 86400, + GPU: 30.0, + } + + err = db.WriteServerMetrics(metrics) + require.NoError(t, err) + + // 强制刷盘 + db.Flush() +} + +func TestTSDB_WriteServiceMetrics(t *testing.T) { + tempDir, err := os.MkdirTemp("", "tsdb_test") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + config := &Config{ + DataPath: filepath.Join(tempDir, "tsdb"), + RetentionDays: 1, + MinFreeDiskSpaceGB: 1, + DedupInterval: time.Second, + } + + db, err := Open(config) + require.NoError(t, err) + defer db.Close() + + metrics := &ServiceMetrics{ + ServiceID: 1, + ServerID: 1, + Timestamp: time.Now(), + Delay: 45.5, + Successful: true, + } + + err = db.WriteServiceMetrics(metrics) + require.NoError(t, err) + + // 测试失败状态 + metrics2 := &ServiceMetrics{ + ServiceID: 1, + ServerID: 2, + Timestamp: time.Now(), + Delay: 0, + Successful: false, + } + + err = db.WriteServiceMetrics(metrics2) + require.NoError(t, err) + + // 强制刷盘 + db.Flush() +} + +func TestTSDB_WriteBatchMetrics(t *testing.T) { + tempDir, err := os.MkdirTemp("", "tsdb_test") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + config := &Config{ + DataPath: filepath.Join(tempDir, "tsdb"), + RetentionDays: 1, + MinFreeDiskSpaceGB: 1, + DedupInterval: time.Second, + } + + db, err := Open(config) + require.NoError(t, err) + defer db.Close() + + // 批量写入服务器指标 + serverMetrics := []*ServerMetrics{ + {ServerID: 1, Timestamp: time.Now(), CPU: 10.0}, + {ServerID: 2, Timestamp: time.Now(), CPU: 20.0}, + {ServerID: 3, Timestamp: time.Now(), CPU: 30.0}, + } + + err = db.WriteBatchServerMetrics(serverMetrics) + require.NoError(t, err) + + // 批量写入服务指标 + serviceMetrics := []*ServiceMetrics{ + {ServiceID: 1, ServerID: 1, Timestamp: time.Now(), Delay: 10.0, Successful: true}, + {ServiceID: 1, ServerID: 2, Timestamp: time.Now(), Delay: 20.0, Successful: true}, + {ServiceID: 2, ServerID: 1, Timestamp: time.Now(), Delay: 15.0, Successful: false}, + } + + err = db.WriteBatchServiceMetrics(serviceMetrics) + require.NoError(t, err) + + db.Flush() +} + +func TestTSDB_WriteToClosedDB(t *testing.T) { + tempDir, err := os.MkdirTemp("", "tsdb_test") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + config := &Config{ + DataPath: filepath.Join(tempDir, "tsdb"), + RetentionDays: 1, + MinFreeDiskSpaceGB: 1, + DedupInterval: time.Second, + } + + db, err := Open(config) + require.NoError(t, err) + + db.Close() + + // 写入已关闭的数据库应该返回错误 + err = db.WriteServerMetrics(&ServerMetrics{ServerID: 1, Timestamp: time.Now()}) + assert.Error(t, err) + + err = db.WriteServiceMetrics(&ServiceMetrics{ServiceID: 1, ServerID: 1, Timestamp: time.Now()}) + assert.Error(t, err) +} + +func TestQueryPeriod_Parse(t *testing.T) { + tests := []struct { + input string + expected QueryPeriod + hasError bool + }{ + {"1d", Period1Day, false}, + {"7d", Period7Days, false}, + {"30d", Period30Days, false}, + {"", Period1Day, false}, + {"invalid", "", true}, + {"1w", "", true}, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + period, err := ParseQueryPeriod(tt.input) + if tt.hasError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected, period) + } + }) + } +} + +func TestQueryPeriod_Duration(t *testing.T) { + assert.Equal(t, 24*time.Hour, Period1Day.Duration()) + assert.Equal(t, 7*24*time.Hour, Period7Days.Duration()) + assert.Equal(t, 30*24*time.Hour, Period30Days.Duration()) +} + +func TestQueryPeriod_DownsampleInterval(t *testing.T) { + assert.Equal(t, 5*time.Minute, Period1Day.DownsampleInterval()) + assert.Equal(t, 30*time.Minute, Period7Days.DownsampleInterval()) + assert.Equal(t, 2*time.Hour, Period30Days.DownsampleInterval()) +} + +func TestTSDB_QueryServiceHistory(t *testing.T) { + tempDir, err := os.MkdirTemp("", "tsdb_test") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + config := &Config{ + DataPath: filepath.Join(tempDir, "tsdb"), + RetentionDays: 1, + MinFreeDiskSpaceGB: 1, + DedupInterval: time.Second, + } + + db, err := Open(config) + require.NoError(t, err) + defer db.Close() + + // 写入测试数据 + now := time.Now() + serviceID := uint64(100) + serverID1 := uint64(1) + serverID2 := uint64(2) + + // 写入多条服务监控数据 + for i := 0; i < 10; i++ { + ts := now.Add(-time.Duration(i) * time.Minute) + + // 服务器1的数据:成功 + err := db.WriteServiceMetrics(&ServiceMetrics{ + ServiceID: serviceID, + ServerID: serverID1, + Timestamp: ts, + Delay: float64(10 + i), + Successful: true, + }) + require.NoError(t, err) + + // 服务器2的数据:部分失败 + err = db.WriteServiceMetrics(&ServiceMetrics{ + ServiceID: serviceID, + ServerID: serverID2, + Timestamp: ts, + Delay: float64(20 + i), + Successful: i%2 == 0, // 偶数成功,奇数失败 + }) + require.NoError(t, err) + } + + // 强制刷盘确保数据可见 + db.Flush() + + // 查询服务历史 + result, err := db.QueryServiceHistory(serviceID, Period1Day) + require.NoError(t, err) + require.NotNil(t, result) + + assert.Equal(t, serviceID, result.ServiceID) + require.Len(t, result.Servers, 2, "expected 2 servers") + + // 验证服务器统计 + for _, server := range result.Servers { + if server.ServerID == serverID1 { + // 服务器1全部成功 + assert.Equal(t, uint64(10), server.Stats.TotalUp) + assert.Equal(t, uint64(0), server.Stats.TotalDown) + assert.Equal(t, float32(100), server.Stats.UpPercent) + } else if server.ServerID == serverID2 { + // 服务器2一半成功 + assert.Equal(t, uint64(5), server.Stats.TotalUp) + assert.Equal(t, uint64(5), server.Stats.TotalDown) + assert.Equal(t, float32(50), server.Stats.UpPercent) + } + } +} + +func TestTSDB_QueryServerMetrics(t *testing.T) { + tempDir, err := os.MkdirTemp("", "tsdb_test") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + config := &Config{ + DataPath: filepath.Join(tempDir, "tsdb"), + RetentionDays: 1, + MinFreeDiskSpaceGB: 1, + DedupInterval: time.Second, + } + + db, err := Open(config) + require.NoError(t, err) + defer db.Close() + + // 写入测试数据 + now := time.Now() + serverID := uint64(1) + + for i := 0; i < 10; i++ { + ts := now.Add(-time.Duration(i) * time.Minute) + err := db.WriteServerMetrics(&ServerMetrics{ + ServerID: serverID, + Timestamp: ts, + CPU: float64(10 + i*5), + }) + require.NoError(t, err) + } + + // 强制刷盘确保数据可见 + db.Flush() + + // 查询服务器指标 + result, err := db.QueryServerMetrics(serverID, MetricServerCPU, Period1Day) + require.NoError(t, err) + require.NotEmpty(t, result, "expected data points") +} + +func TestTSDB_QueryEmptyResult(t *testing.T) { + tempDir, err := os.MkdirTemp("", "tsdb_test") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + config := &Config{ + DataPath: filepath.Join(tempDir, "tsdb"), + RetentionDays: 1, + MinFreeDiskSpaceGB: 1, + DedupInterval: time.Second, + } + + db, err := Open(config) + require.NoError(t, err) + defer db.Close() + + // 查询不存在的服务历史 + result, err := db.QueryServiceHistory(9999, Period1Day) + require.NoError(t, err) + require.NotNil(t, result) + assert.Empty(t, result.Servers) + + // 查询不存在的服务器指标 + serverResult, err := db.QueryServerMetrics(9999, MetricServerCPU, Period1Day) + require.NoError(t, err) + assert.Empty(t, serverResult) +} + +func TestTSDB_QueryClosedDB(t *testing.T) { + tempDir, err := os.MkdirTemp("", "tsdb_test") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + config := &Config{ + DataPath: filepath.Join(tempDir, "tsdb"), + RetentionDays: 1, + MinFreeDiskSpaceGB: 1, + DedupInterval: time.Second, + } + + db, err := Open(config) + require.NoError(t, err) + db.Close() + + // 查询已关闭的数据库应该返回错误 + _, err = db.QueryServiceHistory(1, Period1Day) + assert.Error(t, err) + + _, err = db.QueryServerMetrics(1, MetricServerCPU, Period1Day) + assert.Error(t, err) +} + +func TestDownsample(t *testing.T) { + points := []rawDataPoint{ + {timestamp: 0, value: 10, status: 1, hasDelay: true, hasStatus: true}, + {timestamp: 1000, value: 20, status: 1, hasDelay: true, hasStatus: true}, + {timestamp: 2000, value: 30, status: 0, hasDelay: true, hasStatus: true}, + {timestamp: 3000, value: 40, status: 1, hasDelay: true, hasStatus: true}, + {timestamp: 4000, value: 50, status: 1, hasDelay: true, hasStatus: true}, + } + + result := downsample(points, 2*time.Second) + + assert.Len(t, result, 3) + + for i := 1; i < len(result); i++ { + assert.Greater(t, result[i].Timestamp, result[i-1].Timestamp) + } +} + +func TestCalculateStats(t *testing.T) { + points := []rawDataPoint{ + {timestamp: 1000, value: 10, status: 1, hasDelay: true, hasStatus: true}, + {timestamp: 2000, value: 20, status: 1, hasDelay: true, hasStatus: true}, + {timestamp: 3000, value: 30, status: 0, hasDelay: true, hasStatus: true}, + {timestamp: 4000, value: 40, status: 1, hasDelay: true, hasStatus: true}, + } + + stats := calculateStats(points, 5*time.Minute) + + assert.Equal(t, uint64(3), stats.TotalUp) + assert.Equal(t, uint64(1), stats.TotalDown) + assert.Equal(t, float32(75), stats.UpPercent) + assert.Equal(t, float64(25), stats.AvgDelay) +} + +func TestCalculateStats_ZeroDelay(t *testing.T) { + points := []rawDataPoint{ + {timestamp: 1000, value: 0, status: 1, hasDelay: true, hasStatus: true}, + {timestamp: 2000, value: 10, status: 1, hasDelay: true, hasStatus: true}, + } + + stats := calculateStats(points, 5*time.Minute) + + assert.Equal(t, float64(5), stats.AvgDelay) + assert.Equal(t, uint64(2), stats.TotalUp) +} + +func TestCalculateStatsEmpty(t *testing.T) { + points := []rawDataPoint{} + stats := calculateStats(points, 5*time.Minute) + + assert.Equal(t, uint64(0), stats.TotalUp) + assert.Equal(t, uint64(0), stats.TotalDown) + assert.Equal(t, float32(0), stats.UpPercent) + assert.Equal(t, float64(0), stats.AvgDelay) + assert.Nil(t, stats.DataPoints) +} + +func TestTSDB_QueryServerMetrics_Float64Precision(t *testing.T) { + tempDir, err := os.MkdirTemp("", "tsdb_test") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + config := &Config{ + DataPath: filepath.Join(tempDir, "tsdb"), + RetentionDays: 1, + MinFreeDiskSpaceGB: 1, + DedupInterval: time.Second, + } + + db, err := Open(config) + require.NoError(t, err) + defer db.Close() + + now := time.Now() + serverID := uint64(1) + largeMemValue := uint64(17_179_869_184) // 16GB + + err = db.WriteServerMetrics(&ServerMetrics{ + ServerID: serverID, + Timestamp: now, + MemUsed: largeMemValue, + }) + require.NoError(t, err) + + db.Flush() + + result, err := db.QueryServerMetrics(serverID, MetricServerMemory, Period1Day) + require.NoError(t, err) + require.NotEmpty(t, result) + + // float64 可以精确表示该值,float32 会丢失精度 + assert.Equal(t, float64(largeMemValue), result[0].Value) +} + +func TestTSDB_QueryServiceHistoryByServerID(t *testing.T) { + tempDir, err := os.MkdirTemp("", "tsdb_test") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + config := &Config{ + DataPath: filepath.Join(tempDir, "tsdb"), + RetentionDays: 1, + MinFreeDiskSpaceGB: 1, + DedupInterval: time.Second, + } + + db, err := Open(config) + require.NoError(t, err) + defer db.Close() + + now := time.Now() + serverID := uint64(1) + serviceID1 := uint64(100) + serviceID2 := uint64(200) + + // 写入两个服务在同一服务器上的数据 + for i := 0; i < 5; i++ { + ts := now.Add(-time.Duration(i) * time.Minute) + + err := db.WriteServiceMetrics(&ServiceMetrics{ + ServiceID: serviceID1, + ServerID: serverID, + Timestamp: ts, + Delay: float64(10 + i), + Successful: true, + }) + require.NoError(t, err) + + err = db.WriteServiceMetrics(&ServiceMetrics{ + ServiceID: serviceID2, + ServerID: serverID, + Timestamp: ts, + Delay: float64(20 + i), + Successful: i%2 == 0, + }) + require.NoError(t, err) + } + + db.Flush() + + results, err := db.QueryServiceHistoryByServerID(serverID, Period1Day) + require.NoError(t, err) + require.Len(t, results, 2, "expected 2 services") + + // 验证 service1:全部成功 + s1, ok := results[serviceID1] + require.True(t, ok) + assert.Equal(t, serviceID1, s1.ServiceID) + require.Len(t, s1.Servers, 1) + assert.Equal(t, serverID, s1.Servers[0].ServerID) + assert.Equal(t, uint64(5), s1.Servers[0].Stats.TotalUp) + assert.Equal(t, uint64(0), s1.Servers[0].Stats.TotalDown) + + // 验证 service2:部分成功 + s2, ok := results[serviceID2] + require.True(t, ok) + assert.Equal(t, serviceID2, s2.ServiceID) + assert.Equal(t, uint64(3), s2.Servers[0].Stats.TotalUp) + assert.Equal(t, uint64(2), s2.Servers[0].Stats.TotalDown) +} + +func TestTSDB_QueryServiceHistoryByServerID_Empty(t *testing.T) { + tempDir, err := os.MkdirTemp("", "tsdb_test") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + config := &Config{ + DataPath: filepath.Join(tempDir, "tsdb"), + RetentionDays: 1, + MinFreeDiskSpaceGB: 1, + DedupInterval: time.Second, + } + + db, err := Open(config) + require.NoError(t, err) + defer db.Close() + + results, err := db.QueryServiceHistoryByServerID(9999, Period1Day) + require.NoError(t, err) + assert.Empty(t, results) +} + +func TestTSDB_QueryServiceHistoryByServerID_ClosedDB(t *testing.T) { + tempDir, err := os.MkdirTemp("", "tsdb_test") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + config := &Config{ + DataPath: filepath.Join(tempDir, "tsdb"), + RetentionDays: 1, + MinFreeDiskSpaceGB: 1, + DedupInterval: time.Second, + } + + db, err := Open(config) + require.NoError(t, err) + db.Close() + + _, err = db.QueryServiceHistoryByServerID(1, Period1Day) + assert.Error(t, err) +} diff --git a/pkg/tsdb/writer.go b/pkg/tsdb/writer.go new file mode 100644 index 0000000..7896c09 --- /dev/null +++ b/pkg/tsdb/writer.go @@ -0,0 +1,301 @@ +package tsdb + +import ( + "fmt" + "strconv" + "sync" + "time" + + "github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb" + "github.com/VictoriaMetrics/VictoriaMetrics/lib/storage" +) + +type bufferedWriter struct { + db *TSDB + buffer []storage.MetricRow + mu sync.Mutex + maxSize int + flushTicker *time.Ticker + stopCh chan struct{} + wg sync.WaitGroup +} + +func newBufferedWriter(db *TSDB, maxSize int, flushInterval time.Duration) *bufferedWriter { + w := &bufferedWriter{ + db: db, + buffer: make([]storage.MetricRow, 0, maxSize), + maxSize: maxSize, + flushTicker: time.NewTicker(flushInterval), + stopCh: make(chan struct{}), + } + w.wg.Add(1) + go w.flushLoop() + return w +} + +func (w *bufferedWriter) flushLoop() { + defer w.wg.Done() + for { + select { + case <-w.flushTicker.C: + w.flush() + case <-w.stopCh: + w.flush() + return + } + } +} + +func (w *bufferedWriter) write(rows []storage.MetricRow) { + w.mu.Lock() + w.buffer = append(w.buffer, rows...) + if len(w.buffer) >= w.maxSize { + rows := w.buffer + w.buffer = make([]storage.MetricRow, 0, w.maxSize) + w.mu.Unlock() + w.db.storage.AddRows(rows, 64) + return + } + w.mu.Unlock() +} + +func (w *bufferedWriter) flush() { + w.mu.Lock() + if len(w.buffer) == 0 { + w.mu.Unlock() + return + } + rows := w.buffer + w.buffer = make([]storage.MetricRow, 0, w.maxSize) + w.mu.Unlock() + + w.db.storage.AddRows(rows, 64) +} + +func (w *bufferedWriter) stop() { + w.flushTicker.Stop() + close(w.stopCh) + w.wg.Wait() +} + +// MetricType 指标类型 +type MetricType string + +const ( + // 服务器指标 + MetricServerCPU MetricType = "nezha_server_cpu" + MetricServerMemory MetricType = "nezha_server_memory" + MetricServerSwap MetricType = "nezha_server_swap" + MetricServerDisk MetricType = "nezha_server_disk" + MetricServerNetInSpeed MetricType = "nezha_server_net_in_speed" + MetricServerNetOutSpeed MetricType = "nezha_server_net_out_speed" + MetricServerNetInTransfer MetricType = "nezha_server_net_in_transfer" + MetricServerNetOutTransfer MetricType = "nezha_server_net_out_transfer" + MetricServerLoad1 MetricType = "nezha_server_load1" + MetricServerLoad5 MetricType = "nezha_server_load5" + MetricServerLoad15 MetricType = "nezha_server_load15" + MetricServerTCPConn MetricType = "nezha_server_tcp_conn" + MetricServerUDPConn MetricType = "nezha_server_udp_conn" + MetricServerProcessCount MetricType = "nezha_server_process_count" + MetricServerTemperature MetricType = "nezha_server_temperature" + MetricServerUptime MetricType = "nezha_server_uptime" + MetricServerGPU MetricType = "nezha_server_gpu" + + // 服务监控指标 + MetricServiceDelay MetricType = "nezha_service_delay" + MetricServiceStatus MetricType = "nezha_service_status" +) + +// ServerMetrics 服务器指标数据 +type ServerMetrics struct { + ServerID uint64 + Timestamp time.Time + CPU float64 + MemUsed uint64 + SwapUsed uint64 + DiskUsed uint64 + NetInSpeed uint64 + NetOutSpeed uint64 + NetInTransfer uint64 + NetOutTransfer uint64 + Load1 float64 + Load5 float64 + Load15 float64 + TCPConnCount uint64 + UDPConnCount uint64 + ProcessCount uint64 + Temperature float64 + Uptime uint64 + GPU float64 +} + +// ServiceMetrics 服务监控指标数据 +type ServiceMetrics struct { + ServiceID uint64 + ServerID uint64 + Timestamp time.Time + Delay float64 + Successful bool +} + +func (db *TSDB) WriteServerMetrics(m *ServerMetrics) error { + db.mu.RLock() + defer db.mu.RUnlock() + if db.closed { + return fmt.Errorf("TSDB is closed") + } + + ts := m.Timestamp.UnixMilli() + serverIDStr := strconv.FormatUint(m.ServerID, 10) + + rows := []storage.MetricRow{ + makeServerMetricRow(MetricServerCPU, serverIDStr, ts, m.CPU), + makeServerMetricRow(MetricServerMemory, serverIDStr, ts, float64(m.MemUsed)), + makeServerMetricRow(MetricServerSwap, serverIDStr, ts, float64(m.SwapUsed)), + makeServerMetricRow(MetricServerDisk, serverIDStr, ts, float64(m.DiskUsed)), + makeServerMetricRow(MetricServerNetInSpeed, serverIDStr, ts, float64(m.NetInSpeed)), + makeServerMetricRow(MetricServerNetOutSpeed, serverIDStr, ts, float64(m.NetOutSpeed)), + makeServerMetricRow(MetricServerNetInTransfer, serverIDStr, ts, float64(m.NetInTransfer)), + makeServerMetricRow(MetricServerNetOutTransfer, serverIDStr, ts, float64(m.NetOutTransfer)), + makeServerMetricRow(MetricServerLoad1, serverIDStr, ts, m.Load1), + makeServerMetricRow(MetricServerLoad5, serverIDStr, ts, m.Load5), + makeServerMetricRow(MetricServerLoad15, serverIDStr, ts, m.Load15), + makeServerMetricRow(MetricServerTCPConn, serverIDStr, ts, float64(m.TCPConnCount)), + makeServerMetricRow(MetricServerUDPConn, serverIDStr, ts, float64(m.UDPConnCount)), + makeServerMetricRow(MetricServerProcessCount, serverIDStr, ts, float64(m.ProcessCount)), + makeServerMetricRow(MetricServerTemperature, serverIDStr, ts, m.Temperature), + makeServerMetricRow(MetricServerUptime, serverIDStr, ts, float64(m.Uptime)), + makeServerMetricRow(MetricServerGPU, serverIDStr, ts, m.GPU), + } + + if db.writer != nil { + db.writer.write(rows) + } else { + db.storage.AddRows(rows, 64) + } + return nil +} + +func (db *TSDB) WriteServiceMetrics(m *ServiceMetrics) error { + db.mu.RLock() + defer db.mu.RUnlock() + if db.closed { + return fmt.Errorf("TSDB is closed") + } + + ts := m.Timestamp.UnixMilli() + serviceIDStr := strconv.FormatUint(m.ServiceID, 10) + serverIDStr := strconv.FormatUint(m.ServerID, 10) + + var status float64 + if m.Successful { + status = 1 + } + + rows := []storage.MetricRow{ + makeServiceMetricRow(MetricServiceDelay, serviceIDStr, serverIDStr, ts, m.Delay), + makeServiceMetricRow(MetricServiceStatus, serviceIDStr, serverIDStr, ts, status), + } + + if db.writer != nil { + db.writer.write(rows) + } else { + db.storage.AddRows(rows, 64) + } + return nil +} + +func makeServerMetricRow(metric MetricType, serverID string, timestamp int64, value float64) storage.MetricRow { + labels := []prompb.Label{ + {Name: "__name__", Value: string(metric)}, + {Name: "server_id", Value: serverID}, + } + return storage.MetricRow{ + MetricNameRaw: storage.MarshalMetricNameRaw(nil, labels), + Timestamp: timestamp, + Value: value, + } +} + +func makeServiceMetricRow(metric MetricType, serviceID, serverID string, timestamp int64, value float64) storage.MetricRow { + labels := []prompb.Label{ + {Name: "__name__", Value: string(metric)}, + {Name: "service_id", Value: serviceID}, + {Name: "server_id", Value: serverID}, + } + return storage.MetricRow{ + MetricNameRaw: storage.MarshalMetricNameRaw(nil, labels), + Timestamp: timestamp, + Value: value, + } +} + +func (db *TSDB) WriteBatchServerMetrics(metrics []*ServerMetrics) error { + db.mu.RLock() + defer db.mu.RUnlock() + if db.closed { + return fmt.Errorf("TSDB is closed") + } + + rows := make([]storage.MetricRow, 0, len(metrics)*17) + for _, m := range metrics { + ts := m.Timestamp.UnixMilli() + serverIDStr := strconv.FormatUint(m.ServerID, 10) + rows = append(rows, + makeServerMetricRow(MetricServerCPU, serverIDStr, ts, m.CPU), + makeServerMetricRow(MetricServerMemory, serverIDStr, ts, float64(m.MemUsed)), + makeServerMetricRow(MetricServerSwap, serverIDStr, ts, float64(m.SwapUsed)), + makeServerMetricRow(MetricServerDisk, serverIDStr, ts, float64(m.DiskUsed)), + makeServerMetricRow(MetricServerNetInSpeed, serverIDStr, ts, float64(m.NetInSpeed)), + makeServerMetricRow(MetricServerNetOutSpeed, serverIDStr, ts, float64(m.NetOutSpeed)), + makeServerMetricRow(MetricServerNetInTransfer, serverIDStr, ts, float64(m.NetInTransfer)), + makeServerMetricRow(MetricServerNetOutTransfer, serverIDStr, ts, float64(m.NetOutTransfer)), + makeServerMetricRow(MetricServerLoad1, serverIDStr, ts, m.Load1), + makeServerMetricRow(MetricServerLoad5, serverIDStr, ts, m.Load5), + makeServerMetricRow(MetricServerLoad15, serverIDStr, ts, m.Load15), + makeServerMetricRow(MetricServerTCPConn, serverIDStr, ts, float64(m.TCPConnCount)), + makeServerMetricRow(MetricServerUDPConn, serverIDStr, ts, float64(m.UDPConnCount)), + makeServerMetricRow(MetricServerProcessCount, serverIDStr, ts, float64(m.ProcessCount)), + makeServerMetricRow(MetricServerTemperature, serverIDStr, ts, m.Temperature), + makeServerMetricRow(MetricServerUptime, serverIDStr, ts, float64(m.Uptime)), + makeServerMetricRow(MetricServerGPU, serverIDStr, ts, m.GPU), + ) + } + + if db.writer != nil { + db.writer.write(rows) + } else { + db.storage.AddRows(rows, 64) + } + return nil +} + +func (db *TSDB) WriteBatchServiceMetrics(metrics []*ServiceMetrics) error { + db.mu.RLock() + defer db.mu.RUnlock() + if db.closed { + return fmt.Errorf("TSDB is closed") + } + + rows := make([]storage.MetricRow, 0, len(metrics)*2) + for _, m := range metrics { + ts := m.Timestamp.UnixMilli() + serviceIDStr := strconv.FormatUint(m.ServiceID, 10) + serverIDStr := strconv.FormatUint(m.ServerID, 10) + var status float64 + if m.Successful { + status = 1 + } + rows = append(rows, + makeServiceMetricRow(MetricServiceDelay, serviceIDStr, serverIDStr, ts, m.Delay), + makeServiceMetricRow(MetricServiceStatus, serviceIDStr, serverIDStr, ts, status), + ) + } + + if db.writer != nil { + db.writer.write(rows) + } else { + db.storage.AddRows(rows, 64) + } + return nil +} diff --git a/service/rpc/nezha.go b/service/rpc/nezha.go index 69c1c26..6373e50 100644 --- a/service/rpc/nezha.go +++ b/service/rpc/nezha.go @@ -12,6 +12,7 @@ import ( "github.com/jinzhu/copier" geoipx "github.com/nezhahq/nezha/pkg/geoip" "github.com/nezhahq/nezha/pkg/grpcx" + "github.com/nezhahq/nezha/pkg/tsdb" "github.com/nezhahq/nezha/model" pb "github.com/nezhahq/nezha/proto" @@ -114,6 +115,44 @@ func (s *NezhaHandler) ReportSystemState(stream pb.NezhaService_ReportSystemStat server.LastActive = time.Now() server.State = &innerState + if singleton.TSDBEnabled() { + maxTemp := 0.0 + for _, t := range innerState.Temperatures { + if t.Temperature > maxTemp { + maxTemp = t.Temperature + } + } + maxGPU := 0.0 + for _, g := range innerState.GPU { + if g > maxGPU { + maxGPU = g + } + } + if err := singleton.TSDBShared.WriteServerMetrics(&tsdb.ServerMetrics{ + ServerID: clientID, + Timestamp: time.Now(), + CPU: innerState.CPU, + MemUsed: innerState.MemUsed, + SwapUsed: innerState.SwapUsed, + DiskUsed: innerState.DiskUsed, + NetInSpeed: innerState.NetInSpeed, + NetOutSpeed: innerState.NetOutSpeed, + NetInTransfer: innerState.NetInTransfer, + NetOutTransfer: innerState.NetOutTransfer, + Load1: innerState.Load1, + Load5: innerState.Load5, + Load15: innerState.Load15, + TCPConnCount: innerState.TcpConnCount, + UDPConnCount: innerState.UdpConnCount, + ProcessCount: innerState.ProcessCount, + Temperature: maxTemp, + Uptime: innerState.Uptime, + GPU: maxGPU, + }); err != nil { + log.Printf("NEZHA>> Failed to write server metrics to TSDB: %v", err) + } + } + // 应对 dashboard / agent 重启的情况,如果从未记录过,先打点,等到小时时间点时入库 if server.PrevTransferInSnapshot == 0 || server.PrevTransferOutSnapshot == 0 { server.PrevTransferInSnapshot = state.NetInTransfer diff --git a/service/singleton/frontend-templates.yaml b/service/singleton/frontend-templates.yaml index d257fbd..37ddd0b 100644 --- a/service/singleton/frontend-templates.yaml +++ b/service/singleton/frontend-templates.yaml @@ -2,14 +2,14 @@ name: "OfficialAdmin" repository: "https://github.com/nezhahq/admin-frontend" author: "nezhahq" - version: "v1.14.7" + version: "v2.0.3" is_admin: true is_official: true - path: "user-dist" name: "Official" repository: "https://github.com/hamster1963/nezha-dash-v1" author: "hamster1963" - version: "v1.33.0" + version: "v2.0.0" is_official: true - path: "nazhua-dist" name: "Nazhua" diff --git a/service/singleton/servicesentinel.go b/service/singleton/servicesentinel.go index 310c49a..9b7aef4 100644 --- a/service/singleton/servicesentinel.go +++ b/service/singleton/servicesentinel.go @@ -16,6 +16,7 @@ import ( "golang.org/x/exp/constraints" "github.com/nezhahq/nezha/model" + "github.com/nezhahq/nezha/pkg/tsdb" "github.com/nezhahq/nezha/pkg/utils" pb "github.com/nezhahq/nezha/proto" ) @@ -39,7 +40,7 @@ type ReportData struct { type _TodayStatsOfService struct { Up uint64 // 今日在线计数 Down uint64 // 今日离线计数 - Delay float32 // 今日平均延迟 + Delay float64 // 今日平均延迟 } type serviceResponseData = _TodayStatsOfService @@ -51,8 +52,9 @@ type serviceTaskStatus struct { } type pingStore struct { - count int - ping float32 + count int + ping float64 + successCount int } /* @@ -108,23 +110,7 @@ func NewServiceSentinel(serviceSentinelDispatchBus chan<- *model.Service) (*Serv year, month, day := time.Now().Date() today := time.Date(year, month, day, 0, 0, 0, 0, Loc) - - var mhs []model.ServiceHistory - // 加载当日记录 - DB.Where("created_at >= ? AND server_id = 0", today).Find(&mhs) - totalDelay := make(map[uint64]float32) - totalDelayCount := make(map[uint64]float32) - for _, mh := range mhs { - totalDelay[mh.ServiceID] += mh.AvgDelay - totalDelayCount[mh.ServiceID]++ - ss.serviceStatusToday[mh.ServiceID].Up += mh.Up - ss.monthlyStatus[mh.ServiceID].TotalUp += mh.Up - ss.serviceStatusToday[mh.ServiceID].Down += mh.Down - ss.monthlyStatus[mh.ServiceID].TotalDown += mh.Down - } - for id, delay := range totalDelay { - ss.serviceStatusToday[id].Delay = delay / float32(totalDelayCount[id]) - } + ss.loadTodayStats(today) // 启动服务监控器 go ss.worker() @@ -135,6 +121,12 @@ func NewServiceSentinel(serviceSentinelDispatchBus chan<- *model.Service) (*Serv return nil, err } + // 每周日凌晨 4:00 执行系统存储维护 + _, err = CronShared.AddFunc("0 0 4 * * 0", PerformMaintenance) + if err != nil { + log.Printf("NEZHA>> Warning: failed to schedule maintenance task: %v", err) + } + return ss, nil } @@ -171,6 +163,16 @@ func (ss *ServiceSentinel) Dispatch(r ReportData) { ss.serviceReportChannel <- r } +// sortServices 按 DisplayIndex 降序、ID 升序排列服务列表 +func sortServices(services []*model.Service) { + slices.SortFunc(services, func(a, b *model.Service) int { + if a.DisplayIndex != b.DisplayIndex { + return cmp.Compare(b.DisplayIndex, a.DisplayIndex) + } + return cmp.Compare(a.ID, b.ID) + }) +} + func (ss *ServiceSentinel) UpdateServiceList() { ss.servicesLock.RLock() defer ss.servicesLock.RUnlock() @@ -179,9 +181,7 @@ func (ss *ServiceSentinel) UpdateServiceList() { defer ss.serviceListLock.Unlock() ss.serviceList = utils.MapValuesToSlice(ss.services) - slices.SortFunc(ss.serviceList, func(a, b *model.Service) int { - return cmp.Compare(a.ID, b.ID) - }) + sortServices(ss.serviceList) } // loadServiceHistory 加载服务监控器的历史状态信息 @@ -207,6 +207,7 @@ func (ss *ServiceSentinel) loadServiceHistory() error { ss.serviceStatusToday[service.ID] = &_TodayStatsOfService{} } ss.serviceList = services + sortServices(ss.serviceList) year, month, day := time.Now().Date() today := time.Date(year, month, day, 0, 0, 0, 0, Loc) @@ -215,33 +216,111 @@ func (ss *ServiceSentinel) loadServiceHistory() error { ss.monthlyStatus[service.ID] = &serviceResponseItem{ service: service, ServiceResponseItem: model.ServiceResponseItem{ - Delay: &[30]float32{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, - Up: &[30]uint64{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, - Down: &[30]uint64{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + Delay: &[30]float64{}, + Up: &[30]uint64{}, + Down: &[30]uint64{}, }, } } - // 加载服务监控历史记录 - var mhs []model.ServiceHistory - DB.Where("created_at > ? AND created_at < ? AND server_id = 0", today.AddDate(0, 0, -29), today).Find(&mhs) - var delayCount = make(map[int]int) - for _, mh := range mhs { - dayIndex := 28 - (int(today.Sub(mh.CreatedAt).Hours()) / 24) - if dayIndex < 0 { - continue - } - ss.monthlyStatus[mh.ServiceID].Delay[dayIndex] = (ss.monthlyStatus[mh.ServiceID].Delay[dayIndex]*float32(delayCount[dayIndex]) + mh.AvgDelay) / float32(delayCount[dayIndex]+1) - delayCount[dayIndex]++ - ss.monthlyStatus[mh.ServiceID].Up[dayIndex] += mh.Up - ss.monthlyStatus[mh.ServiceID].TotalUp += mh.Up - ss.monthlyStatus[mh.ServiceID].Down[dayIndex] += mh.Down - ss.monthlyStatus[mh.ServiceID].TotalDown += mh.Down + if TSDBEnabled() { + ss.loadMonthlyStatusFromTSDB(services, today) + } else { + ss.loadMonthlyStatusFromDB(today) } return nil } +func (ss *ServiceSentinel) loadMonthlyStatusFromTSDB(services []*model.Service, today time.Time) { + for _, service := range services { + dailyStats, err := TSDBShared.QueryServiceDailyStats(service.ID, today, 30) + if err != nil { + log.Printf("NEZHA>> Failed to load TSDB history for service %d: %v", service.ID, err) + continue + } + ms := ss.monthlyStatus[service.ID] + for i := 0; i < 29; i++ { + ms.Up[i] = dailyStats[i].Up + ms.TotalUp += dailyStats[i].Up + ms.Down[i] = dailyStats[i].Down + ms.TotalDown += dailyStats[i].Down + ms.Delay[i] = dailyStats[i].Delay + } + } +} + +func (ss *ServiceSentinel) loadMonthlyStatusFromDB(today time.Time) { + var mhs []model.ServiceHistory + DB.Where("created_at > ? AND created_at < ? AND server_id = 0", today.AddDate(0, 0, -29), today).Find(&mhs) + delayCount := make(map[uint64]map[int]int) + for _, mh := range mhs { + dayIndex := 28 - int(today.Sub(mh.CreatedAt).Hours())/24 + if dayIndex < 0 { + continue + } + ms := ss.monthlyStatus[mh.ServiceID] + if ms == nil { + continue + } + if delayCount[mh.ServiceID] == nil { + delayCount[mh.ServiceID] = make(map[int]int) + } + ms.Delay[dayIndex] = (ms.Delay[dayIndex]*float64(delayCount[mh.ServiceID][dayIndex]) + mh.AvgDelay) / float64(delayCount[mh.ServiceID][dayIndex]+1) + delayCount[mh.ServiceID][dayIndex]++ + ms.Up[dayIndex] += mh.Up + ms.TotalUp += mh.Up + ms.Down[dayIndex] += mh.Down + ms.TotalDown += mh.Down + } +} + +func (ss *ServiceSentinel) loadTodayStats(today time.Time) { + if TSDBEnabled() { + for serviceID, ms := range ss.monthlyStatus { + result, err := TSDBShared.QueryServiceHistory(serviceID, tsdb.Period1Day) + if err != nil { + log.Printf("NEZHA>> Failed to load TSDB today stats for service %d: %v", serviceID, err) + continue + } + var totalUp, totalDown uint64 + var totalDelay float64 + var delayCount int + for _, serverStats := range result.Servers { + totalUp += serverStats.Stats.TotalUp + totalDown += serverStats.Stats.TotalDown + if serverStats.Stats.AvgDelay > 0 { + totalDelay += serverStats.Stats.AvgDelay + delayCount++ + } + } + ss.serviceStatusToday[serviceID].Up = totalUp + ss.serviceStatusToday[serviceID].Down = totalDown + if delayCount > 0 { + ss.serviceStatusToday[serviceID].Delay = totalDelay / float64(delayCount) + } + ms.TotalUp += totalUp + ms.TotalDown += totalDown + } + } else { + var mhs []model.ServiceHistory + DB.Where("created_at >= ? AND server_id = 0", today).Find(&mhs) + totalDelay := make(map[uint64]float64) + totalDelayCount := make(map[uint64]int) + for _, mh := range mhs { + ss.serviceStatusToday[mh.ServiceID].Up += mh.Up + ss.monthlyStatus[mh.ServiceID].TotalUp += mh.Up + ss.serviceStatusToday[mh.ServiceID].Down += mh.Down + ss.monthlyStatus[mh.ServiceID].TotalDown += mh.Down + totalDelay[mh.ServiceID] += mh.AvgDelay + totalDelayCount[mh.ServiceID]++ + } + for id, delay := range totalDelay { + ss.serviceStatusToday[id].Delay = delay / float64(totalDelayCount[id]) + } + } +} + func (ss *ServiceSentinel) Update(m *model.Service) error { ss.serviceResponseDataStoreLock.Lock() defer ss.serviceResponseDataStoreLock.Unlock() @@ -266,9 +345,9 @@ func (ss *ServiceSentinel) Update(m *model.Service) error { ss.monthlyStatus[m.ID] = &serviceResponseItem{ service: m, ServiceResponseItem: model.ServiceResponseItem{ - Delay: &[30]float32{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, - Up: &[30]uint64{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, - Down: &[30]uint64{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + Delay: &[30]float64{}, + Up: &[30]uint64{}, + Down: &[30]uint64{}, }, } if ss.serviceCurrentStatusData[m.ID] == nil { @@ -406,6 +485,7 @@ func (ss *ServiceSentinel) worker() { mh := r.Data if mh.Type == model.TaskTypeTCPPing || mh.Type == model.TaskTypeICMPPing { + // TCP/ICMP Ping 使用平均值计算后再写入 serviceTcpMap, ok := ss.serviceResponsePing[mh.GetId()] if !ok { serviceTcpMap = make(map[uint64]*pingStore) @@ -416,28 +496,56 @@ func (ss *ServiceSentinel) worker() { ts = &pingStore{} } ts.count++ - ts.ping = (ts.ping*float32(ts.count-1) + mh.Delay) / float32(ts.count) + ts.ping = (ts.ping*float64(ts.count-1) + float64(mh.Delay)) / float64(ts.count) + if mh.Successful { + ts.successCount++ + } if ts.count == Conf.AvgPingCount { - if err := DB.Create(&model.ServiceHistory{ - ServiceID: mh.GetId(), - AvgDelay: ts.ping, - Data: mh.Data, - ServerID: r.Reporter, - }).Error; err != nil { - log.Printf("NEZHA>> Failed to save service monitor metrics: %v", err) + if TSDBEnabled() { + if err := TSDBShared.WriteServiceMetrics(&tsdb.ServiceMetrics{ + ServiceID: mh.GetId(), + ServerID: r.Reporter, + Timestamp: time.Now(), + Delay: ts.ping, + Successful: ts.successCount*2 >= ts.count, + }); err != nil { + log.Printf("NEZHA>> Failed to save service monitor metrics to TSDB: %v", err) + } + } else { + if err := DB.Create(&model.ServiceHistory{ + ServiceID: mh.GetId(), + AvgDelay: ts.ping, + Data: mh.Data, + ServerID: r.Reporter, + }).Error; err != nil { + log.Printf("NEZHA>> Failed to save service monitor metrics: %v", err) + } } ts.count = 0 - ts.ping = mh.Delay + ts.ping = 0 + ts.successCount = 0 } serviceTcpMap[r.Reporter] = ts + } else { + if TSDBEnabled() { + if err := TSDBShared.WriteServiceMetrics(&tsdb.ServiceMetrics{ + ServiceID: mh.GetId(), + ServerID: r.Reporter, + Timestamp: time.Now(), + Delay: float64(mh.Delay), + Successful: mh.Successful, + }); err != nil { + log.Printf("NEZHA>> Failed to save service monitor metrics to TSDB: %v", err) + } + } } ss.serviceResponseDataStoreLock.Lock() // 写入当天状态 if mh.Successful { ss.serviceStatusToday[mh.GetId()].Delay = (ss.serviceStatusToday[mh. - GetId()].Delay*float32(ss.serviceStatusToday[mh.GetId()].Up) + - mh.Delay) / float32(ss.serviceStatusToday[mh.GetId()].Up+1) + GetId()].Delay*float64(ss.serviceStatusToday[mh.GetId()].Up) + + float64(mh.Delay)) / float64(ss.serviceStatusToday[mh.GetId()].Up+1) ss.serviceStatusToday[mh.GetId()].Up++ } else { ss.serviceStatusToday[mh.GetId()].Down++ @@ -463,7 +571,7 @@ func (ss *ServiceSentinel) worker() { rd := ss.serviceResponseDataStore[mh.GetId()] if cs.Successful { rd.Up++ - rd.Delay = (rd.Delay*float32(rd.Up-1) + cs.Delay) / float32(rd.Up) + rd.Delay = (rd.Delay*float64(rd.Up-1) + float64(cs.Delay)) / float64(rd.Up) } else { rd.Down++ } @@ -482,20 +590,20 @@ func (ss *ServiceSentinel) worker() { stateCode = GetStatusCode(upPercent) } - // 数据持久化 if len(ss.serviceCurrentStatusData[mh.GetId()].result) == _CurrentStatusSize { ss.serviceCurrentStatusData[mh.GetId()].t = currentTime - rd := ss.serviceResponseDataStore[mh.GetId()] - if err := DB.Create(&model.ServiceHistory{ - ServiceID: mh.GetId(), - AvgDelay: rd.Delay, - Data: mh.Data, - Up: rd.Up, - Down: rd.Down, - }).Error; err != nil { - log.Printf("NEZHA>> Failed to save service monitor metrics: %v", err) + if !TSDBEnabled() { + rd := ss.serviceResponseDataStore[mh.GetId()] + if err := DB.Create(&model.ServiceHistory{ + ServiceID: mh.GetId(), + AvgDelay: rd.Delay, + Data: mh.Data, + Up: rd.Up, + Down: rd.Down, + }).Error; err != nil { + log.Printf("NEZHA>> Failed to save service monitor metrics: %v", err) + } } - ss.serviceCurrentStatusData[mh.GetId()].result = ss.serviceCurrentStatusData[mh.GetId()].result[:0] } diff --git a/service/singleton/servicesentinel_test.go b/service/singleton/servicesentinel_test.go new file mode 100644 index 0000000..bc0492c --- /dev/null +++ b/service/singleton/servicesentinel_test.go @@ -0,0 +1,336 @@ +package singleton + +import ( + "os" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + + "github.com/nezhahq/nezha/model" + "github.com/nezhahq/nezha/pkg/tsdb" +) + +func newTestSentinel(serviceIDs []uint64) *ServiceSentinel { + ss := &ServiceSentinel{ + serviceStatusToday: make(map[uint64]*_TodayStatsOfService), + monthlyStatus: make(map[uint64]*serviceResponseItem), + } + for _, id := range serviceIDs { + ss.serviceStatusToday[id] = &_TodayStatsOfService{} + ss.monthlyStatus[id] = &serviceResponseItem{ + service: &model.Service{Common: model.Common{ID: id}}, + ServiceResponseItem: model.ServiceResponseItem{ + Delay: &[30]float64{}, + Up: &[30]uint64{}, + Down: &[30]uint64{}, + }, + } + } + return ss +} + +func setupTestDB(t *testing.T) func() { + t.Helper() + var err error + DB, err = gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) + require.NoError(t, err) + require.NoError(t, DB.AutoMigrate(model.ServiceHistory{})) + return func() { DB = nil } +} + +func setupTestTSDB(t *testing.T) (*tsdb.TSDB, func()) { + t.Helper() + tempDir, err := os.MkdirTemp("", "tsdb_sentinel_test") + require.NoError(t, err) + config := &tsdb.Config{ + DataPath: filepath.Join(tempDir, "tsdb"), + RetentionDays: 30, + MinFreeDiskSpaceGB: 1, + DedupInterval: time.Second, + } + db, err := tsdb.Open(config) + require.NoError(t, err) + TSDBShared = db + return db, func() { + db.Close() + TSDBShared = nil + os.RemoveAll(tempDir) + } +} + +func TestLoadMonthlyStatusFromDB(t *testing.T) { + cleanup := setupTestDB(t) + defer cleanup() + + year, month, day := time.Now().Date() + today := time.Date(year, month, day, 0, 0, 0, 0, time.UTC) + + serviceID := uint64(1) + ss := newTestSentinel([]uint64{serviceID}) + + DB.Create(&model.ServiceHistory{ + ServiceID: serviceID, + ServerID: 0, + AvgDelay: 10.0, + Up: 5, + Down: 1, + CreatedAt: today.Add(-25 * time.Hour), + }) + DB.Create(&model.ServiceHistory{ + ServiceID: serviceID, + ServerID: 0, + AvgDelay: 20.0, + Up: 3, + Down: 2, + CreatedAt: today.Add(-25 * time.Hour), + }) + DB.Create(&model.ServiceHistory{ + ServiceID: serviceID, + ServerID: 0, + AvgDelay: 30.0, + Up: 10, + Down: 0, + CreatedAt: today.Add(-49 * time.Hour), + }) + + ss.loadMonthlyStatusFromDB(today) + + ms := ss.monthlyStatus[serviceID] + + // day -1: index 27, two records with AvgDelay 10 and 20 + assert.InDelta(t, 15.0, ms.Delay[27], 0.01) + assert.Equal(t, uint64(8), ms.Up[27]) + assert.Equal(t, uint64(3), ms.Down[27]) + + // day -2: index 26 + assert.InDelta(t, 30.0, ms.Delay[26], 0.01) + assert.Equal(t, uint64(10), ms.Up[26]) + assert.Equal(t, uint64(0), ms.Down[26]) + + // totals + assert.Equal(t, uint64(18), ms.TotalUp) + assert.Equal(t, uint64(3), ms.TotalDown) + + // today (index 29) should be untouched + assert.Equal(t, float64(0), ms.Delay[29]) + assert.Equal(t, uint64(0), ms.Up[29]) +} + +func TestLoadMonthlyStatusFromDB_IgnoresToday(t *testing.T) { + cleanup := setupTestDB(t) + defer cleanup() + + year, month, day := time.Now().Date() + today := time.Date(year, month, day, 0, 0, 0, 0, time.UTC) + + serviceID := uint64(1) + ss := newTestSentinel([]uint64{serviceID}) + + DB.Create(&model.ServiceHistory{ + ServiceID: serviceID, + ServerID: 0, + AvgDelay: 50.0, + Up: 100, + Down: 5, + CreatedAt: today.Add(2 * time.Hour), + }) + + ss.loadMonthlyStatusFromDB(today) + + ms := ss.monthlyStatus[serviceID] + assert.Equal(t, uint64(0), ms.TotalUp) + assert.Equal(t, uint64(0), ms.TotalDown) +} + +func TestLoadMonthlyStatusFromDB_UnknownServiceIgnored(t *testing.T) { + cleanup := setupTestDB(t) + defer cleanup() + + year, month, day := time.Now().Date() + today := time.Date(year, month, day, 0, 0, 0, 0, time.UTC) + + ss := newTestSentinel([]uint64{1}) + + DB.Create(&model.ServiceHistory{ + ServiceID: 999, + ServerID: 0, + AvgDelay: 10.0, + Up: 5, + Down: 1, + CreatedAt: today.Add(-25 * time.Hour), + }) + + ss.loadMonthlyStatusFromDB(today) + + ms := ss.monthlyStatus[uint64(1)] + assert.Equal(t, uint64(0), ms.TotalUp) +} + +func TestLoadTodayStatsFromDB(t *testing.T) { + cleanup := setupTestDB(t) + defer cleanup() + + year, month, day := time.Now().Date() + today := time.Date(year, month, day, 0, 0, 0, 0, time.UTC) + + serviceID := uint64(1) + ss := newTestSentinel([]uint64{serviceID}) + + DB.Create(&model.ServiceHistory{ + ServiceID: serviceID, + ServerID: 0, + AvgDelay: 10.0, + Up: 5, + Down: 1, + CreatedAt: today.Add(1 * time.Hour), + }) + DB.Create(&model.ServiceHistory{ + ServiceID: serviceID, + ServerID: 0, + AvgDelay: 30.0, + Up: 3, + Down: 2, + CreatedAt: today.Add(2 * time.Hour), + }) + + ss.loadTodayStats(today) + + st := ss.serviceStatusToday[serviceID] + assert.Equal(t, uint64(8), st.Up) + assert.Equal(t, uint64(3), st.Down) + assert.InDelta(t, 20.0, st.Delay, 0.01) + + ms := ss.monthlyStatus[serviceID] + assert.Equal(t, uint64(8), ms.TotalUp) + assert.Equal(t, uint64(3), ms.TotalDown) +} + +func TestLoadMonthlyStatusFromTSDB(t *testing.T) { + db, cleanup := setupTestTSDB(t) + defer cleanup() + + year, month, day := time.Now().Date() + today := time.Date(year, month, day, 0, 0, 0, 0, time.UTC) + + serviceID := uint64(1) + services := []*model.Service{{Common: model.Common{ID: serviceID}}} + ss := newTestSentinel([]uint64{serviceID}) + + yesterday := today.Add(-25 * time.Hour) + for i := 0; i < 5; i++ { + ts := yesterday.Add(time.Duration(i) * time.Minute) + require.NoError(t, db.WriteServiceMetrics(&tsdb.ServiceMetrics{ + ServiceID: serviceID, + ServerID: 1, + Timestamp: ts, + Delay: float64(10 + i), + Successful: true, + })) + } + for i := 0; i < 3; i++ { + require.NoError(t, db.WriteServiceMetrics(&tsdb.ServiceMetrics{ + ServiceID: serviceID, + ServerID: 1, + Timestamp: yesterday.Add(time.Duration(i+10) * time.Minute), + Delay: float64(20 + i), + Successful: false, + })) + } + + db.Flush() + + ss.loadMonthlyStatusFromTSDB(services, today) + + ms := ss.monthlyStatus[serviceID] + // day -1: dayIndex 28 + assert.Equal(t, uint64(5), ms.Up[28]) + assert.Equal(t, uint64(3), ms.Down[28]) + assert.Equal(t, uint64(5), ms.TotalUp) + assert.Equal(t, uint64(3), ms.TotalDown) + assert.Greater(t, ms.Delay[28], float64(0)) + + // today (index 29) should be untouched + assert.Equal(t, uint64(0), ms.Up[29]) +} + +func TestLoadTodayStatsFromTSDB(t *testing.T) { + db, cleanup := setupTestTSDB(t) + defer cleanup() + + year, month, day := time.Now().Date() + today := time.Date(year, month, day, 0, 0, 0, 0, time.UTC) + + serviceID := uint64(1) + ss := newTestSentinel([]uint64{serviceID}) + + now := time.Now() + for i := 0; i < 4; i++ { + ts := now.Add(-time.Duration(i) * time.Minute) + require.NoError(t, db.WriteServiceMetrics(&tsdb.ServiceMetrics{ + ServiceID: serviceID, + ServerID: 1, + Timestamp: ts, + Delay: float64(10 + i), + Successful: true, + })) + } + for i := 0; i < 2; i++ { + ts := now.Add(-time.Duration(i+10) * time.Minute) + require.NoError(t, db.WriteServiceMetrics(&tsdb.ServiceMetrics{ + ServiceID: serviceID, + ServerID: 1, + Timestamp: ts, + Delay: 0, + Successful: false, + })) + } + + db.Flush() + + ss.loadTodayStats(today) + + st := ss.serviceStatusToday[serviceID] + assert.Greater(t, st.Up, uint64(0)) + assert.Greater(t, st.Down, uint64(0)) + + ms := ss.monthlyStatus[serviceID] + assert.Equal(t, st.Up, ms.TotalUp) + assert.Equal(t, st.Down, ms.TotalDown) +} + +func TestLoadMonthlyStatusFromTSDB_NoDoubleCountToday(t *testing.T) { + db, cleanup := setupTestTSDB(t) + defer cleanup() + + year, month, day := time.Now().Date() + today := time.Date(year, month, day, 0, 0, 0, 0, time.UTC) + + serviceID := uint64(1) + services := []*model.Service{{Common: model.Common{ID: serviceID}}} + ss := newTestSentinel([]uint64{serviceID}) + + now := time.Now() + for i := 0; i < 5; i++ { + require.NoError(t, db.WriteServiceMetrics(&tsdb.ServiceMetrics{ + ServiceID: serviceID, + ServerID: 1, + Timestamp: now.Add(-time.Duration(i) * time.Minute), + Delay: 10.0, + Successful: true, + })) + } + db.Flush() + + ss.loadMonthlyStatusFromTSDB(services, today) + totalAfterMonthly := ss.monthlyStatus[serviceID].TotalUp + + ss.loadTodayStats(today) + totalAfterToday := ss.monthlyStatus[serviceID].TotalUp + + assert.Equal(t, totalAfterMonthly+ss.serviceStatusToday[serviceID].Up, totalAfterToday) +} diff --git a/service/singleton/singleton.go b/service/singleton/singleton.go index e586d0b..b04be1a 100644 --- a/service/singleton/singleton.go +++ b/service/singleton/singleton.go @@ -87,12 +87,13 @@ func InitDBFromPath(path string) error { } err = DB.AutoMigrate(model.Server{}, model.User{}, model.ServerGroup{}, model.NotificationGroup{}, model.Notification{}, model.AlertRule{}, model.Service{}, model.NotificationGroupNotification{}, - model.ServiceHistory{}, model.Cron{}, model.Transfer{}, model.ServerGroupServer{}, + model.Cron{}, model.Transfer{}, model.ServerGroupServer{}, model.NAT{}, model.DDNSProfile{}, model.NotificationGroupNotification{}, model.WAF{}, model.Oauth2Bind{}) if err != nil { return err } + return nil } @@ -130,14 +131,9 @@ func RecordTransferHourlyUsage(servers ...*model.Server) { log.Printf("NEZHA>> Saved traffic metrics to database. Affected %d row(s), Error: %v", len(txs), DB.Create(txs).Error) } -// CleanServiceHistory 清理无效或过时的 监控记录 和 流量记录 -func CleanServiceHistory() { - // 清理已被删除的服务器的监控记录与流量记录 - DB.Unscoped().Delete(&model.ServiceHistory{}, "created_at < ? OR service_id NOT IN (SELECT `id` FROM services)", time.Now().AddDate(0, 0, -30)) - // 由于网络监控记录的数据较多,并且前端仅使用了 1 天的数据 - // 考虑到 sqlite 数据量问题,仅保留一天数据, - // server_id = 0 的数据会用于/service页面的可用性展示 - DB.Unscoped().Delete(&model.ServiceHistory{}, "(created_at < ? AND server_id != 0) OR service_id NOT IN (SELECT `id` FROM services)", time.Now().AddDate(0, 0, -1)) +// CleanMonitorHistory 清理流量记录(TSDB 有自己的保留策略) +func CleanMonitorHistory() { + // 清理已被删除的服务器的流量记录 DB.Unscoped().Delete(&model.Transfer{}, "server_id NOT IN (SELECT `id` FROM servers)") // 计算可清理流量记录的时长 var allServerKeep time.Time @@ -179,6 +175,28 @@ func CleanServiceHistory() { } } +// PerformMaintenance 执行系统维护(SQLite VACUUM 和 TSDB 维护) +func PerformMaintenance() { + log.Println("NEZHA>> Starting system maintenance...") + + // 1. SQLite 维护 + if DB != nil { + log.Println("NEZHA>> SQLite: Starting VACUUM...") + if err := DB.Exec("VACUUM").Error; err != nil { + log.Printf("NEZHA>> SQLite: VACUUM failed: %v", err) + } else { + log.Println("NEZHA>> SQLite: VACUUM completed") + } + } + + // 2. TSDB 维护 + if TSDBEnabled() { + TSDBShared.Maintenance() + } + + log.Println("NEZHA>> System maintenance completed") +} + // IPDesensitize 根据设置选择是否对IP进行打码处理 返回处理后的IP(关闭打码则返回原IP) func IPDesensitize(ip string) string { if Conf.EnablePlainIPInNotification { diff --git a/service/singleton/tsdb.go b/service/singleton/tsdb.go new file mode 100644 index 0000000..745e83d --- /dev/null +++ b/service/singleton/tsdb.go @@ -0,0 +1,73 @@ +package singleton + +import ( + "log" + "time" + + "github.com/nezhahq/nezha/model" + "github.com/nezhahq/nezha/pkg/tsdb" +) + +var TSDBShared *tsdb.TSDB + +func InitTSDB() error { + config := &tsdb.Config{ + RetentionDays: 30, + MinFreeDiskSpaceGB: 1, + MaxMemoryMB: 256, + } + + if Conf.TSDB.DataPath != "" { + config.DataPath = Conf.TSDB.DataPath + } + if Conf.TSDB.RetentionDays > 0 { + config.RetentionDays = Conf.TSDB.RetentionDays + } + if Conf.TSDB.MinFreeDiskSpaceGB > 0 { + config.MinFreeDiskSpaceGB = Conf.TSDB.MinFreeDiskSpaceGB + } + if Conf.TSDB.MaxMemoryMB > 0 { + config.MaxMemoryMB = Conf.TSDB.MaxMemoryMB + } + if Conf.TSDB.WriteBufferSize > 0 { + config.WriteBufferSize = Conf.TSDB.WriteBufferSize + } + if Conf.TSDB.WriteBufferFlushInterval > 0 { + config.WriteBufferFlushInterval = time.Duration(Conf.TSDB.WriteBufferFlushInterval) * time.Second + } + + if !config.Enabled() { + log.Println("NEZHA>> TSDB is disabled (tsdb.data_path not configured)") + if DB != nil { + return DB.AutoMigrate(model.ServiceHistory{}) + } + return nil + } + + var err error + TSDBShared, err = tsdb.Open(config) + if err != nil { + return err + } + + log.Println("NEZHA>> TSDB initialized successfully") + + if DB != nil && DB.Migrator().HasTable("service_histories") { + log.Println("NEZHA>> Dropping legacy service_histories table (TSDB is now enabled). Historical data will NOT be migrated.") + if err := DB.Migrator().DropTable("service_histories"); err != nil { + log.Printf("NEZHA>> Warning: failed to drop service_histories table: %v", err) + } + } + + return nil +} + +func TSDBEnabled() bool { + return TSDBShared != nil && !TSDBShared.IsClosed() +} + +func CloseTSDB() { + if TSDBShared != nil { + TSDBShared.Close() + } +} From 4c1584dd5cd527cb20d8ec86146010474c6124db Mon Sep 17 00:00:00 2001 From: naiba Date: Mon, 16 Feb 2026 02:18:57 +0800 Subject: [PATCH 29/45] feat(tsdb): increase 1d downsample resolution from 5min to 30s --- pkg/tsdb/query.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/tsdb/query.go b/pkg/tsdb/query.go index e5b7379..6f4d8ff 100644 --- a/pkg/tsdb/query.go +++ b/pkg/tsdb/query.go @@ -48,7 +48,7 @@ func (p QueryPeriod) Duration() time.Duration { } // DownsampleInterval 返回降采样间隔 -// 1d: 5分钟一个点 (288个点) +// 1d: 30秒一个点 (2880个点) // 7d: 30分钟一个点 (336个点) // 30d: 2小时一个点 (360个点) func (p QueryPeriod) DownsampleInterval() time.Duration { @@ -58,7 +58,7 @@ func (p QueryPeriod) DownsampleInterval() time.Duration { case Period30Days: return 2 * time.Hour default: - return 5 * time.Minute + return 30 * time.Second } } From f9e379bdcce2443904c541f2f696496525705a06 Mon Sep 17 00:00:00 2001 From: naiba Date: Mon, 16 Feb 2026 02:23:53 +0800 Subject: [PATCH 30/45] fix(tsdb): update test to match 30s downsample interval for 1d period --- pkg/tsdb/tsdb_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/tsdb/tsdb_test.go b/pkg/tsdb/tsdb_test.go index fe2ab4d..d6c16de 100644 --- a/pkg/tsdb/tsdb_test.go +++ b/pkg/tsdb/tsdb_test.go @@ -249,7 +249,7 @@ func TestQueryPeriod_Duration(t *testing.T) { } func TestQueryPeriod_DownsampleInterval(t *testing.T) { - assert.Equal(t, 5*time.Minute, Period1Day.DownsampleInterval()) + assert.Equal(t, 30*time.Second, Period1Day.DownsampleInterval()) assert.Equal(t, 30*time.Minute, Period7Days.DownsampleInterval()) assert.Equal(t, 2*time.Hour, Period30Days.DownsampleInterval()) } From d9e05e594a0b96a8a23268fb32264cb1366641eb Mon Sep 17 00:00:00 2001 From: naiba Date: Mon, 16 Feb 2026 13:36:45 +0800 Subject: [PATCH 31/45] feat: add AtomGit code and release sync workflows --- .github/sync_atomgit.py | 123 +++++++++++++++++++++ .github/workflows/sync-code-atomgit.yml | 16 +++ .github/workflows/sync-release-atomgit.yml | 17 +++ 3 files changed, 156 insertions(+) create mode 100644 .github/sync_atomgit.py create mode 100644 .github/workflows/sync-code-atomgit.yml create mode 100644 .github/workflows/sync-release-atomgit.yml diff --git a/.github/sync_atomgit.py b/.github/sync_atomgit.py new file mode 100644 index 0000000..fe82c31 --- /dev/null +++ b/.github/sync_atomgit.py @@ -0,0 +1,123 @@ +import os +import time +import requests +from github import Github + +ATOMGIT_API = "https://api.atomgit.com/api/v5" +ATOMGIT_OWNER = "naiba" +ATOMGIT_REPO = "nezha-dashboard" +GITHUB_REPO = "nezhahq/nezha" + + +def get_github_latest_release(): + g = Github() + repo = g.get_repo(GITHUB_REPO) + release = repo.get_latest_release() + if not release: + print("No releases found.") + return + + print(f"Latest release tag is: {release.tag_name}") + print(f"Latest release info is: {release.body}") + files = [] + for asset in release.get_assets(): + url = asset.browser_download_url + name = asset.name + + response = requests.get(url) + if response.status_code == 200: + with open(name, "wb") as f: + f.write(response.content) + print(f"Downloaded {name}") + else: + print(f"Failed to download {name}") + files.append(get_abs_path(name)) + sync_to_atomgit(release.tag_name, release.body, files) + + +def sync_to_atomgit(tag, body, files): + access_token = os.environ["ATOMGIT_PAT"] + release_api_uri = f"{ATOMGIT_API}/repos/{ATOMGIT_OWNER}/{ATOMGIT_REPO}/releases" + + release_data = { + "access_token": access_token, + "tag_name": tag, + "name": tag, + "body": body, + "prerelease": False, + "target_commitish": "master", + } + + release_resp = None + for attempt in range(3): + try: + release_resp = requests.post(release_api_uri, json=release_data, timeout=30) + release_resp.raise_for_status() + break + except requests.exceptions.Timeout: + print( + f"Create release timed out, retrying in 30s... (attempt {attempt + 1})" + ) + time.sleep(30) + except requests.exceptions.RequestException as err: + print(f"Create release failed: {err}") + if release_resp is not None: + print(f"Response: {release_resp.text}") + break + + if release_resp is None or release_resp.status_code not in (200, 201): + print("Failed to create release on AtomGit, aborting.") + return + + print(f"Created release {tag} on AtomGit") + + for file_path in files: + upload_asset(access_token, tag, file_path) + + print("Sync is completed!") + + +def upload_asset(access_token, tag, file_path): + file_name = os.path.basename(file_path) + upload_url_api = ( + f"{ATOMGIT_API}/repos/{ATOMGIT_OWNER}/{ATOMGIT_REPO}" + f"/releases/{tag}/upload_url?file_name={file_name}" + ) + + for attempt in range(3): + try: + resp = requests.get( + upload_url_api, + headers={"Authorization": f"Bearer {access_token}"}, + timeout=30, + ) + resp.raise_for_status() + upload_info = resp.json() + + obs_url = upload_info["url"] + obs_headers = upload_info["headers"] + + with open(file_path, "rb") as f: + put_resp = requests.put( + obs_url, headers=obs_headers, data=f, timeout=120 + ) + + if put_resp.text.strip() == "success" or put_resp.status_code in (200, 201): + print(f"Uploaded {file_name}") + return + else: + print( + f"Upload {file_name} failed: {put_resp.status_code} {put_resp.text}" + ) + except requests.exceptions.RequestException as err: + print(f"Upload {file_name} attempt {attempt + 1} failed: {err}") + time.sleep(10) + + print(f"Failed to upload {file_name} after 3 attempts") + + +def get_abs_path(path): + return os.path.join(os.getcwd(), path) + + +get_github_latest_release() diff --git a/.github/workflows/sync-code-atomgit.yml b/.github/workflows/sync-code-atomgit.yml new file mode 100644 index 0000000..10982ec --- /dev/null +++ b/.github/workflows/sync-code-atomgit.yml @@ -0,0 +1,16 @@ +name: Sync Code to AtomGit + +on: + push: + branches: [master] + +jobs: + sync-code-to-atomgit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: adambirds/sync-github-to-gitlab-action@v1.1.0 + with: + destination_repository: git@atomgit.com:naiba/nezha-dashboard.git + destination_branch_name: master + destination_ssh_key: ${{ secrets.ATOMGIT_SSH_KEY }} diff --git a/.github/workflows/sync-release-atomgit.yml b/.github/workflows/sync-release-atomgit.yml new file mode 100644 index 0000000..cac88db --- /dev/null +++ b/.github/workflows/sync-release-atomgit.yml @@ -0,0 +1,17 @@ +name: Sync Release to AtomGit + +on: + workflow_dispatch: + +jobs: + sync-release-to-atomgit: + runs-on: ubuntu-latest + timeout-minutes: 120 + env: + ATOMGIT_PAT: ${{ secrets.ATOMGIT_PAT }} + steps: + - uses: actions/checkout@v4 + - name: Sync to AtomGit + run: | + pip3 install PyGitHub + python3 .github/sync_atomgit.py From 6d2a12b7a4f4cb7c2c2d38bc1d92257a39aa6ab6 Mon Sep 17 00:00:00 2001 From: naiba Date: Mon, 16 Feb 2026 13:42:26 +0800 Subject: [PATCH 32/45] fix: use Authorization header instead of access_token in request body for AtomGit API --- .github/sync_atomgit.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/sync_atomgit.py b/.github/sync_atomgit.py index fe82c31..06aac74 100644 --- a/.github/sync_atomgit.py +++ b/.github/sync_atomgit.py @@ -39,8 +39,8 @@ def sync_to_atomgit(tag, body, files): access_token = os.environ["ATOMGIT_PAT"] release_api_uri = f"{ATOMGIT_API}/repos/{ATOMGIT_OWNER}/{ATOMGIT_REPO}/releases" + auth_headers = {"Authorization": f"Bearer {access_token}"} release_data = { - "access_token": access_token, "tag_name": tag, "name": tag, "body": body, @@ -51,7 +51,9 @@ def sync_to_atomgit(tag, body, files): release_resp = None for attempt in range(3): try: - release_resp = requests.post(release_api_uri, json=release_data, timeout=30) + release_resp = requests.post( + release_api_uri, json=release_data, headers=auth_headers, timeout=30 + ) release_resp.raise_for_status() break except requests.exceptions.Timeout: From b6977b2d4592e26a24d35162d3c49c98b785a0ad Mon Sep 17 00:00:00 2001 From: naiba Date: Mon, 16 Feb 2026 15:31:46 +0800 Subject: [PATCH 33/45] feat: trigger AtomGit release sync on release --- .github/workflows/release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1ecd3b9..9426615 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -170,6 +170,7 @@ jobs: GH_DEBUG: api run: | gh workflow run sync-release.yml + gh workflow run sync-release-atomgit.yml release-docker: runs-on: ubuntu-latest From bef07c400c85ec52be9826e26210e70ff78e9d4c Mon Sep 17 00:00:00 2001 From: naiba Date: Thu, 19 Feb 2026 06:34:02 +0000 Subject: [PATCH 34/45] chore: bump admin-frontend to v2.0.4, nezha-dash-v1 to v2.0.1 --- service/singleton/frontend-templates.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/service/singleton/frontend-templates.yaml b/service/singleton/frontend-templates.yaml index 37ddd0b..bbd5cf4 100644 --- a/service/singleton/frontend-templates.yaml +++ b/service/singleton/frontend-templates.yaml @@ -2,14 +2,14 @@ name: "OfficialAdmin" repository: "https://github.com/nezhahq/admin-frontend" author: "nezhahq" - version: "v2.0.3" + version: "v2.0.4" is_admin: true is_official: true - path: "user-dist" name: "Official" repository: "https://github.com/hamster1963/nezha-dash-v1" author: "hamster1963" - version: "v2.0.0" + version: "v2.0.1" is_official: true - path: "nazhua-dist" name: "Nazhua" From 04553ca17f6a3bced318d444f3d40d6032cb1517 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Thu, 19 Feb 2026 07:35:24 +0100 Subject: [PATCH 35/45] Added translation using Weblate (Galician) (#1168) Co-authored-by: nlimeres --- .../translations/gl_ES/LC_MESSAGES/nezha.mo | Bin 0 -> 459 bytes .../translations/gl_ES/LC_MESSAGES/nezha.po | 312 ++++++++++++++++++ 2 files changed, 312 insertions(+) create mode 100644 pkg/i18n/translations/gl_ES/LC_MESSAGES/nezha.mo create mode 100644 pkg/i18n/translations/gl_ES/LC_MESSAGES/nezha.po diff --git a/pkg/i18n/translations/gl_ES/LC_MESSAGES/nezha.mo b/pkg/i18n/translations/gl_ES/LC_MESSAGES/nezha.mo new file mode 100644 index 0000000000000000000000000000000000000000..d3b53273b4fc060278a1b3ec5a7bce00d8171849 GIT binary patch literal 459 zcmYk2%TB{E5Jih%l_hHyMq&X1Clm=(+y)UyS|TNFm8QIQZZbGQsU6uaufJfy@9`}- zM52sz{E+XA?)CTf)>p^WhGECBXV^3x8Jg-DKF#B~*&EGYO{I8-Oye-ew;)w1I6)&? zm03wS+8@J5K@Y41Y9Td_6)SUmwPFgVf*@-;!3n&SO%wL127(>}FTwEy`yRTPkT^Vi zoQwl2qDtdbQm!hhg(PS=k0Lbo;s6bT*bAde&-asHHoGuV&Q_EGPJu4}IT;jM*NQk! zA(V#PeuagZ1-6jPsoMz3;f>pN#W_{QLQt7Ita8?K31XEMhjFk@+qI9aYr!>eWAeQ= zDKxxkr>f!Tt_kx55(hjJdC3_;y+x_5X|& eNA1z&XxByEzIBgBz(Ka(ao2`Hr*`+u>ihtdNQlb- literal 0 HcmV?d00001 diff --git a/pkg/i18n/translations/gl_ES/LC_MESSAGES/nezha.po b/pkg/i18n/translations/gl_ES/LC_MESSAGES/nezha.po new file mode 100644 index 0000000..3371523 --- /dev/null +++ b/pkg/i18n/translations/gl_ES/LC_MESSAGES/nezha.po @@ -0,0 +1,312 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-01-30 21:58+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: gl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#: cmd/dashboard/controller/alertrule.go:104 +#, c-format +msgid "alert id %d does not exist" +msgstr "" + +#: cmd/dashboard/controller/alertrule.go:108 +#: cmd/dashboard/controller/alertrule.go:156 +#: cmd/dashboard/controller/alertrule.go:176 +#: cmd/dashboard/controller/controller.go:226 +#: cmd/dashboard/controller/cron.go:58 cmd/dashboard/controller/cron.go:124 +#: cmd/dashboard/controller/cron.go:136 cmd/dashboard/controller/cron.go:195 +#: cmd/dashboard/controller/cron.go:224 cmd/dashboard/controller/ddns.go:131 +#: cmd/dashboard/controller/ddns.go:192 cmd/dashboard/controller/fm.go:43 +#: cmd/dashboard/controller/nat.go:59 cmd/dashboard/controller/nat.go:111 +#: cmd/dashboard/controller/nat.go:122 cmd/dashboard/controller/nat.go:162 +#: cmd/dashboard/controller/notification.go:112 +#: cmd/dashboard/controller/notification.go:166 +#: cmd/dashboard/controller/notification_group.go:76 +#: cmd/dashboard/controller/notification_group.go:152 +#: cmd/dashboard/controller/notification_group.go:164 +#: cmd/dashboard/controller/notification_group.go:233 +#: cmd/dashboard/controller/server.go:66 cmd/dashboard/controller/server.go:78 +#: cmd/dashboard/controller/server.go:137 +#: cmd/dashboard/controller/server.go:201 +#: cmd/dashboard/controller/server_group.go:75 +#: cmd/dashboard/controller/server_group.go:150 +#: cmd/dashboard/controller/server_group.go:229 +#: cmd/dashboard/controller/service.go:271 +#: cmd/dashboard/controller/service.go:342 +#: cmd/dashboard/controller/service.go:369 +#: cmd/dashboard/controller/terminal.go:41 +msgid "permission denied" +msgstr "" + +#: cmd/dashboard/controller/alertrule.go:184 +msgid "duration need to be at least 3" +msgstr "" + +#: cmd/dashboard/controller/alertrule.go:188 +msgid "cycle_interval need to be at least 1" +msgstr "" + +#: cmd/dashboard/controller/alertrule.go:191 +msgid "cycle_start is not set" +msgstr "" + +#: cmd/dashboard/controller/alertrule.go:194 +msgid "cycle_start is a future value" +msgstr "" + +#: cmd/dashboard/controller/alertrule.go:199 +msgid "need to configure at least a single rule" +msgstr "" + +#: cmd/dashboard/controller/controller.go:220 +#: cmd/dashboard/controller/oauth2.go:153 +#: cmd/dashboard/controller/server_group.go:162 +#: cmd/dashboard/controller/service.go:97 cmd/dashboard/controller/user.go:27 +#: cmd/dashboard/controller/user.go:63 +msgid "unauthorized" +msgstr "" + +#: cmd/dashboard/controller/controller.go:243 +msgid "database error" +msgstr "" + +#: cmd/dashboard/controller/cron.go:75 cmd/dashboard/controller/cron.go:149 +msgid "scheduled tasks cannot be triggered by alarms" +msgstr "" + +#: cmd/dashboard/controller/cron.go:132 cmd/dashboard/controller/cron.go:190 +#, c-format +msgid "task id %d does not exist" +msgstr "" + +#: cmd/dashboard/controller/ddns.go:57 cmd/dashboard/controller/ddns.go:122 +msgid "the retry count must be an integer between 1 and 10" +msgstr "" + +#: cmd/dashboard/controller/ddns.go:81 cmd/dashboard/controller/ddns.go:154 +msgid "error parsing %s: %v" +msgstr "" + +#: cmd/dashboard/controller/ddns.go:127 cmd/dashboard/controller/nat.go:118 +#, c-format +msgid "profile id %d does not exist" +msgstr "" + +#: cmd/dashboard/controller/fm.go:39 cmd/dashboard/controller/terminal.go:37 +msgid "server not found or not connected" +msgstr "" + +#: cmd/dashboard/controller/notification.go:69 +#: cmd/dashboard/controller/notification.go:131 +msgid "a test message" +msgstr "" + +#: cmd/dashboard/controller/notification.go:108 +#, c-format +msgid "notification id %d does not exist" +msgstr "" + +#: cmd/dashboard/controller/notification_group.go:94 +#: cmd/dashboard/controller/notification_group.go:175 +msgid "have invalid notification id" +msgstr "" + +#: cmd/dashboard/controller/notification_group.go:160 +#: cmd/dashboard/controller/server_group.go:158 +#, c-format +msgid "group id %d does not exist" +msgstr "" + +#: cmd/dashboard/controller/oauth2.go:42 cmd/dashboard/controller/oauth2.go:83 +msgid "provider is required" +msgstr "" + +#: cmd/dashboard/controller/oauth2.go:52 cmd/dashboard/controller/oauth2.go:87 +#: cmd/dashboard/controller/oauth2.go:132 +msgid "provider not found" +msgstr "" + +#: cmd/dashboard/controller/oauth2.go:100 +msgid "operation not permitted" +msgstr "" + +#: cmd/dashboard/controller/oauth2.go:138 +msgid "code is required" +msgstr "" + +#: cmd/dashboard/controller/oauth2.go:175 +msgid "oauth2 user not binded yet" +msgstr "" + +#: cmd/dashboard/controller/oauth2.go:217 +#: cmd/dashboard/controller/oauth2.go:223 +#: cmd/dashboard/controller/oauth2.go:228 +msgid "invalid state key" +msgstr "" + +#: cmd/dashboard/controller/server.go:74 +#, c-format +msgid "server id %d does not exist" +msgstr "" + +#: cmd/dashboard/controller/server.go:250 +msgid "operation timeout" +msgstr "" + +#: cmd/dashboard/controller/server.go:257 +msgid "get server config failed: %v" +msgstr "" + +#: cmd/dashboard/controller/server.go:261 +msgid "get server config failed" +msgstr "" + +#: cmd/dashboard/controller/server_group.go:92 +#: cmd/dashboard/controller/server_group.go:172 +msgid "have invalid server id" +msgstr "" + +#: cmd/dashboard/controller/service.go:90 +#: cmd/dashboard/controller/service.go:165 +msgid "server not found" +msgstr "" + +#: cmd/dashboard/controller/service.go:267 +#, c-format +msgid "service id %d does not exist" +msgstr "" + +#: cmd/dashboard/controller/user.go:68 +msgid "incorrect password" +msgstr "" + +#: cmd/dashboard/controller/user.go:82 +msgid "you don't have any oauth2 bindings" +msgstr "" + +#: cmd/dashboard/controller/user.go:131 +msgid "password length must be greater than 6" +msgstr "" + +#: cmd/dashboard/controller/user.go:134 +msgid "username can't be empty" +msgstr "" + +#: cmd/dashboard/controller/user.go:137 +msgid "invalid role" +msgstr "" + +#: cmd/dashboard/controller/user.go:176 +msgid "can't delete yourself" +msgstr "" + +#: service/rpc/io_stream.go:128 +msgid "timeout: no connection established" +msgstr "" + +#: service/rpc/io_stream.go:131 +msgid "timeout: user connection not established" +msgstr "" + +#: service/rpc/io_stream.go:134 +msgid "timeout: agent connection not established" +msgstr "" + +#: service/rpc/nezha.go:71 +msgid "Scheduled Task Executed Successfully" +msgstr "" + +#: service/rpc/nezha.go:75 +msgid "Scheduled Task Executed Failed" +msgstr "" + +#: service/rpc/nezha.go:274 +msgid "IP Changed" +msgstr "" + +#: service/singleton/alertsentinel.go:169 +msgid "Incident" +msgstr "" + +#: service/singleton/alertsentinel.go:179 +msgid "Resolved" +msgstr "" + +#: service/singleton/crontask.go:54 +msgid "Tasks failed to register: [" +msgstr "" + +#: service/singleton/crontask.go:61 +msgid "" +"] These tasks will not execute properly. Fix them in the admin dashboard." +msgstr "" + +#: service/singleton/crontask.go:144 service/singleton/crontask.go:169 +#, c-format +msgid "[Task failed] %s: server %s is offline and cannot execute the task" +msgstr "" + +#: service/singleton/servicesentinel.go:468 +#, c-format +msgid "[Latency] %s %2f > %2f, Reporter: %s" +msgstr "" + +#: service/singleton/servicesentinel.go:475 +#, c-format +msgid "[Latency] %s %2f < %2f, Reporter: %s" +msgstr "" + +#: service/singleton/servicesentinel.go:501 +#, c-format +msgid "[%s] %s Reporter: %s, Error: %s" +msgstr "" + +#: service/singleton/servicesentinel.go:544 +#, c-format +msgid "[TLS] Fetch cert info failed, Reporter: %s, Error: %s" +msgstr "" + +#: service/singleton/servicesentinel.go:584 +#, c-format +msgid "The TLS certificate will expire within seven days. Expiration time: %s" +msgstr "" + +#: service/singleton/servicesentinel.go:597 +#, c-format +msgid "" +"TLS certificate changed, old: issuer %s, expires at %s; new: issuer %s, " +"expires at %s" +msgstr "" + +#: service/singleton/servicesentinel.go:633 +msgid "No Data" +msgstr "" + +#: service/singleton/servicesentinel.go:635 +msgid "Good" +msgstr "" + +#: service/singleton/servicesentinel.go:637 +msgid "Low Availability" +msgstr "" + +#: service/singleton/servicesentinel.go:639 +msgid "Down" +msgstr "" + +#: service/singleton/user.go:60 +msgid "user id not specified" +msgstr "" From 1a0d915afa40827736c68dbcf05741fdc60e06b1 Mon Sep 17 00:00:00 2001 From: NikoCat233 Date: Fri, 27 Feb 2026 00:53:31 +0800 Subject: [PATCH 36/45] Filter hidden servers in guest server-group API --- cmd/dashboard/controller/server_group.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/cmd/dashboard/controller/server_group.go b/cmd/dashboard/controller/server_group.go index 67a17f1..bc491e8 100644 --- a/cmd/dashboard/controller/server_group.go +++ b/cmd/dashboard/controller/server_group.go @@ -26,12 +26,27 @@ func listServerGroup(c *gin.Context) ([]*model.ServerGroupResponseItem, error) { return nil, err } + _, isMember := c.Get(model.CtxKeyAuthorizedUser) + authorized := isMember + + visibleServerIDs := make(map[uint64]struct{}) + if !authorized { + for _, server := range singleton.ServerShared.GetSortedListForGuest() { + visibleServerIDs[server.ID] = struct{}{} + } + } + groupServers := make(map[uint64][]uint64, 0) var sgs []model.ServerGroupServer if err := singleton.DB.Find(&sgs).Error; err != nil { return nil, err } for _, s := range sgs { + if !authorized { + if _, ok := visibleServerIDs[s.ServerId]; !ok { + continue + } + } if _, ok := groupServers[s.ServerGroupId]; !ok { groupServers[s.ServerGroupId] = make([]uint64, 0) } From c48c63c8ad15761800defdc02b7f9dbd96b3fbf9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 28 Feb 2026 23:24:16 +0800 Subject: [PATCH 37/45] update contributors[no ci] --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index cb239f4..7904400 100644 --- a/README.md +++ b/README.md @@ -46,12 +46,12 @@ add your theme to [service/singleton/frontend-templates.yaml](service/singleton/ 奶爸 UUBulb Akkia -卖女孩的小火柴 +卖女孩的小火柴 nap0o 黑歌 xykt -MikoyChinese Weblate (bot) +MikoyChinese JackieSung4ev Kris Lemoe @@ -96,6 +96,7 @@ add your theme to [service/singleton/frontend-templates.yaml](service/singleton/ Tater Li Tao Chen Spetrum +NikoCat233 Nanjing Hopefun Network Technology Co. Ltd. Mingzhuo Yin Martijn Lindeman From be8ff11cae4522219c11ec5236d4a9b326c4e038 Mon Sep 17 00:00:00 2001 From: naiba Date: Sun, 1 Mar 2026 03:32:32 +0000 Subject: [PATCH 38/45] fix: upgrade CodeQL Action to v3 and generate swagger docs before build - Upgrade github/codeql-action from v2 to v3 (v1/v2 deprecated) - Upgrade actions/setup-go from v4 to v5 - Add swag init step before autobuild to generate cmd/dashboard/docs package, fixing "could not import: invalid package name" warning Co-authored-by: naiba/CloudCode --- .github/workflows/codeql-analysis.yml | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 656eecc..f2ce05d 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -41,13 +41,13 @@ jobs: uses: actions/checkout@v4 - name: Install Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: go-version-file: go.mod # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -55,10 +55,14 @@ jobs: # Prefix the list here with "+" to use these queries and those in the config file. # queries: ./path/to/local/query, your-org/your-repo/queries@main - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) + # Generate swagger docs before build (cmd/dashboard/docs is .gitignored) + - name: Generate swagger docs + run: | + go install github.com/swaggo/swag/cmd/swag@latest + swag init --pd -d . -g ./cmd/dashboard/main.go -o ./cmd/dashboard/docs --requiredByDefault + - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -72,4 +76,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 From 69ac37d98c4b8a256af868a7cccb594ef3b9a3d4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 1 Mar 2026 11:33:11 +0800 Subject: [PATCH 39/45] update contributors[no ci] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7904400..5f5c54a 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ add your theme to [service/singleton/frontend-templates.yaml](service/singleton/ ## Contributors -奶爸 +naiba UUBulb Akkia 卖女孩的小火柴 From d57d7b73b61e4f21e1272e840eab47713d535116 Mon Sep 17 00:00:00 2001 From: naiba Date: Sun, 1 Mar 2026 03:49:01 +0000 Subject: [PATCH 40/45] =?UTF-8?q?Fix:=20=E8=AE=BE=E7=BD=AECookie=20Secure?= =?UTF-8?q?=E5=B1=9E=E6=80=A7=E5=A2=9E=E5=BC=BA=E5=AE=89=E5=85=A8=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 根据运行环境动态设置Secure属性 - 开发环境兼容HTTP,生产环境强制HTTPS - 解决CodeQL扫描发现的安全漏洞 Co-authored-by: naiba/CloudCode --- cmd/dashboard/controller/oauth2.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cmd/dashboard/controller/oauth2.go b/cmd/dashboard/controller/oauth2.go index ebe7e50..2a418ad 100644 --- a/cmd/dashboard/controller/oauth2.go +++ b/cmd/dashboard/controller/oauth2.go @@ -65,8 +65,7 @@ func oauth2redirect(c *gin.Context) (*model.Oauth2LoginResponse, error) { RedirectURL: redirectURL, }, cache.DefaultExpiration) - url := o2conf.AuthCodeURL(state, oauth2.AccessTypeOnline) - c.SetCookie("nz-o2s", stateKey, 60*5, "", "", false, false) + c.SetCookie("nz-o2s", stateKey, 60*5, "", "", c.Request.URL.Scheme == "https" || c.Request.TLS != nil, false) return &model.Oauth2LoginResponse{Redirect: url}, nil } From 589563e744e0639bbff83f2719227897bd06d5d5 Mon Sep 17 00:00:00 2001 From: naiba Date: Sun, 1 Mar 2026 11:50:56 +0800 Subject: [PATCH 41/45] chore: upgrade frontend --- service/singleton/frontend-templates.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/service/singleton/frontend-templates.yaml b/service/singleton/frontend-templates.yaml index bbd5cf4..9b6faf0 100644 --- a/service/singleton/frontend-templates.yaml +++ b/service/singleton/frontend-templates.yaml @@ -2,14 +2,14 @@ name: "OfficialAdmin" repository: "https://github.com/nezhahq/admin-frontend" author: "nezhahq" - version: "v2.0.4" + version: "v2.0.5" is_admin: true is_official: true - path: "user-dist" name: "Official" - repository: "https://github.com/hamster1963/nezha-dash-v1" + repository: "https://github.com/hamster1963/nezha-dash-v2" author: "hamster1963" - version: "v2.0.1" + version: "v2.0.2" is_official: true - path: "nazhua-dist" name: "Nazhua" From a5d4537a8c9be2d58e4f312d3d6acb9e635e84ed Mon Sep 17 00:00:00 2001 From: naiba Date: Sun, 1 Mar 2026 04:05:42 +0000 Subject: [PATCH 42/45] =?UTF-8?q?fix:=20=E6=81=A2=E5=A4=8D=E8=A2=AB?= =?UTF-8?q?=E8=AF=AF=E5=88=A0=E7=9A=84=20AuthCodeURL=20=E8=B0=83=E7=94=A8?= =?UTF-8?q?=EF=BC=8C=E4=BF=AE=E5=A4=8D=E7=BC=96=E8=AF=91=E5=A4=B1=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 上次提交误删了 url := o2conf.AuthCodeURL(...) 导致编译失败。 同时添加注释说明 Cookie Secure 属性动态设置的原因。 Co-authored-by: naiba/CloudCode --- cmd/dashboard/controller/oauth2.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cmd/dashboard/controller/oauth2.go b/cmd/dashboard/controller/oauth2.go index 2a418ad..acb0218 100644 --- a/cmd/dashboard/controller/oauth2.go +++ b/cmd/dashboard/controller/oauth2.go @@ -65,6 +65,8 @@ func oauth2redirect(c *gin.Context) (*model.Oauth2LoginResponse, error) { RedirectURL: redirectURL, }, cache.DefaultExpiration) + url := o2conf.AuthCodeURL(state, oauth2.AccessTypeOnline) + // CodeQL go/cookie-secure-not-set: 根据请求协议动态设置 Secure 属性,避免 HTTP 环境下 Cookie 无法使用 c.SetCookie("nz-o2s", stateKey, 60*5, "", "", c.Request.URL.Scheme == "https" || c.Request.TLS != nil, false) return &model.Oauth2LoginResponse{Redirect: url}, nil From 91a636ce25bf9cd52608a0aa95c5a3e6db9a1bdb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 02:27:20 +0000 Subject: [PATCH 43/45] chore(deps): bump google.golang.org/grpc from 1.76.0 to 1.79.3 Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.76.0 to 1.79.3. - [Release notes](https://github.com/grpc/grpc-go/releases) - [Commits](https://github.com/grpc/grpc-go/compare/v1.76.0...v1.79.3) --- updated-dependencies: - dependency-name: google.golang.org/grpc dependency-version: 1.79.3 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- go.mod | 22 ++++++++++----------- go.sum | 60 +++++++++++++++++++++++++++++----------------------------- 2 files changed, 41 insertions(+), 41 deletions(-) diff --git a/go.mod b/go.mod index d604d2a..7fae4ae 100644 --- a/go.mod +++ b/go.mod @@ -30,14 +30,13 @@ require ( github.com/stretchr/testify v1.11.1 github.com/swaggo/files v1.0.1 github.com/swaggo/gin-swagger v1.6.0 - github.com/swaggo/swag v1.16.4 github.com/tidwall/gjson v1.18.0 - golang.org/x/crypto v0.45.0 + golang.org/x/crypto v0.46.0 golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 - golang.org/x/net v0.47.0 - golang.org/x/oauth2 v0.32.0 - golang.org/x/sync v0.18.0 - google.golang.org/grpc v1.76.0 + golang.org/x/net v0.48.0 + golang.org/x/oauth2 v0.34.0 + golang.org/x/sync v0.19.0 + google.golang.org/grpc v1.79.3 google.golang.org/protobuf v1.36.10 gorm.io/driver/sqlite v1.5.7 gorm.io/gorm v1.26.0 @@ -85,6 +84,7 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/swaggo/swag v1.16.4 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect @@ -97,11 +97,11 @@ require ( github.com/valyala/quicktemplate v1.8.0 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect golang.org/x/arch v0.16.0 // indirect - golang.org/x/mod v0.29.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/text v0.31.0 // indirect + golang.org/x/mod v0.30.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/text v0.32.0 // indirect golang.org/x/time v0.14.0 // indirect - golang.org/x/tools v0.38.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251007200510-49b9836ed3ff // indirect + golang.org/x/tools v0.39.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 22350d6..bbcb524 100644 --- a/go.sum +++ b/go.sum @@ -205,39 +205,39 @@ github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfS github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= -go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= -go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= -go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= -go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= -go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= -go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= -go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= -go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= -go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= golang.org/x/arch v0.16.0 h1:foMtLTdyOmIniqWCHjY6+JxuC54XP1fDwx4N0ASyW+U= golang.org/x/arch v0.16.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 h1:TQwNpfvNkxAVlItJf6Cr5JTsVZoC/Sj7K3OZv2Pc14A= golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= -golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= +golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= -golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -245,8 +245,8 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -254,22 +254,22 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= -golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= +golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251007200510-49b9836ed3ff h1:A90eA31Wq6HOMIQlLfzFwzqGKBTuaVztYu/g8sn+8Zc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251007200510-49b9836ed3ff/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 9acffc11f6c2d82fd38ef6afab344b35349bd4dc Mon Sep 17 00:00:00 2001 From: naiba Date: Fri, 20 Mar 2026 11:23:03 +0000 Subject: [PATCH 44/45] chore: bump Go to 1.26, update dependencies and frontend templates Co-authored-by: naiba/CloudCode --- .github/workflows/release.yml | 4 +- .github/workflows/test.yml | 2 +- go.mod | 104 +++++++++++-------- go.sum | 119 ++++++++++++++++++++++ service/singleton/frontend-templates.yaml | 4 +- 5 files changed, 183 insertions(+), 50 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9426615..ab166ad 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,7 +23,7 @@ jobs: name: Build artifacts runs-on: ubuntu-latest container: - image: goreleaser/goreleaser-cross:v1.25 + image: goreleaser/goreleaser-cross:v1.26 steps: - name: Install dependencies run: | @@ -49,7 +49,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: "1.25.x" + go-version: "1.26.x" - name: generate swagger docs run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a668c60..f109ee0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,7 +27,7 @@ jobs: - uses: actions/setup-go@v5 with: - go-version: "1.25.x" + go-version: "1.26.x" - name: generate swagger docs run: | diff --git a/go.mod b/go.mod index 7fae4ae..8913abb 100644 --- a/go.mod +++ b/go.mod @@ -1,46 +1,46 @@ module github.com/nezhahq/nezha -go 1.25.6 +go 1.26 require ( github.com/VictoriaMetrics/VictoriaMetrics v1.134.0 github.com/appleboy/gin-jwt/v2 v2.10.3 - github.com/dustinkirkland/golang-petname v0.0.0-20240428194347-eebcea082ee0 + github.com/dustinkirkland/golang-petname v0.0.0-20260215035315-f0c533e9ce9b github.com/gin-contrib/pprof v1.5.3 - github.com/gin-gonic/gin v1.10.0 - github.com/go-viper/mapstructure/v2 v2.4.0 - github.com/goccy/go-json v0.10.5 + github.com/gin-gonic/gin v1.12.0 + github.com/go-viper/mapstructure/v2 v2.5.0 + github.com/goccy/go-json v0.10.6 github.com/gorilla/websocket v1.5.3 github.com/hashicorp/go-uuid v1.0.3 github.com/jinzhu/copier v0.4.0 github.com/knadh/koanf/maps v0.1.2 github.com/knadh/koanf/providers/env v1.1.0 - github.com/knadh/koanf/providers/file v1.2.0 - github.com/knadh/koanf/v2 v2.3.0 - github.com/leonelquinteros/gotext v1.7.1 - github.com/libdns/cloudflare v0.2.1 - github.com/libdns/he v1.1.1 - github.com/libdns/libdns v1.0.0 - github.com/miekg/dns v1.1.65 + github.com/knadh/koanf/providers/file v1.2.1 + github.com/knadh/koanf/v2 v2.3.3 + github.com/leonelquinteros/gotext v1.7.2 + github.com/libdns/cloudflare v0.2.2 + github.com/libdns/he v1.2.1 + github.com/libdns/libdns v1.1.1 + github.com/miekg/dns v1.1.72 github.com/nezhahq/libdns-tencentcloud v0.0.0-20250501081622-bd293105845a - github.com/ory/graceful v0.1.3 + github.com/ory/graceful v0.2.0 github.com/oschwald/maxminddb-golang v1.13.1 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/robfig/cron/v3 v3.0.1 github.com/stretchr/testify v1.11.1 github.com/swaggo/files v1.0.1 - github.com/swaggo/gin-swagger v1.6.0 + github.com/swaggo/gin-swagger v1.6.1 github.com/tidwall/gjson v1.18.0 - golang.org/x/crypto v0.46.0 - golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 - golang.org/x/net v0.48.0 - golang.org/x/oauth2 v0.34.0 - golang.org/x/sync v0.19.0 + golang.org/x/crypto v0.49.0 + golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 + golang.org/x/net v0.52.0 + golang.org/x/oauth2 v0.36.0 + golang.org/x/sync v0.20.0 google.golang.org/grpc v1.79.3 - google.golang.org/protobuf v1.36.10 - gorm.io/driver/sqlite v1.5.7 - gorm.io/gorm v1.26.0 - sigs.k8s.io/yaml v1.4.0 + google.golang.org/protobuf v1.36.11 + gorm.io/driver/sqlite v1.6.0 + gorm.io/gorm v1.31.1 + sigs.k8s.io/yaml v1.6.0 ) require ( @@ -49,21 +49,30 @@ require ( github.com/VictoriaMetrics/fastcache v1.13.2 // indirect github.com/VictoriaMetrics/metrics v1.40.2 // indirect github.com/VictoriaMetrics/metricsql v0.84.8 // indirect - github.com/bytedance/sonic v1.13.2 // indirect - github.com/bytedance/sonic/loader v0.2.4 // indirect + github.com/bytedance/gopkg v0.1.4 // indirect + github.com/bytedance/sonic v1.15.0 // indirect + github.com/bytedance/sonic/loader v0.5.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/cloudwego/base64x v0.1.5 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.9 // indirect + github.com/gabriel-vasile/mimetype v1.4.13 // indirect github.com/gin-contrib/sse v1.1.0 // indirect - github.com/go-openapi/jsonpointer v0.21.0 // indirect - github.com/go-openapi/jsonreference v0.21.0 // indirect - github.com/go-openapi/spec v0.21.0 // indirect - github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-openapi/jsonpointer v0.22.5 // indirect + github.com/go-openapi/jsonreference v0.21.5 // indirect + github.com/go-openapi/spec v0.22.4 // indirect + github.com/go-openapi/swag v0.25.5 // indirect + github.com/go-openapi/swag/conv v0.25.5 // indirect + github.com/go-openapi/swag/jsonname v0.25.5 // indirect + github.com/go-openapi/swag/jsonutils v0.25.5 // indirect + github.com/go-openapi/swag/loading v0.25.5 // indirect + github.com/go-openapi/swag/stringutils v0.25.5 // indirect + github.com/go-openapi/swag/typeutils v0.25.5 // indirect + github.com/go-openapi/swag/yamlutils v0.25.5 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.26.0 // indirect + github.com/go-playground/validator/v10 v10.30.1 // indirect + github.com/goccy/go-yaml v1.19.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect @@ -71,11 +80,11 @@ require ( github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.18.0 // indirect - github.com/klauspost/cpuid/v2 v2.2.10 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect - github.com/mailru/easyjson v0.9.0 // indirect + github.com/mailru/easyjson v0.9.2 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-sqlite3 v1.14.24 // indirect + github.com/mattn/go-sqlite3 v1.14.37 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -83,25 +92,30 @@ require ( github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/quic-go/quic-go v0.59.0 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect - github.com/swaggo/swag v1.16.4 // indirect - github.com/tidwall/match v1.1.1 // indirect + github.com/swaggo/swag v1.16.6 // indirect + github.com/tidwall/match v1.2.0 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.2.12 // indirect + github.com/ugorji/go/codec v1.3.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fastrand v1.1.0 // indirect github.com/valyala/gozstd v1.24.0 // indirect github.com/valyala/histogram v1.2.0 // indirect github.com/valyala/quicktemplate v1.8.0 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect - golang.org/x/arch v0.16.0 // indirect - golang.org/x/mod v0.30.0 // indirect - golang.org/x/sys v0.39.0 // indirect - golang.org/x/text v0.32.0 // indirect - golang.org/x/time v0.14.0 // indirect - golang.org/x/tools v0.39.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect + go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/arch v0.25.0 // indirect + golang.org/x/mod v0.34.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/text v0.35.0 // indirect + golang.org/x/time v0.15.0 // indirect + golang.org/x/tools v0.43.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index bbcb524..3338fcb 100644 --- a/go.sum +++ b/go.sum @@ -16,15 +16,23 @@ github.com/appleboy/gin-jwt/v2 v2.10.3 h1:KNcPC+XPRNpuoBh+j+rgs5bQxN+SwG/0tHbIqp github.com/appleboy/gin-jwt/v2 v2.10.3/go.mod h1:LDUaQ8mF2W6LyXIbd5wqlV2SFebuyYs4RDwqMNgpsp8= github.com/appleboy/gofight/v2 v2.1.2 h1:VOy3jow4vIK8BRQJoC/I9muxyYlJ2yb9ht2hZoS3rf4= github.com/appleboy/gofight/v2 v2.1.2/go.mod h1:frW+U1QZEdDgixycTj4CygQ48yLTUhplt43+Wczp3rw= +github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM= +github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4= github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ= github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= +github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= +github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= +github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -32,10 +40,14 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustinkirkland/golang-petname v0.0.0-20240428194347-eebcea082ee0 h1:aYo8nnk3ojoQkP5iErif5Xxv0Mo0Ga/FR5+ffl/7+Nk= github.com/dustinkirkland/golang-petname v0.0.0-20240428194347-eebcea082ee0/go.mod h1:8AuBTZBRSFqEYBPYULd+NN474/zZBLP+6WeT5S9xlAc= +github.com/dustinkirkland/golang-petname v0.0.0-20260215035315-f0c533e9ce9b h1:qZ21OofI7zneC9dOEqul4FmIWz/YjJJMrf6fL7jrFYQ= +github.com/dustinkirkland/golang-petname v0.0.0-20260215035315-f0c533e9ce9b/go.mod h1:8AuBTZBRSFqEYBPYULd+NN474/zZBLP+6WeT5S9xlAc= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= +github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= +github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= github.com/gin-contrib/pprof v1.5.3 h1:Bj5SxJ3kQDVez/s/+f9+meedJIqLS+xlkIVDe/lcvgM= @@ -44,18 +56,42 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8= +github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA= +github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0= github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE= +github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw= github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= +github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ= +github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-openapi/swag v0.25.5 h1:pNkwbUEeGwMtcgxDr+2GBPAk4kT+kJ+AaB+TMKAg+TU= +github.com/go-openapi/swag v0.25.5/go.mod h1:B3RT6l8q7X803JRxa2e59tHOiZlX1t8viplOcs9CwTA= +github.com/go-openapi/swag/conv v0.25.5 h1:wAXBYEXJjoKwE5+vc9YHhpQOFj2JYBMF2DUi+tGu97g= +github.com/go-openapi/swag/conv v0.25.5/go.mod h1:CuJ1eWvh1c4ORKx7unQnFGyvBbNlRKbnRyAvDvzWA4k= +github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo= +github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU= +github.com/go-openapi/swag/jsonutils v0.25.5 h1:XUZF8awQr75MXeC+/iaw5usY/iM7nXPDwdG3Jbl9vYo= +github.com/go-openapi/swag/jsonutils v0.25.5/go.mod h1:48FXUaz8YsDAA9s5AnaUvAmry1UcLcNVWUjY42XkrN4= +github.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU= +github.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g= +github.com/go-openapi/swag/stringutils v0.25.5 h1:NVkoDOA8YBgtAR/zvCx5rhJKtZF3IzXcDdwOsYzrB6M= +github.com/go-openapi/swag/stringutils v0.25.5/go.mod h1:PKK8EZdu4QJq8iezt17HM8RXnLAzY7gW0O1KKarrZII= +github.com/go-openapi/swag/typeutils v0.25.5 h1:EFJ+PCga2HfHGdo8s8VJXEVbeXRCYwzzr9u4rJk7L7E= +github.com/go-openapi/swag/typeutils v0.25.5/go.mod h1:itmFmScAYE1bSD8C4rS0W+0InZUBrB2xSPbWt6DLGuc= +github.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ= +github.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -64,10 +100,18 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= +github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= +github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= +github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= @@ -99,14 +143,20 @@ github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYW github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= github.com/knadh/koanf/providers/env v1.1.0 h1:U2VXPY0f+CsNDkvdsG8GcsnK4ah85WwWyJgef9oQMSc= github.com/knadh/koanf/providers/env v1.1.0/go.mod h1:QhHHHZ87h9JxJAn2czdEl6pdkNnDh/JS1Vtsyt65hTY= github.com/knadh/koanf/providers/file v1.2.0 h1:hrUJ6Y9YOA49aNu/RSYzOTFlqzXSCpmYIDXI7OJU6+U= github.com/knadh/koanf/providers/file v1.2.0/go.mod h1:bp1PM5f83Q+TOUu10J/0ApLBd9uIzg+n9UgthfY+nRA= +github.com/knadh/koanf/providers/file v1.2.1 h1:bEWbtQwYrA+W2DtdBrQWyXqJaJSG3KrP3AESOJYp9wM= +github.com/knadh/koanf/providers/file v1.2.1/go.mod h1:bp1PM5f83Q+TOUu10J/0ApLBd9uIzg+n9UgthfY+nRA= github.com/knadh/koanf/v2 v2.3.0 h1:Qg076dDRFHvqnKG97ZEsi9TAg2/nFTa9hCdcSa1lvlM= github.com/knadh/koanf/v2 v2.3.0/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28= +github.com/knadh/koanf/v2 v2.3.3 h1:jLJC8XCRfLC7n4F+ZKKdBsbq1bfXTpuFhf4L7t94D94= +github.com/knadh/koanf/v2 v2.3.3/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -116,20 +166,34 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/leonelquinteros/gotext v1.7.1 h1:/JNPeE3lY5JeVYv2+KBpz39994W3W9fmZCGq3eO9Ri8= github.com/leonelquinteros/gotext v1.7.1/go.mod h1:I0WoFDn9u2D3VbPnnDPT8mzZu0iSXG8iih+AH2fHHqg= +github.com/leonelquinteros/gotext v1.7.2 h1:bDPndU8nt+/kRo1m4l/1OXiiy2v7Z7dfPQ9+YP7G1Mc= +github.com/leonelquinteros/gotext v1.7.2/go.mod h1:9/haCkm5P7Jay1sxKDGJ5WIg4zkz8oZKw4ekNpALob8= github.com/libdns/cloudflare v0.2.1 h1:E8aoP5o79AU47t1XyzCgSecST3GvWv/nC3ycibg0t+o= github.com/libdns/cloudflare v0.2.1/go.mod h1:Aq4IXdjalB6mD0ELvKqJiIGim8zSC6mlIshRPMOAb5w= +github.com/libdns/cloudflare v0.2.2 h1:XWHv+C1dDcApqazlh08Q6pjytYLgR2a+Y3xrXFu0vsI= +github.com/libdns/cloudflare v0.2.2/go.mod h1:w9uTmRCDlAoafAsTPnn2nJ0XHK/eaUMh86DUk8BWi60= github.com/libdns/he v1.1.1 h1:5Dm3BBcRsm1A9gKxQyFTJq/VK+jJf/1b2maSSaBVe+E= github.com/libdns/he v1.1.1/go.mod h1:cGV1fwTcDefVfgM6VsTQ6kpUrBd1GdudmP8duLHR5D8= +github.com/libdns/he v1.2.1 h1:cjTZlxM5wv2lBPmtxsQCqMgmXMqTnmR4eLqUVwEkqis= +github.com/libdns/he v1.2.1/go.mod h1:SWTm80gn+7sUASGsQbRHayenoW4QIw/iGmsrkDzFghM= github.com/libdns/libdns v1.0.0 h1:IvYaz07JNz6jUQ4h/fv2R4sVnRnm77J/aOuC9B+TQTA= github.com/libdns/libdns v1.0.0/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= +github.com/libdns/libdns v1.1.1 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U= +github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/mailru/easyjson v0.9.2 h1:dX8U45hQsZpxd80nLvDGihsQ/OxlvTkVUXH2r/8cb2M= +github.com/mailru/easyjson v0.9.2/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-sqlite3 v1.14.37 h1:3DOZp4cXis1cUIpCfXLtmlGolNLp2VEqhiB/PARNBIg= +github.com/mattn/go-sqlite3 v1.14.37/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/miekg/dns v1.1.65 h1:0+tIPHzUW0GCge7IiK3guGP57VAw7hoPDfApjkMD1Fc= github.com/miekg/dns v1.1.65/go.mod h1:Dzw9769uoKVaLuODMDZz9M6ynFU6Em65csPuoi8G0ck= +github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= +github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= @@ -144,6 +208,8 @@ github.com/nezhahq/libdns-tencentcloud v0.0.0-20250501081622-bd293105845a h1:wCB github.com/nezhahq/libdns-tencentcloud v0.0.0-20250501081622-bd293105845a/go.mod h1:CUbNGv2k24auuhwa7MMVXl45fniBMm2eVi57FlWLcIs= github.com/ory/graceful v0.1.3 h1:FaeXcHZh168WzS+bqruqWEw/HgXWLdNv2nJ+fbhxbhc= github.com/ory/graceful v0.1.3/go.mod h1:4zFz687IAF7oNHHiB586U4iL+/4aV09o/PYLE34t2bA= +github.com/ory/graceful v0.2.0 h1:5wqyCvKZG2+sorOA9rmQJm9aZP+RJdTrVHVDcvMOSuQ= +github.com/ory/graceful v0.2.0/go.mod h1:hg2iCy+LCWOXahBZ+NQa4dk8J2govyQD79rrqrgMyY8= github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE= github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= @@ -155,6 +221,10 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= +github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= @@ -162,25 +232,34 @@ github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M= github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo= +github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs7cY= +github.com/swaggo/gin-swagger v1.6.1/go.mod h1:LQ+hJStHakCWRiK/YNYtJOu4mR2FP+pxLnILT/qNiTw= github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A= github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg= +github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= +github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM= +github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= @@ -190,6 +269,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= +github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G8= @@ -203,6 +284,8 @@ github.com/valyala/quicktemplate v1.8.0/go.mod h1:qIqW8/igXt8fdrUln5kOSb+KWMaJ4Y github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= +go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= @@ -215,29 +298,47 @@ go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2W go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/arch v0.16.0 h1:foMtLTdyOmIniqWCHjY6+JxuC54XP1fDwx4N0ASyW+U= golang.org/x/arch v0.16.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE= +golang.org/x/arch v0.25.0 h1:qnk6Ksugpi5Bz32947rkUgDt9/s5qvqDPl/gBKdMJLE= +golang.org/x/arch v0.25.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 h1:TQwNpfvNkxAVlItJf6Cr5JTsVZoC/Sj7K3OZv2Pc14A= golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk= +golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA= +golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -247,6 +348,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -256,22 +359,32 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= +golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7 h1:ndE4FoJqsIceKP2oYSnUZqhTdYufCYYkqwtFzfrhI7w= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -281,8 +394,14 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I= gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4= +gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= +gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= gorm.io/gorm v1.26.0 h1:9lqQVPG5aNNS6AyHdRiwScAVnXHg/L/Srzx55G5fOgs= gorm.io/gorm v1.26.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= +gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= +gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/service/singleton/frontend-templates.yaml b/service/singleton/frontend-templates.yaml index 9b6faf0..e879fc9 100644 --- a/service/singleton/frontend-templates.yaml +++ b/service/singleton/frontend-templates.yaml @@ -9,13 +9,13 @@ name: "Official" repository: "https://github.com/hamster1963/nezha-dash-v2" author: "hamster1963" - version: "v2.0.2" + version: "v2.0.3" is_official: true - path: "nazhua-dist" name: "Nazhua" repository: "https://github.com/hi2shark/nazhua" author: "hi2hi" - version: "v0.9.0" + version: "v0.9.1" - path: "nezha-ascii-dist" name: "Nezha-ASCII" repository: "https://github.com/hamster1963/nezha-ascii" From 50dc8e660326b9f22990898142c58b7a5312b42a Mon Sep 17 00:00:00 2001 From: naiba Date: Sun, 5 Apr 2026 10:00:11 +0800 Subject: [PATCH 45/45] chore: upgrade frontend --- service/singleton/frontend-templates.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/singleton/frontend-templates.yaml b/service/singleton/frontend-templates.yaml index e879fc9..f6b4fa9 100644 --- a/service/singleton/frontend-templates.yaml +++ b/service/singleton/frontend-templates.yaml @@ -2,7 +2,7 @@ name: "OfficialAdmin" repository: "https://github.com/nezhahq/admin-frontend" author: "nezhahq" - version: "v2.0.5" + version: "v2.0.6" is_admin: true is_official: true - path: "user-dist"