From f366677744d99fc63fbd347322a8a5144b9b03cc Mon Sep 17 00:00:00 2001 From: Ryrenz <163799701+ryrenz@users.noreply.github.com> Date: Sat, 12 Sep 2026 06:56:35 -0400 Subject: [PATCH 1/6] feat: surface GPU memory in host state (#1235) - Add State_GPU message to the proto with utilization and memory fields - Add HostState.GPUs with conversion in both PB() and PB2State() - Keep the existing gpu field populated so older agents keep working Regenerated with protoc-gen-go v1.34.2 / protoc v5.29.3 to match the existing files; nezha_grpc.pb.go is untouched since the service is unchanged. Closes #1217 --- model/host.go | 30 +++++ proto/nezha.pb.go | 335 ++++++++++++++++++++++++++++++---------------- proto/nezha.proto | 10 ++ 3 files changed, 256 insertions(+), 119 deletions(-) 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/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 { From 5d5830fef359750f7db295d8e79b87fe797d498a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 12 Sep 2026 18:56:54 +0800 Subject: [PATCH 2/6] update contributors[no ci] --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d84eb831..6e3cf055 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,7 @@ add your theme to [service/singleton/frontend-templates.yaml](service/singleton/ Darc Z. Creling Core F -Coming +Coming Wel adminsama Acgpiano eya46 @@ -155,6 +155,7 @@ add your theme to [service/singleton/frontend-templates.yaml](service/singleton/ 77-QiQi Chra Mubelotix +Ryrenz zhdsmy ## Special Thanks From 8cb86e385365cd009ce3d68589bcaef7f290a88e Mon Sep 17 00:00:00 2001 From: naiba Date: Sat, 12 Sep 2026 11:11:22 +0000 Subject: [PATCH 3/6] chore(frontend-templates): update frontend versions --- 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 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" From d19952a834f26a700cd042c09a9f85e97db4086e Mon Sep 17 00:00:00 2001 From: NikoCat233 Date: Sat, 12 Sep 2026 22:59:17 +0800 Subject: [PATCH 4/6] feat(auth): allow JWT IP changes by configuration (#1236) * feat(auth): allow JWT IP changes by configuration * fix(auth): make JWT IP change setting concurrency-safe * test(auth): move JWT IP change setting tests out of MCP test file --- cmd/dashboard/controller/jwt.go | 2 +- cmd/dashboard/controller/jwt_session_test.go | 55 ++++++++++++++++++ cmd/dashboard/controller/setting.go | 9 +++ .../controller/setting_jwt_ip_change_test.go | 26 +++++++++ model/config.go | 21 +++++++ model/config_test.go | 4 +- model/jwt_ip_change_atomic_test.go | 56 +++++++++++++++++++ model/setting_api.go | 1 + 8 files changed, 172 insertions(+), 2 deletions(-) create mode 100644 cmd/dashboard/controller/setting_jwt_ip_change_test.go create mode 100644 model/jwt_ip_change_atomic_test.go 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/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"` } From 1155c1757310c13eb9f5a7eaf1f6dc92ada86c58 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 12 Sep 2026 22:59:36 +0800 Subject: [PATCH 5/6] update contributors[no ci] --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6e3cf055..9cdf081a 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,7 @@ add your theme to [service/singleton/frontend-templates.yaml](service/singleton/ Darc Z. Creling Core F -Coming Wel +NikoCat233 adminsama Acgpiano eya46 @@ -136,6 +136,7 @@ add your theme to [service/singleton/frontend-templates.yaml](service/singleton/ 凌 榆木 93 +Coming Wel :D Esdese GreenTeodoro839 @@ -145,7 +146,6 @@ add your theme to [service/singleton/frontend-templates.yaml](service/singleton/ Martijn Lindeman Mingzhuo Yin Nanjing Hopefun Network Technology Co. Ltd. -NikoCat233 Spetrum Tao Chen Tater Li From 06d387779d0dcabf5464e09b4dbad408df526bb9 Mon Sep 17 00:00:00 2001 From: naiba Date: Sun, 13 Sep 2026 00:35:12 +0000 Subject: [PATCH 6/6] fix(tsdb): survive low disk space --- pkg/tsdb/disk_guard_test.go | 124 +++++++++++++++++++++ pkg/tsdb/maintenance.go | 5 +- pkg/tsdb/tsdb.go | 128 +++++++++++++++++++++- pkg/tsdb/writer.go | 23 +++- service/singleton/tsdb.go | 17 ++- service/singleton/tsdb_disk_guard_test.go | 58 ++++++++++ 6 files changed, 341 insertions(+), 14 deletions(-) create mode 100644 pkg/tsdb/disk_guard_test.go create mode 100644 service/singleton/tsdb_disk_guard_test.go 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/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) +}