diff --git a/README.md b/README.md
index d84eb831..9cdf081a 100644
--- a/README.md
+++ b/README.md
@@ -119,7 +119,7 @@ add your theme to [service/singleton/frontend-templates.yaml](service/singleton/
-
+
@@ -136,6 +136,7 @@ add your theme to [service/singleton/frontend-templates.yaml](service/singleton/
+
@@ -145,7 +146,6 @@ add your theme to [service/singleton/frontend-templates.yaml](service/singleton/
-
@@ -155,6 +155,7 @@ add your theme to [service/singleton/frontend-templates.yaml](service/singleton/
+
## Special Thanks
diff --git a/cmd/dashboard/controller/jwt.go b/cmd/dashboard/controller/jwt.go
index db0865e0..9fd7ff0c 100644
--- a/cmd/dashboard/controller/jwt.go
+++ b/cmd/dashboard/controller/jwt.go
@@ -152,7 +152,7 @@ func identityHandler() func(c *gin.Context) any {
return nil
}
currentIP := c.GetString(model.CtxKeyRealIPStr)
- if sess.IP != currentIP {
+ if !singleton.Conf.JWTIPChangeAllowed() && sess.IP != currentIP {
c.Set(model.CtxKeyIsIPMismatch, true)
return nil
}
diff --git a/cmd/dashboard/controller/jwt_session_test.go b/cmd/dashboard/controller/jwt_session_test.go
index 29b5e15e..0f169cb3 100644
--- a/cmd/dashboard/controller/jwt_session_test.go
+++ b/cmd/dashboard/controller/jwt_session_test.go
@@ -193,6 +193,61 @@ func TestIdentityHandlerFlagsIPMismatch(t *testing.T) {
assert.True(t, verify.GetBool(model.CtxKeyIsIPMismatch))
}
+func TestIdentityHandlerAllowsIPMismatchWhenConfigured(t *testing.T) {
+ cleanup := setupJWTSessionTest(t)
+ defer cleanup()
+ singleton.Conf.SetJWTIPChangeAllowed(true)
+
+ ctx := newCtxForUser(0, "1.2.3.4", "ua")
+ user := model.User{Common: model.Common{ID: 100}, TokenVersion: 7}
+ claims, err := issueJWTSession(ctx, &user, 1)
+ require.NoError(t, err)
+
+ verify := newCtxForUser(0, "9.9.9.9", "ua")
+ verify.Set("JWT_PAYLOAD", jwt.MapClaims{
+ jwtClaimUserID: claims[jwtClaimUserID],
+ jwtClaimKeyID: claims[jwtClaimKeyID],
+ })
+
+ identity := identityHandler()(verify)
+ require.NotNil(t, identity, "IP mismatch must be accepted when allow_jwt_ip_change is enabled")
+ assert.False(t, verify.GetBool(model.CtxKeyIsIPMismatch))
+}
+
+func TestIdentityHandlerConcurrentIPChangeSetting(t *testing.T) {
+ cleanup := setupJWTSessionTest(t)
+ defer cleanup()
+
+ ctx := newCtxForUser(0, "1.2.3.4", "ua")
+ user := model.User{Common: model.Common{ID: 100}, TokenVersion: 7}
+ claims, err := issueJWTSession(ctx, &user, 1)
+ require.NoError(t, err)
+
+ stop, done := make(chan struct{}), make(chan struct{})
+ go func() {
+ defer close(done)
+ for {
+ select {
+ case <-stop:
+ return
+ default:
+ singleton.Conf.SetJWTIPChangeAllowed(true)
+ singleton.Conf.SetJWTIPChangeAllowed(false)
+ }
+ }
+ }()
+ defer func() {
+ close(stop)
+ <-done
+ }()
+
+ for i := 0; i < 100; i++ {
+ verify := newCtxForUser(0, "1.2.3.4", "ua")
+ verify.Set("JWT_PAYLOAD", jwt.MapClaims(claims))
+ require.NotNil(t, identityHandler()(verify), "matching IP must pass during setting updates")
+ }
+}
+
func TestIdentityHandlerRejectsUnknownKeyID(t *testing.T) {
cleanup := setupJWTSessionTest(t)
defer cleanup()
diff --git a/cmd/dashboard/controller/setting.go b/cmd/dashboard/controller/setting.go
index aac58a56..0b6b70e6 100644
--- a/cmd/dashboard/controller/setting.go
+++ b/cmd/dashboard/controller/setting.go
@@ -97,6 +97,9 @@ func updateConfig(c *gin.Context) (any, error) {
singleton.Conf.EnableIPChangeNotification = sf.EnableIPChangeNotification
singleton.Conf.EnablePlainIPInNotification = sf.EnablePlainIPInNotification
+ if sf.AllowJWTIPChange != nil {
+ singleton.Conf.SetJWTIPChangeAllowed(*sf.AllowJWTIPChange)
+ }
singleton.Conf.Cover = sf.Cover
singleton.Conf.InstallHost = sf.InstallHost
singleton.Conf.DashboardHost = sf.DashboardHost
@@ -160,6 +163,12 @@ func fireMCPKillSwitch() {
// the current config to avoid accidentally tripping the kill switch on
// partial PATCH calls that omit enable_mcp.
func resolveSettingEnableMCP(formValue *bool, current bool) bool {
+ return resolveOptionalBool(formValue, current)
+}
+
+// resolveOptionalBool preserves a setting for partial PATCH requests when its
+// boolean field is absent, while still allowing an explicit false value.
+func resolveOptionalBool(formValue *bool, current bool) bool {
if formValue == nil {
return current
}
diff --git a/cmd/dashboard/controller/setting_jwt_ip_change_test.go b/cmd/dashboard/controller/setting_jwt_ip_change_test.go
new file mode 100644
index 00000000..a3f5229b
--- /dev/null
+++ b/cmd/dashboard/controller/setting_jwt_ip_change_test.go
@@ -0,0 +1,26 @@
+package controller
+
+import (
+ "encoding/json"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/nezhahq/nezha/model"
+)
+
+func TestSettingForm_AllowJWTIPChangePreservesAbsentAndDecodesExplicitValues(t *testing.T) {
+ var omitted model.SettingForm
+ require.NoError(t, json.Unmarshal([]byte(`{"site_name":"X"}`), &omitted))
+ require.Nil(t, omitted.AllowJWTIPChange)
+
+ var enabled model.SettingForm
+ require.NoError(t, json.Unmarshal([]byte(`{"allow_jwt_ip_change":true}`), &enabled))
+ require.NotNil(t, enabled.AllowJWTIPChange)
+ require.True(t, *enabled.AllowJWTIPChange)
+
+ var disabled model.SettingForm
+ require.NoError(t, json.Unmarshal([]byte(`{"allow_jwt_ip_change":false}`), &disabled))
+ require.NotNil(t, disabled.AllowJWTIPChange)
+ require.False(t, *disabled.AllowJWTIPChange)
+}
diff --git a/model/config.go b/model/config.go
index 3e766b2f..fde4bd19 100644
--- a/model/config.go
+++ b/model/config.go
@@ -61,6 +61,9 @@ type ConfigDashboard struct {
AdminTemplate string `koanf:"admin_template" json:"admin_template,omitempty"`
EnablePlainIPInNotification bool `koanf:"enable_plain_ip_in_notification" json:"enable_plain_ip_in_notification,omitempty"` // 通知信息IP不打码
+ // AllowJWTIPChange disables the IP binding check for browser login sessions.
+ // Keep this disabled by default so a stolen JWT remains bound to its issuing IP.
+ AllowJWTIPChange bool `koanf:"allow_jwt_ip_change" json:"allow_jwt_ip_change,omitempty"`
EnableMCP bool `koanf:"enable_mcp" json:"enable_mcp,omitempty"` // 是否启用 MCP 入口(默认关闭;启用前请审视 PAT scope/whitelist)
@@ -103,6 +106,10 @@ type Config struct {
// SettingResponse 按值拷贝 ConfigDashboard 触发 copylocks。
mcpEnabled atomic.Bool `koanf:"-" json:"-" yaml:"-"`
+ // jwtIPChangeAllowed mirrors AllowJWTIPChange for concurrent authentication
+ // and setting updates. Keep atomic fields outside ConfigDashboard.
+ jwtIPChangeAllowed atomic.Bool `koanf:"-" json:"-" yaml:"-"`
+
// oauth2 配置
Oauth2 map[string]*Oauth2Config `koanf:"oauth2" json:"oauth2,omitempty"`
@@ -225,6 +232,8 @@ func (c *Config) Read(path string, frontendTemplates []FrontendTemplate) error {
c.JWTTimeout = 1
}
+ c.jwtIPChangeAllowed.Store(c.AllowJWTIPChange)
+
if c.AgentSecretKey == "" {
c.AgentSecretKey, err = utils.GenerateRandomString(32)
if err != nil {
@@ -252,6 +261,17 @@ func (c *Config) SetMCPEnabled(v bool) {
c.mcpEnabled.Store(v)
}
+// JWTIPChangeAllowed reports whether JWT sessions may change IP concurrently.
+func (c *Config) JWTIPChangeAllowed() bool {
+ return c.jwtIPChangeAllowed.Load()
+}
+
+// SetJWTIPChangeAllowed updates only the atomic mirror. save() synchronizes
+// the public field for persistence; authentication must use the accessor.
+func (c *Config) SetJWTIPChangeAllowed(v bool) {
+ c.jwtIPChangeAllowed.Store(v)
+}
+
// Save 保存配置文件
func (c *Config) Save() error {
return c.save()
@@ -320,6 +340,7 @@ func (c *Config) patchYAMLField(key string, value any) error {
func (c *Config) save() error {
c.EnableMCP = c.mcpEnabled.Load()
+ c.AllowJWTIPChange = c.jwtIPChangeAllowed.Load()
data, err := yaml.Marshal(c)
if err != nil {
return err
diff --git a/model/config_test.go b/model/config_test.go
index 4242c199..d5d4b6cd 100644
--- a/model/config_test.go
+++ b/model/config_test.go
@@ -24,6 +24,7 @@ func TestReadConfig(t *testing.T) {
{"user_template", c.UserTemplate, c.UserTemplate == "user-dist"},
{"admin_template", c.AdminTemplate, c.AdminTemplate == "admin-dist"},
{"agent_secret_key", c.AgentSecretKey, c.AgentSecretKey != ""},
+ {"allow_jwt_ip_change", c.AllowJWTIPChange, !c.AllowJWTIPChange},
}
for _, field := range testFields {
@@ -36,7 +37,7 @@ func TestReadConfig(t *testing.T) {
})
t.Run("ReadFile", func(t *testing.T) {
- const testCfg = "jwt_secret_key: test\nuser_template: um\nadmin_template: am\nagent_secret_key: none\nsite_name: lowkick"
+ const testCfg = "jwt_secret_key: test\nuser_template: um\nadmin_template: am\nagent_secret_key: none\nsite_name: lowkick\nallow_jwt_ip_change: true"
var testFrontendTemplates = []FrontendTemplate{
{Path: "um"},
@@ -59,6 +60,7 @@ func TestReadConfig(t *testing.T) {
{"admin_template", c.AdminTemplate, c.AdminTemplate == "am"},
{"agent_secret_key", c.AgentSecretKey, c.AgentSecretKey == "none"},
{"site_name", c.SiteName, c.SiteName == "lowkick"},
+ {"allow_jwt_ip_change", c.AllowJWTIPChange, c.AllowJWTIPChange},
}
for _, field := range testFields {
diff --git a/model/host.go b/model/host.go
index f054798b..18f4e3da 100644
--- a/model/host.go
+++ b/model/host.go
@@ -35,9 +35,28 @@ type HostState struct {
ProcessCount uint64 `json:"process_count,omitempty"`
Temperatures []SensorTemperature `json:"temperatures,omitempty"`
GPU []float64 `json:"gpu,omitempty"`
+ GPUs []GPUStat `json:"gpus,omitempty"`
+}
+
+// GPUStat carries per-card figures, index-aligned with Host.GPU. Memory is in
+// MiB and stays zero for vendors that do not report it, so MemoryTotal == 0
+// means "unknown" rather than "no memory".
+type GPUStat struct {
+ Utilization float64 `json:"utilization"`
+ MemoryUsed uint64 `json:"memory_used,omitempty"`
+ MemoryTotal uint64 `json:"memory_total,omitempty"`
}
func (s *HostState) PB() *pb.State {
+ gs := make([]*pb.State_GPU, 0, len(s.GPUs))
+ for _, g := range s.GPUs {
+ gs = append(gs, &pb.State_GPU{
+ Utilization: g.Utilization,
+ MemoryUsed: g.MemoryUsed,
+ MemoryTotal: g.MemoryTotal,
+ })
+ }
+
var ts []*pb.State_SensorTemperature
for _, t := range s.Temperatures {
ts = append(ts, &pb.State_SensorTemperature{
@@ -64,10 +83,20 @@ func (s *HostState) PB() *pb.State {
ProcessCount: s.ProcessCount,
Temperatures: ts,
Gpu: s.GPU,
+ Gpus: gs,
}
}
func PB2State(s *pb.State) HostState {
+ var gs []GPUStat
+ for _, g := range s.GetGpus() {
+ gs = append(gs, GPUStat{
+ Utilization: g.GetUtilization(),
+ MemoryUsed: g.GetMemoryUsed(),
+ MemoryTotal: g.GetMemoryTotal(),
+ })
+ }
+
var ts []SensorTemperature
for _, t := range s.GetTemperatures() {
ts = append(ts, SensorTemperature{
@@ -93,6 +122,7 @@ func PB2State(s *pb.State) HostState {
UdpConnCount: s.GetUdpConnCount(),
ProcessCount: s.GetProcessCount(),
Temperatures: ts,
+ GPUs: gs,
GPU: s.GetGpu(),
}
}
diff --git a/model/jwt_ip_change_atomic_test.go b/model/jwt_ip_change_atomic_test.go
new file mode 100644
index 00000000..bbb2b343
--- /dev/null
+++ b/model/jwt_ip_change_atomic_test.go
@@ -0,0 +1,56 @@
+package model
+
+import (
+ "path/filepath"
+ "sync"
+ "testing"
+)
+
+func TestJWTIPChangeAllowedPersistence(t *testing.T) {
+ c := &Config{}
+ if c.JWTIPChangeAllowed() {
+ t.Fatal("zero-value Config must bind JWT sessions to their IP")
+ }
+ // An absent agent secret makes Read save the config during initialization.
+ path := newTempConfig(t, "jwt_secret_key: test\nallow_jwt_ip_change: true\n")
+ if err := c.Read(path, nil); err != nil {
+ t.Fatal(err)
+ }
+ if !c.JWTIPChangeAllowed() {
+ t.Fatal("Read must initialize the atomic mirror before saving")
+ }
+ for _, want := range []bool{true, false} {
+ c.SetJWTIPChangeAllowed(want)
+ if err := c.Save(); err != nil {
+ t.Fatal(err)
+ }
+ reloaded := &Config{}
+ if err := reloaded.Read(path, nil); err != nil {
+ t.Fatal(err)
+ }
+ if c.AllowJWTIPChange != want || reloaded.JWTIPChangeAllowed() != want {
+ t.Fatalf("saved and reloaded setting must be %v", want)
+ }
+ }
+}
+
+func TestJWTIPChangeAllowedConcurrentAccess(t *testing.T) {
+ c := &Config{filePath: filepath.Join(t.TempDir(), "config.yaml")}
+ var wg sync.WaitGroup
+ wg.Go(func() {
+ for i := 0; i < 10000; i++ {
+ c.SetJWTIPChangeAllowed(i%2 == 0)
+ }
+ })
+ wg.Go(func() {
+ for i := 0; i < 10000; i++ {
+ _ = c.JWTIPChangeAllowed()
+ }
+ })
+ for i := 0; i < 20; i++ {
+ if err := c.Save(); err != nil {
+ t.Error(err)
+ }
+ }
+ wg.Wait()
+}
diff --git a/model/setting_api.go b/model/setting_api.go
index 590afb9e..bd0510a0 100644
--- a/model/setting_api.go
+++ b/model/setting_api.go
@@ -19,6 +19,7 @@ type SettingForm struct {
AgentTLS bool `json:"tls,omitempty" validate:"optional"`
EnableIPChangeNotification bool `json:"enable_ip_change_notification,omitempty" validate:"optional"`
EnablePlainIPInNotification bool `json:"enable_plain_ip_in_notification,omitempty" validate:"optional"`
+ AllowJWTIPChange *bool `json:"allow_jwt_ip_change,omitempty" validate:"optional"`
EnableMCP *bool `json:"enable_mcp,omitempty" validate:"optional"`
}
diff --git a/pkg/tsdb/disk_guard_test.go b/pkg/tsdb/disk_guard_test.go
new file mode 100644
index 00000000..2fa2109a
--- /dev/null
+++ b/pkg/tsdb/disk_guard_test.go
@@ -0,0 +1,124 @@
+package tsdb
+
+import (
+ "path/filepath"
+ "sync/atomic"
+ "testing"
+ "time"
+
+ "github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+type testReadOnlyChecker struct {
+ value atomic.Bool
+}
+
+func (c *testReadOnlyChecker) IsReadOnly() bool {
+ return c.value.Load()
+}
+
+func newDiskGuardTestDB(t *testing.T) *TSDB {
+ t.Helper()
+ db, err := Open(&Config{
+ DataPath: filepath.Join(t.TempDir(), "tsdb"),
+ RetentionDays: 1,
+ MinFreeDiskSpaceGB: 1,
+ DedupInterval: time.Second,
+ })
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, db.Close()) })
+ return db
+}
+
+func bufferedRows(w *bufferedWriter) int {
+ w.mu.Lock()
+ defer w.mu.Unlock()
+ return len(w.buffer)
+}
+
+func TestReadOnlyStorageDropsWritesAndKeepsQueriesAvailable(t *testing.T) {
+ db := newDiskGuardTestDB(t)
+ checker := &testReadOnlyChecker{}
+ db.readOnly = checker
+ originalAddRows := db.addRowsFn
+ var addRowsCalls atomic.Int32
+ db.addRowsFn = func(rows []storage.MetricRow, precisionBits uint8) {
+ addRowsCalls.Add(1)
+ originalAddRows(rows, precisionBits)
+ }
+
+ firstTimestamp := time.Now().Add(-time.Minute)
+ require.NoError(t, db.WriteServerMetrics(&ServerMetrics{
+ ServerID: 1,
+ Timestamp: firstTimestamp,
+ CPU: 10,
+ }))
+ db.Flush()
+ require.Equal(t, int32(1), addRowsCalls.Load())
+
+ before, err := db.QueryServerMetrics(1, MetricServerCPU, Period1Day)
+ require.NoError(t, err)
+ require.NotEmpty(t, before)
+
+ checker.value.Store(true)
+ require.NoError(t, db.WriteServerMetrics(&ServerMetrics{
+ ServerID: 1,
+ Timestamp: firstTimestamp.Add(2 * time.Second),
+ CPU: 20,
+ }))
+ assert.True(t, db.WritesPaused())
+ assert.Zero(t, bufferedRows(db.writer), "read-only writes must not accumulate in memory")
+ assert.Equal(t, int32(1), addRowsCalls.Load(), "read-only samples must not reach VictoriaMetrics")
+
+ afterDrop, err := db.QueryServerMetrics(1, MetricServerCPU, Period1Day)
+ require.NoError(t, err)
+ assert.Equal(t, before, afterDrop, "read-only mode must preserve existing query access")
+
+ checker.value.Store(false)
+ require.NoError(t, db.WriteServerMetrics(&ServerMetrics{
+ ServerID: 1,
+ Timestamp: firstTimestamp.Add(4 * time.Second),
+ CPU: 30,
+ }))
+ db.Flush()
+ assert.False(t, db.WritesPaused())
+ assert.Equal(t, int32(2), addRowsCalls.Load(), "writes must resume after storage leaves read-only mode")
+}
+
+func TestDiskFullAddRowsPanicPausesWrites(t *testing.T) {
+ db := newDiskGuardTestDB(t)
+ db.addRowsFn = func([]storage.MetricRow, uint8) {
+ panic("write data: no space left on device")
+ }
+
+ require.NoError(t, db.WriteServerMetrics(&ServerMetrics{
+ ServerID: 1,
+ Timestamp: time.Now(),
+ CPU: 10,
+ }))
+ assert.NotPanics(t, db.Flush)
+ assert.True(t, db.WritesPaused())
+
+ require.NoError(t, db.WriteServerMetrics(&ServerMetrics{
+ ServerID: 1,
+ Timestamp: time.Now().Add(time.Second),
+ CPU: 20,
+ }))
+ assert.Zero(t, bufferedRows(db.writer))
+}
+
+func TestNonDiskStoragePanicIsNotHidden(t *testing.T) {
+ db := newDiskGuardTestDB(t)
+ db.addRowsFn = func([]storage.MetricRow, uint8) {
+ panic("storage invariant failed")
+ }
+
+ require.NoError(t, db.WriteServerMetrics(&ServerMetrics{
+ ServerID: 1,
+ Timestamp: time.Now(),
+ CPU: 10,
+ }))
+ assert.PanicsWithValue(t, "storage invariant failed", db.Flush)
+}
diff --git a/pkg/tsdb/maintenance.go b/pkg/tsdb/maintenance.go
index fe9c73e1..4f2c4a66 100644
--- a/pkg/tsdb/maintenance.go
+++ b/pkg/tsdb/maintenance.go
@@ -10,8 +10,11 @@ func (db *TSDB) Maintenance() {
if db.closed {
return
}
+ if !db.acceptsWrites() {
+ return
+ }
log.Println("NEZHA>> TSDB starting maintenance (flush)...")
- db.storage.DebugFlush()
+ db.debugFlushSafely()
log.Println("NEZHA>> TSDB maintenance completed")
}
diff --git a/pkg/tsdb/tsdb.go b/pkg/tsdb/tsdb.go
index d3627b61..9ae4a16b 100644
--- a/pkg/tsdb/tsdb.go
+++ b/pkg/tsdb/tsdb.go
@@ -1,15 +1,26 @@
package tsdb
import (
+ "errors"
"fmt"
"log"
"path/filepath"
+ "strings"
"sync"
+ "sync/atomic"
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
)
+// ErrDiskFull is returned when VictoriaMetrics cannot initialize because the
+// filesystem containing the TSDB has run out of space.
+var ErrDiskFull = errors.New("TSDB disk is full")
+
+type readOnlyChecker interface {
+ IsReadOnly() bool
+}
+
// TSDB 封装 VictoriaMetrics 存储
type TSDB struct {
storage *storage.Storage
@@ -18,6 +29,13 @@ type TSDB struct {
closed bool
writer *bufferedWriter
+
+ readOnly readOnlyChecker
+ addRowsFn func([]storage.MetricRow, uint8)
+ debugFlushFn func()
+
+ readOnlyObserved atomic.Bool
+ diskFullPaused atomic.Bool
}
// InitGlobalSettings 初始化 VictoriaMetrics 包级别的全局设置。
@@ -35,7 +53,18 @@ func InitGlobalSettings(config *Config) {
}
// Open 打开或创建 TSDB 存储
-func Open(config *Config) (*TSDB, error) {
+func Open(config *Config) (db *TSDB, err error) {
+ defer func() {
+ if recovered := recover(); recovered != nil {
+ if diskErr := diskFullPanicError(recovered); diskErr != nil {
+ db = nil
+ err = diskErr
+ return
+ }
+ panic(recovered)
+ }
+ }()
+
if config == nil {
config = DefaultConfig()
}
@@ -59,9 +88,12 @@ func Open(config *Config) (*TSDB, error) {
stor := storage.MustOpenStorage(dataPath, opts)
- db := &TSDB{
- storage: stor,
- config: config,
+ db = &TSDB{
+ storage: stor,
+ config: config,
+ readOnly: stor,
+ addRowsFn: stor.AddRows,
+ debugFlushFn: stor.DebugFlush,
}
db.writer = newBufferedWriter(db, config.WriteBufferSize, config.WriteBufferFlushInterval)
@@ -72,6 +104,86 @@ func Open(config *Config) (*TSDB, error) {
return db, nil
}
+func diskFullPanicError(recovered any) error {
+ message := strings.ToLower(fmt.Sprint(recovered))
+ for _, marker := range []string{
+ "no space left on device",
+ "disk quota exceeded",
+ "not enough space on the disk",
+ } {
+ if strings.Contains(message, marker) {
+ return fmt.Errorf("%w: %v", ErrDiskFull, recovered)
+ }
+ }
+ return nil
+}
+
+// WritesPaused reports whether new samples are currently being discarded.
+// Queries remain available while VictoriaMetrics is in read-only mode.
+func (db *TSDB) WritesPaused() bool {
+ if db.diskFullPaused.Load() {
+ return true
+ }
+ return db.readOnly != nil && db.readOnly.IsReadOnly()
+}
+
+func (db *TSDB) acceptsWrites() bool {
+ if db.diskFullPaused.Load() {
+ return false
+ }
+
+ readOnly := db.readOnly != nil && db.readOnly.IsReadOnly()
+ if readOnly {
+ if db.readOnlyObserved.CompareAndSwap(false, true) {
+ log.Println("NEZHA>> TSDB writes paused because free disk space is below the configured limit; dashboard, queries, and alerts remain available")
+ }
+ return false
+ }
+
+ if db.readOnlyObserved.CompareAndSwap(true, false) {
+ log.Println("NEZHA>> TSDB writes resumed after free disk space recovered")
+ }
+ return true
+}
+
+func (db *TSDB) pauseWritesAfterDiskFull(recovered any) {
+ if db.diskFullPaused.CompareAndSwap(false, true) {
+ log.Printf("NEZHA>> TSDB disk is full; disabling TSDB writes until dashboard restart while keeping dashboard, queries, and alerts running: %v", recovered)
+ }
+}
+
+func (db *TSDB) addRowsSafely(rows []storage.MetricRow) {
+ if len(rows) == 0 || !db.acceptsWrites() {
+ return
+ }
+
+ defer func() {
+ if recovered := recover(); recovered != nil {
+ if diskFullPanicError(recovered) == nil {
+ panic(recovered)
+ }
+ db.pauseWritesAfterDiskFull(recovered)
+ }
+ }()
+ db.addRowsFn(rows, 64)
+}
+
+func (db *TSDB) debugFlushSafely() {
+ if !db.acceptsWrites() {
+ return
+ }
+
+ defer func() {
+ if recovered := recover(); recovered != nil {
+ if diskFullPanicError(recovered) == nil {
+ panic(recovered)
+ }
+ db.pauseWritesAfterDiskFull(recovered)
+ }
+ }()
+ db.debugFlushFn()
+}
+
// Close 关闭 TSDB 存储
func (db *TSDB) Close() error {
db.mu.Lock()
@@ -110,8 +222,14 @@ func (db *TSDB) IsClosed() bool {
// Flush 强制刷盘(主要用于测试)
func (db *TSDB) Flush() {
+ db.mu.RLock()
+ defer db.mu.RUnlock()
+ if db.closed {
+ return
+ }
+
if db.writer != nil {
db.writer.flush()
}
- db.storage.DebugFlush()
+ db.debugFlushSafely()
}
diff --git a/pkg/tsdb/writer.go b/pkg/tsdb/writer.go
index 7896c094..d6471e2e 100644
--- a/pkg/tsdb/writer.go
+++ b/pkg/tsdb/writer.go
@@ -47,18 +47,29 @@ func (w *bufferedWriter) flushLoop() {
}
func (w *bufferedWriter) write(rows []storage.MetricRow) {
+ if !w.db.acceptsWrites() {
+ w.discard()
+ return
+ }
+
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)
+ w.db.addRowsSafely(rows)
return
}
w.mu.Unlock()
}
+func (w *bufferedWriter) discard() {
+ w.mu.Lock()
+ w.buffer = make([]storage.MetricRow, 0, w.maxSize)
+ w.mu.Unlock()
+}
+
func (w *bufferedWriter) flush() {
w.mu.Lock()
if len(w.buffer) == 0 {
@@ -69,7 +80,7 @@ func (w *bufferedWriter) flush() {
w.buffer = make([]storage.MetricRow, 0, w.maxSize)
w.mu.Unlock()
- w.db.storage.AddRows(rows, 64)
+ w.db.addRowsSafely(rows)
}
func (w *bufferedWriter) stop() {
@@ -171,7 +182,7 @@ func (db *TSDB) WriteServerMetrics(m *ServerMetrics) error {
if db.writer != nil {
db.writer.write(rows)
} else {
- db.storage.AddRows(rows, 64)
+ db.addRowsSafely(rows)
}
return nil
}
@@ -200,7 +211,7 @@ func (db *TSDB) WriteServiceMetrics(m *ServiceMetrics) error {
if db.writer != nil {
db.writer.write(rows)
} else {
- db.storage.AddRows(rows, 64)
+ db.addRowsSafely(rows)
}
return nil
}
@@ -265,7 +276,7 @@ func (db *TSDB) WriteBatchServerMetrics(metrics []*ServerMetrics) error {
if db.writer != nil {
db.writer.write(rows)
} else {
- db.storage.AddRows(rows, 64)
+ db.addRowsSafely(rows)
}
return nil
}
@@ -295,7 +306,7 @@ func (db *TSDB) WriteBatchServiceMetrics(metrics []*ServiceMetrics) error {
if db.writer != nil {
db.writer.write(rows)
} else {
- db.storage.AddRows(rows, 64)
+ db.addRowsSafely(rows)
}
return nil
}
diff --git a/proto/nezha.pb.go b/proto/nezha.pb.go
index 2a6b84af..16c73f50 100644
--- a/proto/nezha.pb.go
+++ b/proto/nezha.pb.go
@@ -169,6 +169,7 @@ type State struct {
ProcessCount uint64 `protobuf:"varint,15,opt,name=process_count,json=processCount,proto3" json:"process_count,omitempty"`
Temperatures []*State_SensorTemperature `protobuf:"bytes,16,rep,name=temperatures,proto3" json:"temperatures,omitempty"`
Gpu []float64 `protobuf:"fixed64,17,rep,packed,name=gpu,proto3" json:"gpu,omitempty"`
+ Gpus []*State_GPU `protobuf:"bytes,18,rep,name=gpus,proto3" json:"gpus,omitempty"`
}
func (x *State) Reset() {
@@ -322,6 +323,79 @@ func (x *State) GetGpu() []float64 {
return nil
}
+func (x *State) GetGpus() []*State_GPU {
+ if x != nil {
+ return x.Gpus
+ }
+ return nil
+}
+
+// State_GPU carries per-card figures. Index-aligned with Host.gpu, matching
+// the existing `gpu` field it supersedes; that field stays populated so older
+// dashboards keep working.
+type State_GPU struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Utilization float64 `protobuf:"fixed64,1,opt,name=utilization,proto3" json:"utilization,omitempty"`
+ MemoryUsed uint64 `protobuf:"varint,2,opt,name=memory_used,json=memoryUsed,proto3" json:"memory_used,omitempty"`
+ MemoryTotal uint64 `protobuf:"varint,3,opt,name=memory_total,json=memoryTotal,proto3" json:"memory_total,omitempty"`
+}
+
+func (x *State_GPU) Reset() {
+ *x = State_GPU{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_proto_nezha_proto_msgTypes[2]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *State_GPU) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*State_GPU) ProtoMessage() {}
+
+func (x *State_GPU) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_nezha_proto_msgTypes[2]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use State_GPU.ProtoReflect.Descriptor instead.
+func (*State_GPU) Descriptor() ([]byte, []int) {
+ return file_proto_nezha_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *State_GPU) GetUtilization() float64 {
+ if x != nil {
+ return x.Utilization
+ }
+ return 0
+}
+
+func (x *State_GPU) GetMemoryUsed() uint64 {
+ if x != nil {
+ return x.MemoryUsed
+ }
+ return 0
+}
+
+func (x *State_GPU) GetMemoryTotal() uint64 {
+ if x != nil {
+ return x.MemoryTotal
+ }
+ return 0
+}
+
type State_SensorTemperature struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -334,7 +408,7 @@ type State_SensorTemperature struct {
func (x *State_SensorTemperature) Reset() {
*x = State_SensorTemperature{}
if protoimpl.UnsafeEnabled {
- mi := &file_proto_nezha_proto_msgTypes[2]
+ mi := &file_proto_nezha_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -347,7 +421,7 @@ func (x *State_SensorTemperature) String() string {
func (*State_SensorTemperature) ProtoMessage() {}
func (x *State_SensorTemperature) ProtoReflect() protoreflect.Message {
- mi := &file_proto_nezha_proto_msgTypes[2]
+ mi := &file_proto_nezha_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -360,7 +434,7 @@ func (x *State_SensorTemperature) ProtoReflect() protoreflect.Message {
// Deprecated: Use State_SensorTemperature.ProtoReflect.Descriptor instead.
func (*State_SensorTemperature) Descriptor() ([]byte, []int) {
- return file_proto_nezha_proto_rawDescGZIP(), []int{2}
+ return file_proto_nezha_proto_rawDescGZIP(), []int{3}
}
func (x *State_SensorTemperature) GetName() string {
@@ -390,7 +464,7 @@ type Task struct {
func (x *Task) Reset() {
*x = Task{}
if protoimpl.UnsafeEnabled {
- mi := &file_proto_nezha_proto_msgTypes[3]
+ mi := &file_proto_nezha_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -403,7 +477,7 @@ func (x *Task) String() string {
func (*Task) ProtoMessage() {}
func (x *Task) ProtoReflect() protoreflect.Message {
- mi := &file_proto_nezha_proto_msgTypes[3]
+ mi := &file_proto_nezha_proto_msgTypes[4]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -416,7 +490,7 @@ func (x *Task) ProtoReflect() protoreflect.Message {
// Deprecated: Use Task.ProtoReflect.Descriptor instead.
func (*Task) Descriptor() ([]byte, []int) {
- return file_proto_nezha_proto_rawDescGZIP(), []int{3}
+ return file_proto_nezha_proto_rawDescGZIP(), []int{4}
}
func (x *Task) GetId() uint64 {
@@ -455,7 +529,7 @@ type TaskResult struct {
func (x *TaskResult) Reset() {
*x = TaskResult{}
if protoimpl.UnsafeEnabled {
- mi := &file_proto_nezha_proto_msgTypes[4]
+ mi := &file_proto_nezha_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -468,7 +542,7 @@ func (x *TaskResult) String() string {
func (*TaskResult) ProtoMessage() {}
func (x *TaskResult) ProtoReflect() protoreflect.Message {
- mi := &file_proto_nezha_proto_msgTypes[4]
+ mi := &file_proto_nezha_proto_msgTypes[5]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -481,7 +555,7 @@ func (x *TaskResult) ProtoReflect() protoreflect.Message {
// Deprecated: Use TaskResult.ProtoReflect.Descriptor instead.
func (*TaskResult) Descriptor() ([]byte, []int) {
- return file_proto_nezha_proto_rawDescGZIP(), []int{4}
+ return file_proto_nezha_proto_rawDescGZIP(), []int{5}
}
func (x *TaskResult) GetId() uint64 {
@@ -530,7 +604,7 @@ type Receipt struct {
func (x *Receipt) Reset() {
*x = Receipt{}
if protoimpl.UnsafeEnabled {
- mi := &file_proto_nezha_proto_msgTypes[5]
+ mi := &file_proto_nezha_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -543,7 +617,7 @@ func (x *Receipt) String() string {
func (*Receipt) ProtoMessage() {}
func (x *Receipt) ProtoReflect() protoreflect.Message {
- mi := &file_proto_nezha_proto_msgTypes[5]
+ mi := &file_proto_nezha_proto_msgTypes[6]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -556,7 +630,7 @@ func (x *Receipt) ProtoReflect() protoreflect.Message {
// Deprecated: Use Receipt.ProtoReflect.Descriptor instead.
func (*Receipt) Descriptor() ([]byte, []int) {
- return file_proto_nezha_proto_rawDescGZIP(), []int{5}
+ return file_proto_nezha_proto_rawDescGZIP(), []int{6}
}
func (x *Receipt) GetProced() bool {
@@ -577,7 +651,7 @@ type Uint64Receipt struct {
func (x *Uint64Receipt) Reset() {
*x = Uint64Receipt{}
if protoimpl.UnsafeEnabled {
- mi := &file_proto_nezha_proto_msgTypes[6]
+ mi := &file_proto_nezha_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -590,7 +664,7 @@ func (x *Uint64Receipt) String() string {
func (*Uint64Receipt) ProtoMessage() {}
func (x *Uint64Receipt) ProtoReflect() protoreflect.Message {
- mi := &file_proto_nezha_proto_msgTypes[6]
+ mi := &file_proto_nezha_proto_msgTypes[7]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -603,7 +677,7 @@ func (x *Uint64Receipt) ProtoReflect() protoreflect.Message {
// Deprecated: Use Uint64Receipt.ProtoReflect.Descriptor instead.
func (*Uint64Receipt) Descriptor() ([]byte, []int) {
- return file_proto_nezha_proto_rawDescGZIP(), []int{6}
+ return file_proto_nezha_proto_rawDescGZIP(), []int{7}
}
func (x *Uint64Receipt) GetData() uint64 {
@@ -624,7 +698,7 @@ type IOStreamData struct {
func (x *IOStreamData) Reset() {
*x = IOStreamData{}
if protoimpl.UnsafeEnabled {
- mi := &file_proto_nezha_proto_msgTypes[7]
+ mi := &file_proto_nezha_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -637,7 +711,7 @@ func (x *IOStreamData) String() string {
func (*IOStreamData) ProtoMessage() {}
func (x *IOStreamData) ProtoReflect() protoreflect.Message {
- mi := &file_proto_nezha_proto_msgTypes[7]
+ mi := &file_proto_nezha_proto_msgTypes[8]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -650,7 +724,7 @@ func (x *IOStreamData) ProtoReflect() protoreflect.Message {
// Deprecated: Use IOStreamData.ProtoReflect.Descriptor instead.
func (*IOStreamData) Descriptor() ([]byte, []int) {
- return file_proto_nezha_proto_rawDescGZIP(), []int{7}
+ return file_proto_nezha_proto_rawDescGZIP(), []int{8}
}
func (x *IOStreamData) GetData() []byte {
@@ -674,7 +748,7 @@ type GeoIP struct {
func (x *GeoIP) Reset() {
*x = GeoIP{}
if protoimpl.UnsafeEnabled {
- mi := &file_proto_nezha_proto_msgTypes[8]
+ mi := &file_proto_nezha_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -687,7 +761,7 @@ func (x *GeoIP) String() string {
func (*GeoIP) ProtoMessage() {}
func (x *GeoIP) ProtoReflect() protoreflect.Message {
- mi := &file_proto_nezha_proto_msgTypes[8]
+ mi := &file_proto_nezha_proto_msgTypes[9]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -700,7 +774,7 @@ func (x *GeoIP) ProtoReflect() protoreflect.Message {
// Deprecated: Use GeoIP.ProtoReflect.Descriptor instead.
func (*GeoIP) Descriptor() ([]byte, []int) {
- return file_proto_nezha_proto_rawDescGZIP(), []int{8}
+ return file_proto_nezha_proto_rawDescGZIP(), []int{9}
}
func (x *GeoIP) GetUse6() bool {
@@ -743,7 +817,7 @@ type IP struct {
func (x *IP) Reset() {
*x = IP{}
if protoimpl.UnsafeEnabled {
- mi := &file_proto_nezha_proto_msgTypes[9]
+ mi := &file_proto_nezha_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -756,7 +830,7 @@ func (x *IP) String() string {
func (*IP) ProtoMessage() {}
func (x *IP) ProtoReflect() protoreflect.Message {
- mi := &file_proto_nezha_proto_msgTypes[9]
+ mi := &file_proto_nezha_proto_msgTypes[10]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -769,7 +843,7 @@ func (x *IP) ProtoReflect() protoreflect.Message {
// Deprecated: Use IP.ProtoReflect.Descriptor instead.
func (*IP) Descriptor() ([]byte, []int) {
- return file_proto_nezha_proto_rawDescGZIP(), []int{9}
+ return file_proto_nezha_proto_rawDescGZIP(), []int{10}
}
func (x *IP) GetIpv4() string {
@@ -810,7 +884,7 @@ var file_proto_nezha_proto_rawDesc = []byte{
0x18, 0x09, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x62, 0x6f, 0x6f, 0x74, 0x54, 0x69, 0x6d, 0x65,
0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28,
0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x10, 0x0a, 0x03, 0x67, 0x70,
- 0x75, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x09, 0x52, 0x03, 0x67, 0x70, 0x75, 0x22, 0xa9, 0x04, 0x0a,
+ 0x75, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x09, 0x52, 0x03, 0x67, 0x70, 0x75, 0x22, 0xcf, 0x04, 0x0a,
0x05, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x63, 0x70, 0x75, 0x18, 0x01, 0x20,
0x01, 0x28, 0x01, 0x52, 0x03, 0x63, 0x70, 0x75, 0x12, 0x19, 0x0a, 0x08, 0x6d, 0x65, 0x6d, 0x5f,
0x75, 0x73, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x6d, 0x65, 0x6d, 0x55,
@@ -845,65 +919,74 @@ var file_proto_nezha_proto_rawDesc = []byte{
0x2e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x5f, 0x53, 0x65, 0x6e, 0x73, 0x6f, 0x72, 0x54, 0x65, 0x6d,
0x70, 0x65, 0x72, 0x61, 0x74, 0x75, 0x72, 0x65, 0x52, 0x0c, 0x74, 0x65, 0x6d, 0x70, 0x65, 0x72,
0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x67, 0x70, 0x75, 0x18, 0x11, 0x20,
- 0x03, 0x28, 0x01, 0x52, 0x03, 0x67, 0x70, 0x75, 0x22, 0x4f, 0x0a, 0x17, 0x53, 0x74, 0x61, 0x74,
- 0x65, 0x5f, 0x53, 0x65, 0x6e, 0x73, 0x6f, 0x72, 0x54, 0x65, 0x6d, 0x70, 0x65, 0x72, 0x61, 0x74,
- 0x75, 0x72, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
- 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x74, 0x65, 0x6d, 0x70, 0x65,
- 0x72, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x01, 0x52, 0x0b, 0x74, 0x65,
- 0x6d, 0x70, 0x65, 0x72, 0x61, 0x74, 0x75, 0x72, 0x65, 0x22, 0x3e, 0x0a, 0x04, 0x54, 0x61, 0x73,
- 0x6b, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x69,
- 0x64, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52,
- 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20,
- 0x01, 0x28, 0x09, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x7a, 0x0a, 0x0a, 0x54, 0x61, 0x73,
- 0x6b, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20,
- 0x01, 0x28, 0x04, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18,
- 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x64,
- 0x65, 0x6c, 0x61, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x02, 0x52, 0x05, 0x64, 0x65, 0x6c, 0x61,
- 0x79, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52,
- 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73,
- 0x66, 0x75, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73, 0x75, 0x63, 0x63, 0x65,
- 0x73, 0x73, 0x66, 0x75, 0x6c, 0x22, 0x21, 0x0a, 0x07, 0x52, 0x65, 0x63, 0x65, 0x69, 0x70, 0x74,
- 0x12, 0x16, 0x0a, 0x06, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08,
- 0x52, 0x06, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x64, 0x22, 0x23, 0x0a, 0x0d, 0x55, 0x69, 0x6e, 0x74,
- 0x36, 0x34, 0x52, 0x65, 0x63, 0x65, 0x69, 0x70, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74,
- 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x22, 0x0a,
- 0x0c, 0x49, 0x4f, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x44, 0x61, 0x74, 0x61, 0x12, 0x12, 0x0a,
- 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74,
- 0x61, 0x22, 0x89, 0x01, 0x0a, 0x05, 0x47, 0x65, 0x6f, 0x49, 0x50, 0x12, 0x12, 0x0a, 0x04, 0x75,
- 0x73, 0x65, 0x36, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x75, 0x73, 0x65, 0x36, 0x12,
- 0x19, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x09, 0x2e, 0x70, 0x72,
- 0x6f, 0x74, 0x6f, 0x2e, 0x49, 0x50, 0x52, 0x02, 0x69, 0x70, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f,
- 0x75, 0x6e, 0x74, 0x72, 0x79, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09,
- 0x52, 0x0b, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x2e, 0x0a,
- 0x13, 0x64, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x5f, 0x62, 0x6f, 0x6f, 0x74, 0x5f,
- 0x74, 0x69, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x11, 0x64, 0x61, 0x73, 0x68,
- 0x62, 0x6f, 0x61, 0x72, 0x64, 0x42, 0x6f, 0x6f, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x22, 0x2c, 0x0a,
- 0x02, 0x49, 0x50, 0x12, 0x12, 0x0a, 0x04, 0x69, 0x70, 0x76, 0x34, 0x18, 0x01, 0x20, 0x01, 0x28,
- 0x09, 0x52, 0x04, 0x69, 0x70, 0x76, 0x34, 0x12, 0x12, 0x0a, 0x04, 0x69, 0x70, 0x76, 0x36, 0x18,
- 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x69, 0x70, 0x76, 0x36, 0x32, 0xd2, 0x02, 0x0a, 0x0c,
- 0x4e, 0x65, 0x7a, 0x68, 0x61, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x37, 0x0a, 0x11,
- 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x53, 0x74, 0x61, 0x74,
- 0x65, 0x12, 0x0c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x1a,
+ 0x03, 0x28, 0x01, 0x52, 0x03, 0x67, 0x70, 0x75, 0x12, 0x24, 0x0a, 0x04, 0x67, 0x70, 0x75, 0x73,
+ 0x18, 0x12, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53,
+ 0x74, 0x61, 0x74, 0x65, 0x5f, 0x47, 0x50, 0x55, 0x52, 0x04, 0x67, 0x70, 0x75, 0x73, 0x22, 0x71,
+ 0x0a, 0x09, 0x53, 0x74, 0x61, 0x74, 0x65, 0x5f, 0x47, 0x50, 0x55, 0x12, 0x20, 0x0a, 0x0b, 0x75,
+ 0x74, 0x69, 0x6c, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x01,
+ 0x52, 0x0b, 0x75, 0x74, 0x69, 0x6c, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1f, 0x0a,
+ 0x0b, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x5f, 0x75, 0x73, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01,
+ 0x28, 0x04, 0x52, 0x0a, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x55, 0x73, 0x65, 0x64, 0x12, 0x21,
+ 0x0a, 0x0c, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x5f, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x03,
+ 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x54, 0x6f, 0x74, 0x61,
+ 0x6c, 0x22, 0x4f, 0x0a, 0x17, 0x53, 0x74, 0x61, 0x74, 0x65, 0x5f, 0x53, 0x65, 0x6e, 0x73, 0x6f,
+ 0x72, 0x54, 0x65, 0x6d, 0x70, 0x65, 0x72, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x12, 0x0a, 0x04,
+ 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65,
+ 0x12, 0x20, 0x0a, 0x0b, 0x74, 0x65, 0x6d, 0x70, 0x65, 0x72, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18,
+ 0x02, 0x20, 0x01, 0x28, 0x01, 0x52, 0x0b, 0x74, 0x65, 0x6d, 0x70, 0x65, 0x72, 0x61, 0x74, 0x75,
+ 0x72, 0x65, 0x22, 0x3e, 0x0a, 0x04, 0x54, 0x61, 0x73, 0x6b, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64,
+ 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79,
+ 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x12,
+ 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x64, 0x61,
+ 0x74, 0x61, 0x22, 0x7a, 0x0a, 0x0a, 0x54, 0x61, 0x73, 0x6b, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74,
+ 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x69, 0x64,
+ 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04,
+ 0x74, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x64, 0x65, 0x6c, 0x61, 0x79, 0x18, 0x03, 0x20,
+ 0x01, 0x28, 0x02, 0x52, 0x05, 0x64, 0x65, 0x6c, 0x61, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61,
+ 0x74, 0x61, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1e,
+ 0x0a, 0x0a, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x66, 0x75, 0x6c, 0x18, 0x05, 0x20, 0x01,
+ 0x28, 0x08, 0x52, 0x0a, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x66, 0x75, 0x6c, 0x22, 0x21,
+ 0x0a, 0x07, 0x52, 0x65, 0x63, 0x65, 0x69, 0x70, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x72, 0x6f,
+ 0x63, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x70, 0x72, 0x6f, 0x63, 0x65,
+ 0x64, 0x22, 0x23, 0x0a, 0x0d, 0x55, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x52, 0x65, 0x63, 0x65, 0x69,
+ 0x70, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04,
+ 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x22, 0x0a, 0x0c, 0x49, 0x4f, 0x53, 0x74, 0x72, 0x65,
+ 0x61, 0x6d, 0x44, 0x61, 0x74, 0x61, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01,
+ 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x89, 0x01, 0x0a, 0x05, 0x47,
+ 0x65, 0x6f, 0x49, 0x50, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x36, 0x18, 0x01, 0x20, 0x01,
+ 0x28, 0x08, 0x52, 0x04, 0x75, 0x73, 0x65, 0x36, 0x12, 0x19, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x02,
+ 0x20, 0x01, 0x28, 0x0b, 0x32, 0x09, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x49, 0x50, 0x52,
+ 0x02, 0x69, 0x70, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x5f, 0x63,
+ 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x75, 0x6e, 0x74,
+ 0x72, 0x79, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x2e, 0x0a, 0x13, 0x64, 0x61, 0x73, 0x68, 0x62, 0x6f,
+ 0x61, 0x72, 0x64, 0x5f, 0x62, 0x6f, 0x6f, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x04, 0x20,
+ 0x01, 0x28, 0x04, 0x52, 0x11, 0x64, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x42, 0x6f,
+ 0x6f, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x22, 0x2c, 0x0a, 0x02, 0x49, 0x50, 0x12, 0x12, 0x0a, 0x04,
+ 0x69, 0x70, 0x76, 0x34, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x69, 0x70, 0x76, 0x34,
+ 0x12, 0x12, 0x0a, 0x04, 0x69, 0x70, 0x76, 0x36, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04,
+ 0x69, 0x70, 0x76, 0x36, 0x32, 0xd2, 0x02, 0x0a, 0x0c, 0x4e, 0x65, 0x7a, 0x68, 0x61, 0x53, 0x65,
+ 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x37, 0x0a, 0x11, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x53,
+ 0x79, 0x73, 0x74, 0x65, 0x6d, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0c, 0x2e, 0x70, 0x72, 0x6f,
+ 0x74, 0x6f, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x1a, 0x0e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+ 0x2e, 0x52, 0x65, 0x63, 0x65, 0x69, 0x70, 0x74, 0x22, 0x00, 0x28, 0x01, 0x30, 0x01, 0x12, 0x31,
+ 0x0a, 0x10, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x49, 0x6e,
+ 0x66, 0x6f, 0x12, 0x0b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x1a,
0x0e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x65, 0x69, 0x70, 0x74, 0x22,
- 0x00, 0x28, 0x01, 0x30, 0x01, 0x12, 0x31, 0x0a, 0x10, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x53,
- 0x79, 0x73, 0x74, 0x65, 0x6d, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x0b, 0x2e, 0x70, 0x72, 0x6f, 0x74,
- 0x6f, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x1a, 0x0e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52,
- 0x65, 0x63, 0x65, 0x69, 0x70, 0x74, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x0b, 0x52, 0x65, 0x71, 0x75,
- 0x65, 0x73, 0x74, 0x54, 0x61, 0x73, 0x6b, 0x12, 0x11, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e,
- 0x54, 0x61, 0x73, 0x6b, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x1a, 0x0b, 0x2e, 0x70, 0x72, 0x6f,
- 0x74, 0x6f, 0x2e, 0x54, 0x61, 0x73, 0x6b, 0x22, 0x00, 0x28, 0x01, 0x30, 0x01, 0x12, 0x3a, 0x0a,
- 0x08, 0x49, 0x4f, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x74,
- 0x6f, 0x2e, 0x49, 0x4f, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x44, 0x61, 0x74, 0x61, 0x1a, 0x13,
- 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x49, 0x4f, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x44,
- 0x61, 0x74, 0x61, 0x22, 0x00, 0x28, 0x01, 0x30, 0x01, 0x12, 0x2b, 0x0a, 0x0b, 0x52, 0x65, 0x70,
- 0x6f, 0x72, 0x74, 0x47, 0x65, 0x6f, 0x49, 0x50, 0x12, 0x0c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
- 0x2e, 0x47, 0x65, 0x6f, 0x49, 0x50, 0x1a, 0x0c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47,
- 0x65, 0x6f, 0x49, 0x50, 0x22, 0x00, 0x12, 0x38, 0x0a, 0x11, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74,
- 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x49, 0x6e, 0x66, 0x6f, 0x32, 0x12, 0x0b, 0x2e, 0x70, 0x72,
- 0x6f, 0x74, 0x6f, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
- 0x2e, 0x55, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x52, 0x65, 0x63, 0x65, 0x69, 0x70, 0x74, 0x22, 0x00,
- 0x42, 0x09, 0x5a, 0x07, 0x2e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f,
- 0x74, 0x6f, 0x33,
+ 0x00, 0x12, 0x33, 0x0a, 0x0b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x61, 0x73, 0x6b,
+ 0x12, 0x11, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x54, 0x61, 0x73, 0x6b, 0x52, 0x65, 0x73,
+ 0x75, 0x6c, 0x74, 0x1a, 0x0b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x54, 0x61, 0x73, 0x6b,
+ 0x22, 0x00, 0x28, 0x01, 0x30, 0x01, 0x12, 0x3a, 0x0a, 0x08, 0x49, 0x4f, 0x53, 0x74, 0x72, 0x65,
+ 0x61, 0x6d, 0x12, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x49, 0x4f, 0x53, 0x74, 0x72,
+ 0x65, 0x61, 0x6d, 0x44, 0x61, 0x74, 0x61, 0x1a, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e,
+ 0x49, 0x4f, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x44, 0x61, 0x74, 0x61, 0x22, 0x00, 0x28, 0x01,
+ 0x30, 0x01, 0x12, 0x2b, 0x0a, 0x0b, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x47, 0x65, 0x6f, 0x49,
+ 0x50, 0x12, 0x0c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x6f, 0x49, 0x50, 0x1a,
+ 0x0c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x6f, 0x49, 0x50, 0x22, 0x00, 0x12,
+ 0x38, 0x0a, 0x11, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x49,
+ 0x6e, 0x66, 0x6f, 0x32, 0x12, 0x0b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x48, 0x6f, 0x73,
+ 0x74, 0x1a, 0x14, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x55, 0x69, 0x6e, 0x74, 0x36, 0x34,
+ 0x52, 0x65, 0x63, 0x65, 0x69, 0x70, 0x74, 0x22, 0x00, 0x42, 0x09, 0x5a, 0x07, 0x2e, 0x2f, 0x70,
+ 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@@ -918,39 +1001,41 @@ func file_proto_nezha_proto_rawDescGZIP() []byte {
return file_proto_nezha_proto_rawDescData
}
-var file_proto_nezha_proto_msgTypes = make([]protoimpl.MessageInfo, 10)
+var file_proto_nezha_proto_msgTypes = make([]protoimpl.MessageInfo, 11)
var file_proto_nezha_proto_goTypes = []any{
(*Host)(nil), // 0: proto.Host
(*State)(nil), // 1: proto.State
- (*State_SensorTemperature)(nil), // 2: proto.State_SensorTemperature
- (*Task)(nil), // 3: proto.Task
- (*TaskResult)(nil), // 4: proto.TaskResult
- (*Receipt)(nil), // 5: proto.Receipt
- (*Uint64Receipt)(nil), // 6: proto.Uint64Receipt
- (*IOStreamData)(nil), // 7: proto.IOStreamData
- (*GeoIP)(nil), // 8: proto.GeoIP
- (*IP)(nil), // 9: proto.IP
+ (*State_GPU)(nil), // 2: proto.State_GPU
+ (*State_SensorTemperature)(nil), // 3: proto.State_SensorTemperature
+ (*Task)(nil), // 4: proto.Task
+ (*TaskResult)(nil), // 5: proto.TaskResult
+ (*Receipt)(nil), // 6: proto.Receipt
+ (*Uint64Receipt)(nil), // 7: proto.Uint64Receipt
+ (*IOStreamData)(nil), // 8: proto.IOStreamData
+ (*GeoIP)(nil), // 9: proto.GeoIP
+ (*IP)(nil), // 10: proto.IP
}
var file_proto_nezha_proto_depIdxs = []int32{
- 2, // 0: proto.State.temperatures:type_name -> proto.State_SensorTemperature
- 9, // 1: proto.GeoIP.ip:type_name -> proto.IP
- 1, // 2: proto.NezhaService.ReportSystemState:input_type -> proto.State
- 0, // 3: proto.NezhaService.ReportSystemInfo:input_type -> proto.Host
- 4, // 4: proto.NezhaService.RequestTask:input_type -> proto.TaskResult
- 7, // 5: proto.NezhaService.IOStream:input_type -> proto.IOStreamData
- 8, // 6: proto.NezhaService.ReportGeoIP:input_type -> proto.GeoIP
- 0, // 7: proto.NezhaService.ReportSystemInfo2:input_type -> proto.Host
- 5, // 8: proto.NezhaService.ReportSystemState:output_type -> proto.Receipt
- 5, // 9: proto.NezhaService.ReportSystemInfo:output_type -> proto.Receipt
- 3, // 10: proto.NezhaService.RequestTask:output_type -> proto.Task
- 7, // 11: proto.NezhaService.IOStream:output_type -> proto.IOStreamData
- 8, // 12: proto.NezhaService.ReportGeoIP:output_type -> proto.GeoIP
- 6, // 13: proto.NezhaService.ReportSystemInfo2:output_type -> proto.Uint64Receipt
- 8, // [8:14] is the sub-list for method output_type
- 2, // [2:8] is the sub-list for method input_type
- 2, // [2:2] is the sub-list for extension type_name
- 2, // [2:2] is the sub-list for extension extendee
- 0, // [0:2] is the sub-list for field type_name
+ 3, // 0: proto.State.temperatures:type_name -> proto.State_SensorTemperature
+ 2, // 1: proto.State.gpus:type_name -> proto.State_GPU
+ 10, // 2: proto.GeoIP.ip:type_name -> proto.IP
+ 1, // 3: proto.NezhaService.ReportSystemState:input_type -> proto.State
+ 0, // 4: proto.NezhaService.ReportSystemInfo:input_type -> proto.Host
+ 5, // 5: proto.NezhaService.RequestTask:input_type -> proto.TaskResult
+ 8, // 6: proto.NezhaService.IOStream:input_type -> proto.IOStreamData
+ 9, // 7: proto.NezhaService.ReportGeoIP:input_type -> proto.GeoIP
+ 0, // 8: proto.NezhaService.ReportSystemInfo2:input_type -> proto.Host
+ 6, // 9: proto.NezhaService.ReportSystemState:output_type -> proto.Receipt
+ 6, // 10: proto.NezhaService.ReportSystemInfo:output_type -> proto.Receipt
+ 4, // 11: proto.NezhaService.RequestTask:output_type -> proto.Task
+ 8, // 12: proto.NezhaService.IOStream:output_type -> proto.IOStreamData
+ 9, // 13: proto.NezhaService.ReportGeoIP:output_type -> proto.GeoIP
+ 7, // 14: proto.NezhaService.ReportSystemInfo2:output_type -> proto.Uint64Receipt
+ 9, // [9:15] is the sub-list for method output_type
+ 3, // [3:9] is the sub-list for method input_type
+ 3, // [3:3] is the sub-list for extension type_name
+ 3, // [3:3] is the sub-list for extension extendee
+ 0, // [0:3] is the sub-list for field type_name
}
func init() { file_proto_nezha_proto_init() }
@@ -984,7 +1069,7 @@ func file_proto_nezha_proto_init() {
}
}
file_proto_nezha_proto_msgTypes[2].Exporter = func(v any, i int) any {
- switch v := v.(*State_SensorTemperature); i {
+ switch v := v.(*State_GPU); i {
case 0:
return &v.state
case 1:
@@ -996,7 +1081,7 @@ func file_proto_nezha_proto_init() {
}
}
file_proto_nezha_proto_msgTypes[3].Exporter = func(v any, i int) any {
- switch v := v.(*Task); i {
+ switch v := v.(*State_SensorTemperature); i {
case 0:
return &v.state
case 1:
@@ -1008,7 +1093,7 @@ func file_proto_nezha_proto_init() {
}
}
file_proto_nezha_proto_msgTypes[4].Exporter = func(v any, i int) any {
- switch v := v.(*TaskResult); i {
+ switch v := v.(*Task); i {
case 0:
return &v.state
case 1:
@@ -1020,7 +1105,7 @@ func file_proto_nezha_proto_init() {
}
}
file_proto_nezha_proto_msgTypes[5].Exporter = func(v any, i int) any {
- switch v := v.(*Receipt); i {
+ switch v := v.(*TaskResult); i {
case 0:
return &v.state
case 1:
@@ -1032,7 +1117,7 @@ func file_proto_nezha_proto_init() {
}
}
file_proto_nezha_proto_msgTypes[6].Exporter = func(v any, i int) any {
- switch v := v.(*Uint64Receipt); i {
+ switch v := v.(*Receipt); i {
case 0:
return &v.state
case 1:
@@ -1044,7 +1129,7 @@ func file_proto_nezha_proto_init() {
}
}
file_proto_nezha_proto_msgTypes[7].Exporter = func(v any, i int) any {
- switch v := v.(*IOStreamData); i {
+ switch v := v.(*Uint64Receipt); i {
case 0:
return &v.state
case 1:
@@ -1056,7 +1141,7 @@ func file_proto_nezha_proto_init() {
}
}
file_proto_nezha_proto_msgTypes[8].Exporter = func(v any, i int) any {
- switch v := v.(*GeoIP); i {
+ switch v := v.(*IOStreamData); i {
case 0:
return &v.state
case 1:
@@ -1068,6 +1153,18 @@ func file_proto_nezha_proto_init() {
}
}
file_proto_nezha_proto_msgTypes[9].Exporter = func(v any, i int) any {
+ switch v := v.(*GeoIP); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_proto_nezha_proto_msgTypes[10].Exporter = func(v any, i int) any {
switch v := v.(*IP); i {
case 0:
return &v.state
@@ -1086,7 +1183,7 @@ func file_proto_nezha_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_proto_nezha_proto_rawDesc,
NumEnums: 0,
- NumMessages: 10,
+ NumMessages: 11,
NumExtensions: 0,
NumServices: 1,
},
diff --git a/proto/nezha.proto b/proto/nezha.proto
index 75debccd..89383b32 100644
--- a/proto/nezha.proto
+++ b/proto/nezha.proto
@@ -44,6 +44,16 @@ message State {
uint64 process_count = 15;
repeated State_SensorTemperature temperatures = 16;
repeated double gpu = 17;
+ repeated State_GPU gpus = 18;
+}
+
+// State_GPU carries per-card figures. Index-aligned with Host.gpu, matching
+// the existing `gpu` field it supersedes; that field stays populated so older
+// dashboards keep working.
+message State_GPU {
+ double utilization = 1;
+ uint64 memory_used = 2;
+ uint64 memory_total = 3;
}
message State_SensorTemperature {
diff --git a/service/singleton/frontend-templates.yaml b/service/singleton/frontend-templates.yaml
index 40e54b1f..fecbb885 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.3.5"
+ version: "v2.3.6"
is_admin: true
is_official: true
- path: "user-dist"
diff --git a/service/singleton/tsdb.go b/service/singleton/tsdb.go
index 745e83d6..e5311bb1 100644
--- a/service/singleton/tsdb.go
+++ b/service/singleton/tsdb.go
@@ -1,6 +1,7 @@
package singleton
import (
+ "errors"
"log"
"time"
@@ -10,6 +11,8 @@ import (
var TSDBShared *tsdb.TSDB
+var openTSDB = tsdb.Open
+
func InitTSDB() error {
config := &tsdb.Config{
RetentionDays: 30,
@@ -44,11 +47,21 @@ func InitTSDB() error {
return nil
}
- var err error
- TSDBShared, err = tsdb.Open(config)
+ TSDBShared = nil
+ db, err := openTSDB(config)
if err != nil {
+ if errors.Is(err, tsdb.ErrDiskFull) {
+ log.Printf("NEZHA>> Warning: TSDB is unavailable because its disk is full; dashboard and alerts will continue without TSDB writes: %v", err)
+ if DB != nil {
+ if migrateErr := DB.AutoMigrate(model.ServiceHistory{}); migrateErr != nil {
+ log.Printf("NEZHA>> Warning: failed to prepare SQLite service history fallback: %v", migrateErr)
+ }
+ }
+ return nil
+ }
return err
}
+ TSDBShared = db
log.Println("NEZHA>> TSDB initialized successfully")
diff --git a/service/singleton/tsdb_disk_guard_test.go b/service/singleton/tsdb_disk_guard_test.go
new file mode 100644
index 00000000..50f71f3f
--- /dev/null
+++ b/service/singleton/tsdb_disk_guard_test.go
@@ -0,0 +1,58 @@
+package singleton
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/nezhahq/nezha/model"
+ "github.com/nezhahq/nezha/pkg/tsdb"
+)
+
+func TestInitTSDBContinuesWhenDiskIsFull(t *testing.T) {
+ originalConf := Conf
+ originalDB := DB
+ originalShared := TSDBShared
+ originalOpen := openTSDB
+ t.Cleanup(func() {
+ Conf = originalConf
+ DB = originalDB
+ TSDBShared = originalShared
+ openTSDB = originalOpen
+ })
+
+ Conf = &ConfigClass{Config: &model.Config{
+ TSDB: model.TSDBConf{DataPath: "/full/tsdb"},
+ }}
+ DB = nil
+ TSDBShared = nil
+ openTSDB = func(*tsdb.Config) (*tsdb.TSDB, error) {
+ return nil, fmt.Errorf("open failed: %w", tsdb.ErrDiskFull)
+ }
+
+ require.NoError(t, InitTSDB())
+ require.Nil(t, TSDBShared)
+}
+
+func TestInitTSDBStillReturnsNonDiskErrors(t *testing.T) {
+ originalConf := Conf
+ originalShared := TSDBShared
+ originalOpen := openTSDB
+ t.Cleanup(func() {
+ Conf = originalConf
+ TSDBShared = originalShared
+ openTSDB = originalOpen
+ })
+
+ Conf = &ConfigClass{Config: &model.Config{
+ TSDB: model.TSDBConf{DataPath: "/broken/tsdb"},
+ }}
+ TSDBShared = nil
+ openTSDB = func(*tsdb.Config) (*tsdb.TSDB, error) {
+ return nil, fmt.Errorf("permission denied")
+ }
+
+ require.ErrorContains(t, InitTSDB(), "permission denied")
+ require.Nil(t, TSDBShared)
+}