fix: sync notification stats across agents (#4525)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Amir Raminfar
2026-03-09 12:31:21 -07:00
committed by GitHub
parent 1cd1b4a0af
commit 330e952ca4
16 changed files with 574 additions and 110 deletions
+25
View File
@@ -541,6 +541,31 @@ func (c *Client) UpdateNotificationConfig(ctx context.Context, subscriptions []t
return err
}
func (c *Client) GetNotificationStats(ctx context.Context) ([]types.SubscriptionStats, error) {
resp, err := c.client.GetNotificationStats(ctx, &pb.GetNotificationStatsRequest{})
if err != nil {
return nil, err
}
stats := make([]types.SubscriptionStats, len(resp.Stats))
for i, s := range resp.Stats {
var lastTriggered *time.Time
if s.LastTriggeredAt != nil {
t := s.LastTriggeredAt.AsTime()
if !t.IsZero() {
lastTriggered = &t
}
}
stats[i] = types.SubscriptionStats{
SubscriptionID: int(s.SubscriptionId),
TriggerCount: s.TriggerCount,
LastTriggeredAt: lastTriggered,
TriggeredContainerIDs: s.TriggeredContainerIds,
}
}
return stats, nil
}
func jsonBytesToOrderedMap(b []byte) *orderedmap.OrderedMap[string, any] {
var data *orderedmap.OrderedMap[string, any]
reader := bytes.NewReader(b)
+155 -65
View File
@@ -1358,6 +1358,86 @@ func (*UpdateNotificationConfigResponse) Descriptor() ([]byte, []int) {
return file_rpc_proto_rawDescGZIP(), []int{26}
}
type GetNotificationStatsRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetNotificationStatsRequest) Reset() {
*x = GetNotificationStatsRequest{}
mi := &file_rpc_proto_msgTypes[27]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetNotificationStatsRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetNotificationStatsRequest) ProtoMessage() {}
func (x *GetNotificationStatsRequest) ProtoReflect() protoreflect.Message {
mi := &file_rpc_proto_msgTypes[27]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetNotificationStatsRequest.ProtoReflect.Descriptor instead.
func (*GetNotificationStatsRequest) Descriptor() ([]byte, []int) {
return file_rpc_proto_rawDescGZIP(), []int{27}
}
type GetNotificationStatsResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Stats []*NotificationSubscriptionStats `protobuf:"bytes,1,rep,name=stats,proto3" json:"stats,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetNotificationStatsResponse) Reset() {
*x = GetNotificationStatsResponse{}
mi := &file_rpc_proto_msgTypes[28]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetNotificationStatsResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetNotificationStatsResponse) ProtoMessage() {}
func (x *GetNotificationStatsResponse) ProtoReflect() protoreflect.Message {
mi := &file_rpc_proto_msgTypes[28]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetNotificationStatsResponse.ProtoReflect.Descriptor instead.
func (*GetNotificationStatsResponse) Descriptor() ([]byte, []int) {
return file_rpc_proto_rawDescGZIP(), []int{28}
}
func (x *GetNotificationStatsResponse) GetStats() []*NotificationSubscriptionStats {
if x != nil {
return x.Stats
}
return nil
}
var File_rpc_proto protoreflect.FileDescriptor
const file_rpc_proto_rawDesc = "" +
@@ -1437,7 +1517,10 @@ const file_rpc_proto_rawDesc = "" +
"\x1fUpdateNotificationConfigRequest\x12H\n" +
"\rsubscriptions\x18\x01 \x03(\v2\".protobuf.NotificationSubscriptionR\rsubscriptions\x12B\n" +
"\vdispatchers\x18\x02 \x03(\v2 .protobuf.NotificationDispatcherR\vdispatchers\"\"\n" +
" UpdateNotificationConfigResponse2\x96\t\n" +
" UpdateNotificationConfigResponse\"\x1d\n" +
"\x1bGetNotificationStatsRequest\"]\n" +
"\x1cGetNotificationStatsResponse\x12=\n" +
"\x05stats\x18\x01 \x03(\v2'.protobuf.NotificationSubscriptionStatsR\x05stats2\xff\t\n" +
"\fAgentService\x12U\n" +
"\x0eListContainers\x12\x1f.protobuf.ListContainersRequest\x1a .protobuf.ListContainersResponse\"\x00\x12R\n" +
"\rFindContainer\x12\x1e.protobuf.FindContainerRequest\x1a\x1f.protobuf.FindContainerResponse\"\x00\x12K\n" +
@@ -1452,7 +1535,8 @@ const file_rpc_proto_rawDesc = "" +
"\x0fContainerAction\x12 .protobuf.ContainerActionRequest\x1a!.protobuf.ContainerActionResponse\"\x00\x12V\n" +
"\rContainerExec\x12\x1e.protobuf.ContainerExecRequest\x1a\x1f.protobuf.ContainerExecResponse\"\x00(\x010\x01\x12\\\n" +
"\x0fContainerAttach\x12 .protobuf.ContainerAttachRequest\x1a!.protobuf.ContainerAttachResponse\"\x00(\x010\x01\x12s\n" +
"\x18UpdateNotificationConfig\x12).protobuf.UpdateNotificationConfigRequest\x1a*.protobuf.UpdateNotificationConfigResponse\"\x00B\x13Z\x11internal/agent/pbb\x06proto3"
"\x18UpdateNotificationConfig\x12).protobuf.UpdateNotificationConfigRequest\x1a*.protobuf.UpdateNotificationConfigResponse\"\x00\x12g\n" +
"\x14GetNotificationStats\x12%.protobuf.GetNotificationStatsRequest\x1a&.protobuf.GetNotificationStatsResponse\"\x00B\x13Z\x11internal/agent/pbb\x06proto3"
var (
file_rpc_proto_rawDescOnce sync.Once
@@ -1466,7 +1550,7 @@ func file_rpc_proto_rawDescGZIP() []byte {
return file_rpc_proto_rawDescData
}
var file_rpc_proto_msgTypes = make([]protoimpl.MessageInfo, 29)
var file_rpc_proto_msgTypes = make([]protoimpl.MessageInfo, 31)
var file_rpc_proto_goTypes = []any{
(*ListContainersRequest)(nil), // 0: protobuf.ListContainersRequest
(*RepeatedString)(nil), // 1: protobuf.RepeatedString
@@ -1495,71 +1579,77 @@ var file_rpc_proto_goTypes = []any{
(*ContainerAttachResponse)(nil), // 24: protobuf.ContainerAttachResponse
(*UpdateNotificationConfigRequest)(nil), // 25: protobuf.UpdateNotificationConfigRequest
(*UpdateNotificationConfigResponse)(nil), // 26: protobuf.UpdateNotificationConfigResponse
nil, // 27: protobuf.ListContainersRequest.FilterEntry
nil, // 28: protobuf.FindContainerRequest.FilterEntry
(*Container)(nil), // 29: protobuf.Container
(*timestamppb.Timestamp)(nil), // 30: google.protobuf.Timestamp
(*LogEvent)(nil), // 31: protobuf.LogEvent
(*ContainerEvent)(nil), // 32: protobuf.ContainerEvent
(*ContainerStat)(nil), // 33: protobuf.ContainerStat
(*Host)(nil), // 34: protobuf.Host
(ContainerAction)(0), // 35: protobuf.ContainerAction
(*NotificationSubscription)(nil), // 36: protobuf.NotificationSubscription
(*NotificationDispatcher)(nil), // 37: protobuf.NotificationDispatcher
(*GetNotificationStatsRequest)(nil), // 27: protobuf.GetNotificationStatsRequest
(*GetNotificationStatsResponse)(nil), // 28: protobuf.GetNotificationStatsResponse
nil, // 29: protobuf.ListContainersRequest.FilterEntry
nil, // 30: protobuf.FindContainerRequest.FilterEntry
(*Container)(nil), // 31: protobuf.Container
(*timestamppb.Timestamp)(nil), // 32: google.protobuf.Timestamp
(*LogEvent)(nil), // 33: protobuf.LogEvent
(*ContainerEvent)(nil), // 34: protobuf.ContainerEvent
(*ContainerStat)(nil), // 35: protobuf.ContainerStat
(*Host)(nil), // 36: protobuf.Host
(ContainerAction)(0), // 37: protobuf.ContainerAction
(*NotificationSubscription)(nil), // 38: protobuf.NotificationSubscription
(*NotificationDispatcher)(nil), // 39: protobuf.NotificationDispatcher
(*NotificationSubscriptionStats)(nil), // 40: protobuf.NotificationSubscriptionStats
}
var file_rpc_proto_depIdxs = []int32{
27, // 0: protobuf.ListContainersRequest.filter:type_name -> protobuf.ListContainersRequest.FilterEntry
29, // 1: protobuf.ListContainersResponse.containers:type_name -> protobuf.Container
28, // 2: protobuf.FindContainerRequest.filter:type_name -> protobuf.FindContainerRequest.FilterEntry
29, // 3: protobuf.FindContainerResponse.container:type_name -> protobuf.Container
30, // 4: protobuf.StreamLogsRequest.since:type_name -> google.protobuf.Timestamp
31, // 5: protobuf.StreamLogsResponse.event:type_name -> protobuf.LogEvent
30, // 6: protobuf.LogsBetweenDatesRequest.since:type_name -> google.protobuf.Timestamp
30, // 7: protobuf.LogsBetweenDatesRequest.until:type_name -> google.protobuf.Timestamp
30, // 8: protobuf.StreamRawBytesRequest.since:type_name -> google.protobuf.Timestamp
30, // 9: protobuf.StreamRawBytesRequest.until:type_name -> google.protobuf.Timestamp
32, // 10: protobuf.StreamEventsResponse.event:type_name -> protobuf.ContainerEvent
33, // 11: protobuf.StreamStatsResponse.stat:type_name -> protobuf.ContainerStat
34, // 12: protobuf.HostInfoResponse.host:type_name -> protobuf.Host
29, // 13: protobuf.StreamContainerStartedResponse.container:type_name -> protobuf.Container
35, // 14: protobuf.ContainerActionRequest.action:type_name -> protobuf.ContainerAction
29, // 0: protobuf.ListContainersRequest.filter:type_name -> protobuf.ListContainersRequest.FilterEntry
31, // 1: protobuf.ListContainersResponse.containers:type_name -> protobuf.Container
30, // 2: protobuf.FindContainerRequest.filter:type_name -> protobuf.FindContainerRequest.FilterEntry
31, // 3: protobuf.FindContainerResponse.container:type_name -> protobuf.Container
32, // 4: protobuf.StreamLogsRequest.since:type_name -> google.protobuf.Timestamp
33, // 5: protobuf.StreamLogsResponse.event:type_name -> protobuf.LogEvent
32, // 6: protobuf.LogsBetweenDatesRequest.since:type_name -> google.protobuf.Timestamp
32, // 7: protobuf.LogsBetweenDatesRequest.until:type_name -> google.protobuf.Timestamp
32, // 8: protobuf.StreamRawBytesRequest.since:type_name -> google.protobuf.Timestamp
32, // 9: protobuf.StreamRawBytesRequest.until:type_name -> google.protobuf.Timestamp
34, // 10: protobuf.StreamEventsResponse.event:type_name -> protobuf.ContainerEvent
35, // 11: protobuf.StreamStatsResponse.stat:type_name -> protobuf.ContainerStat
36, // 12: protobuf.HostInfoResponse.host:type_name -> protobuf.Host
31, // 13: protobuf.StreamContainerStartedResponse.container:type_name -> protobuf.Container
37, // 14: protobuf.ContainerActionRequest.action:type_name -> protobuf.ContainerAction
21, // 15: protobuf.ContainerExecRequest.resize:type_name -> protobuf.ResizePayload
21, // 16: protobuf.ContainerAttachRequest.resize:type_name -> protobuf.ResizePayload
36, // 17: protobuf.UpdateNotificationConfigRequest.subscriptions:type_name -> protobuf.NotificationSubscription
37, // 18: protobuf.UpdateNotificationConfigRequest.dispatchers:type_name -> protobuf.NotificationDispatcher
1, // 19: protobuf.ListContainersRequest.FilterEntry.value:type_name -> protobuf.RepeatedString
1, // 20: protobuf.FindContainerRequest.FilterEntry.value:type_name -> protobuf.RepeatedString
0, // 21: protobuf.AgentService.ListContainers:input_type -> protobuf.ListContainersRequest
3, // 22: protobuf.AgentService.FindContainer:input_type -> protobuf.FindContainerRequest
5, // 23: protobuf.AgentService.StreamLogs:input_type -> protobuf.StreamLogsRequest
7, // 24: protobuf.AgentService.LogsBetweenDates:input_type -> protobuf.LogsBetweenDatesRequest
8, // 25: protobuf.AgentService.StreamRawBytes:input_type -> protobuf.StreamRawBytesRequest
10, // 26: protobuf.AgentService.StreamEvents:input_type -> protobuf.StreamEventsRequest
12, // 27: protobuf.AgentService.StreamStats:input_type -> protobuf.StreamStatsRequest
16, // 28: protobuf.AgentService.StreamContainerStarted:input_type -> protobuf.StreamContainerStartedRequest
14, // 29: protobuf.AgentService.HostInfo:input_type -> protobuf.HostInfoRequest
18, // 30: protobuf.AgentService.ContainerAction:input_type -> protobuf.ContainerActionRequest
20, // 31: protobuf.AgentService.ContainerExec:input_type -> protobuf.ContainerExecRequest
23, // 32: protobuf.AgentService.ContainerAttach:input_type -> protobuf.ContainerAttachRequest
25, // 33: protobuf.AgentService.UpdateNotificationConfig:input_type -> protobuf.UpdateNotificationConfigRequest
2, // 34: protobuf.AgentService.ListContainers:output_type -> protobuf.ListContainersResponse
4, // 35: protobuf.AgentService.FindContainer:output_type -> protobuf.FindContainerResponse
6, // 36: protobuf.AgentService.StreamLogs:output_type -> protobuf.StreamLogsResponse
6, // 37: protobuf.AgentService.LogsBetweenDates:output_type -> protobuf.StreamLogsResponse
9, // 38: protobuf.AgentService.StreamRawBytes:output_type -> protobuf.StreamRawBytesResponse
11, // 39: protobuf.AgentService.StreamEvents:output_type -> protobuf.StreamEventsResponse
13, // 40: protobuf.AgentService.StreamStats:output_type -> protobuf.StreamStatsResponse
17, // 41: protobuf.AgentService.StreamContainerStarted:output_type -> protobuf.StreamContainerStartedResponse
15, // 42: protobuf.AgentService.HostInfo:output_type -> protobuf.HostInfoResponse
19, // 43: protobuf.AgentService.ContainerAction:output_type -> protobuf.ContainerActionResponse
22, // 44: protobuf.AgentService.ContainerExec:output_type -> protobuf.ContainerExecResponse
24, // 45: protobuf.AgentService.ContainerAttach:output_type -> protobuf.ContainerAttachResponse
26, // 46: protobuf.AgentService.UpdateNotificationConfig:output_type -> protobuf.UpdateNotificationConfigResponse
34, // [34:47] is the sub-list for method output_type
21, // [21:34] is the sub-list for method input_type
21, // [21:21] is the sub-list for extension type_name
21, // [21:21] is the sub-list for extension extendee
0, // [0:21] is the sub-list for field type_name
38, // 17: protobuf.UpdateNotificationConfigRequest.subscriptions:type_name -> protobuf.NotificationSubscription
39, // 18: protobuf.UpdateNotificationConfigRequest.dispatchers:type_name -> protobuf.NotificationDispatcher
40, // 19: protobuf.GetNotificationStatsResponse.stats:type_name -> protobuf.NotificationSubscriptionStats
1, // 20: protobuf.ListContainersRequest.FilterEntry.value:type_name -> protobuf.RepeatedString
1, // 21: protobuf.FindContainerRequest.FilterEntry.value:type_name -> protobuf.RepeatedString
0, // 22: protobuf.AgentService.ListContainers:input_type -> protobuf.ListContainersRequest
3, // 23: protobuf.AgentService.FindContainer:input_type -> protobuf.FindContainerRequest
5, // 24: protobuf.AgentService.StreamLogs:input_type -> protobuf.StreamLogsRequest
7, // 25: protobuf.AgentService.LogsBetweenDates:input_type -> protobuf.LogsBetweenDatesRequest
8, // 26: protobuf.AgentService.StreamRawBytes:input_type -> protobuf.StreamRawBytesRequest
10, // 27: protobuf.AgentService.StreamEvents:input_type -> protobuf.StreamEventsRequest
12, // 28: protobuf.AgentService.StreamStats:input_type -> protobuf.StreamStatsRequest
16, // 29: protobuf.AgentService.StreamContainerStarted:input_type -> protobuf.StreamContainerStartedRequest
14, // 30: protobuf.AgentService.HostInfo:input_type -> protobuf.HostInfoRequest
18, // 31: protobuf.AgentService.ContainerAction:input_type -> protobuf.ContainerActionRequest
20, // 32: protobuf.AgentService.ContainerExec:input_type -> protobuf.ContainerExecRequest
23, // 33: protobuf.AgentService.ContainerAttach:input_type -> protobuf.ContainerAttachRequest
25, // 34: protobuf.AgentService.UpdateNotificationConfig:input_type -> protobuf.UpdateNotificationConfigRequest
27, // 35: protobuf.AgentService.GetNotificationStats:input_type -> protobuf.GetNotificationStatsRequest
2, // 36: protobuf.AgentService.ListContainers:output_type -> protobuf.ListContainersResponse
4, // 37: protobuf.AgentService.FindContainer:output_type -> protobuf.FindContainerResponse
6, // 38: protobuf.AgentService.StreamLogs:output_type -> protobuf.StreamLogsResponse
6, // 39: protobuf.AgentService.LogsBetweenDates:output_type -> protobuf.StreamLogsResponse
9, // 40: protobuf.AgentService.StreamRawBytes:output_type -> protobuf.StreamRawBytesResponse
11, // 41: protobuf.AgentService.StreamEvents:output_type -> protobuf.StreamEventsResponse
13, // 42: protobuf.AgentService.StreamStats:output_type -> protobuf.StreamStatsResponse
17, // 43: protobuf.AgentService.StreamContainerStarted:output_type -> protobuf.StreamContainerStartedResponse
15, // 44: protobuf.AgentService.HostInfo:output_type -> protobuf.HostInfoResponse
19, // 45: protobuf.AgentService.ContainerAction:output_type -> protobuf.ContainerActionResponse
22, // 46: protobuf.AgentService.ContainerExec:output_type -> protobuf.ContainerExecResponse
24, // 47: protobuf.AgentService.ContainerAttach:output_type -> protobuf.ContainerAttachResponse
26, // 48: protobuf.AgentService.UpdateNotificationConfig:output_type -> protobuf.UpdateNotificationConfigResponse
28, // 49: protobuf.AgentService.GetNotificationStats:output_type -> protobuf.GetNotificationStatsResponse
36, // [36:50] is the sub-list for method output_type
22, // [22:36] is the sub-list for method input_type
22, // [22:22] is the sub-list for extension type_name
22, // [22:22] is the sub-list for extension extendee
0, // [0:22] is the sub-list for field type_name
}
func init() { file_rpc_proto_init() }
@@ -1582,7 +1672,7 @@ func file_rpc_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_rpc_proto_rawDesc), len(file_rpc_proto_rawDesc)),
NumEnums: 0,
NumMessages: 29,
NumMessages: 31,
NumExtensions: 0,
NumServices: 1,
},
+38
View File
@@ -32,6 +32,7 @@ const (
AgentService_ContainerExec_FullMethodName = "/protobuf.AgentService/ContainerExec"
AgentService_ContainerAttach_FullMethodName = "/protobuf.AgentService/ContainerAttach"
AgentService_UpdateNotificationConfig_FullMethodName = "/protobuf.AgentService/UpdateNotificationConfig"
AgentService_GetNotificationStats_FullMethodName = "/protobuf.AgentService/GetNotificationStats"
)
// AgentServiceClient is the client API for AgentService service.
@@ -51,6 +52,7 @@ type AgentServiceClient interface {
ContainerExec(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[ContainerExecRequest, ContainerExecResponse], error)
ContainerAttach(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[ContainerAttachRequest, ContainerAttachResponse], error)
UpdateNotificationConfig(ctx context.Context, in *UpdateNotificationConfigRequest, opts ...grpc.CallOption) (*UpdateNotificationConfigResponse, error)
GetNotificationStats(ctx context.Context, in *GetNotificationStatsRequest, opts ...grpc.CallOption) (*GetNotificationStatsResponse, error)
}
type agentServiceClient struct {
@@ -251,6 +253,16 @@ func (c *agentServiceClient) UpdateNotificationConfig(ctx context.Context, in *U
return out, nil
}
func (c *agentServiceClient) GetNotificationStats(ctx context.Context, in *GetNotificationStatsRequest, opts ...grpc.CallOption) (*GetNotificationStatsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetNotificationStatsResponse)
err := c.cc.Invoke(ctx, AgentService_GetNotificationStats_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// AgentServiceServer is the server API for AgentService service.
// All implementations must embed UnimplementedAgentServiceServer
// for forward compatibility.
@@ -268,6 +280,7 @@ type AgentServiceServer interface {
ContainerExec(grpc.BidiStreamingServer[ContainerExecRequest, ContainerExecResponse]) error
ContainerAttach(grpc.BidiStreamingServer[ContainerAttachRequest, ContainerAttachResponse]) error
UpdateNotificationConfig(context.Context, *UpdateNotificationConfigRequest) (*UpdateNotificationConfigResponse, error)
GetNotificationStats(context.Context, *GetNotificationStatsRequest) (*GetNotificationStatsResponse, error)
mustEmbedUnimplementedAgentServiceServer()
}
@@ -317,6 +330,9 @@ func (UnimplementedAgentServiceServer) ContainerAttach(grpc.BidiStreamingServer[
func (UnimplementedAgentServiceServer) UpdateNotificationConfig(context.Context, *UpdateNotificationConfigRequest) (*UpdateNotificationConfigResponse, error) {
return nil, status.Error(codes.Unimplemented, "method UpdateNotificationConfig not implemented")
}
func (UnimplementedAgentServiceServer) GetNotificationStats(context.Context, *GetNotificationStatsRequest) (*GetNotificationStatsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetNotificationStats not implemented")
}
func (UnimplementedAgentServiceServer) mustEmbedUnimplementedAgentServiceServer() {}
func (UnimplementedAgentServiceServer) testEmbeddedByValue() {}
@@ -508,6 +524,24 @@ func _AgentService_UpdateNotificationConfig_Handler(srv interface{}, ctx context
return interceptor(ctx, in, info, handler)
}
func _AgentService_GetNotificationStats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetNotificationStatsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AgentServiceServer).GetNotificationStats(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AgentService_GetNotificationStats_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AgentServiceServer).GetNotificationStats(ctx, req.(*GetNotificationStatsRequest))
}
return interceptor(ctx, in, info, handler)
}
// AgentService_ServiceDesc is the grpc.ServiceDesc for AgentService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@@ -535,6 +569,10 @@ var AgentService_ServiceDesc = grpc.ServiceDesc{
MethodName: "UpdateNotificationConfig",
Handler: _AgentService_UpdateNotificationConfig_Handler,
},
{
MethodName: "GetNotificationStats",
Handler: _AgentService_GetNotificationStats_Handler,
},
},
Streams: []grpc.StreamDesc{
{
+109 -34
View File
@@ -1012,6 +1012,74 @@ func (x *NotificationDispatcher) GetHeaders() map[string]string {
return nil
}
type NotificationSubscriptionStats struct {
state protoimpl.MessageState `protogen:"open.v1"`
SubscriptionId int32 `protobuf:"varint,1,opt,name=subscriptionId,proto3" json:"subscriptionId,omitempty"`
TriggerCount int64 `protobuf:"varint,2,opt,name=triggerCount,proto3" json:"triggerCount,omitempty"`
LastTriggeredAt *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=lastTriggeredAt,proto3" json:"lastTriggeredAt,omitempty"`
TriggeredContainerIds []string `protobuf:"bytes,4,rep,name=triggeredContainerIds,proto3" json:"triggeredContainerIds,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *NotificationSubscriptionStats) Reset() {
*x = NotificationSubscriptionStats{}
mi := &file_types_proto_msgTypes[11]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *NotificationSubscriptionStats) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*NotificationSubscriptionStats) ProtoMessage() {}
func (x *NotificationSubscriptionStats) ProtoReflect() protoreflect.Message {
mi := &file_types_proto_msgTypes[11]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use NotificationSubscriptionStats.ProtoReflect.Descriptor instead.
func (*NotificationSubscriptionStats) Descriptor() ([]byte, []int) {
return file_types_proto_rawDescGZIP(), []int{11}
}
func (x *NotificationSubscriptionStats) GetSubscriptionId() int32 {
if x != nil {
return x.SubscriptionId
}
return 0
}
func (x *NotificationSubscriptionStats) GetTriggerCount() int64 {
if x != nil {
return x.TriggerCount
}
return 0
}
func (x *NotificationSubscriptionStats) GetLastTriggeredAt() *timestamppb.Timestamp {
if x != nil {
return x.LastTriggeredAt
}
return nil
}
func (x *NotificationSubscriptionStats) GetTriggeredContainerIds() []string {
if x != nil {
return x.TriggeredContainerIds
}
return nil
}
var File_types_proto protoreflect.FileDescriptor
const file_types_proto_rawDesc = "" +
@@ -1110,7 +1178,12 @@ const file_types_proto_rawDesc = "" +
"\aheaders\x18\x06 \x03(\v2-.protobuf.NotificationDispatcher.HeadersEntryR\aheaders\x1a:\n" +
"\fHeadersEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01*3\n" +
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xe7\x01\n" +
"\x1dNotificationSubscriptionStats\x12&\n" +
"\x0esubscriptionId\x18\x01 \x01(\x05R\x0esubscriptionId\x12\"\n" +
"\ftriggerCount\x18\x02 \x01(\x03R\ftriggerCount\x12D\n" +
"\x0flastTriggeredAt\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\x0flastTriggeredAt\x124\n" +
"\x15triggeredContainerIds\x18\x04 \x03(\tR\x15triggeredContainerIds*3\n" +
"\x0fContainerAction\x12\t\n" +
"\x05Start\x10\x00\x12\b\n" +
"\x04Stop\x10\x01\x12\v\n" +
@@ -1129,43 +1202,45 @@ func file_types_proto_rawDescGZIP() []byte {
}
var file_types_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_types_proto_msgTypes = make([]protoimpl.MessageInfo, 14)
var file_types_proto_msgTypes = make([]protoimpl.MessageInfo, 15)
var file_types_proto_goTypes = []any{
(ContainerAction)(0), // 0: protobuf.ContainerAction
(*Container)(nil), // 1: protobuf.Container
(*ContainerStat)(nil), // 2: protobuf.ContainerStat
(*LogFragment)(nil), // 3: protobuf.LogFragment
(*LogEvent)(nil), // 4: protobuf.LogEvent
(*SingleMessage)(nil), // 5: protobuf.SingleMessage
(*GroupMessage)(nil), // 6: protobuf.GroupMessage
(*ComplexMessage)(nil), // 7: protobuf.ComplexMessage
(*ContainerEvent)(nil), // 8: protobuf.ContainerEvent
(*Host)(nil), // 9: protobuf.Host
(*NotificationSubscription)(nil), // 10: protobuf.NotificationSubscription
(*NotificationDispatcher)(nil), // 11: protobuf.NotificationDispatcher
nil, // 12: protobuf.Container.LabelsEntry
nil, // 13: protobuf.Host.LabelsEntry
nil, // 14: protobuf.NotificationDispatcher.HeadersEntry
(*timestamppb.Timestamp)(nil), // 15: google.protobuf.Timestamp
(*anypb.Any)(nil), // 16: google.protobuf.Any
(ContainerAction)(0), // 0: protobuf.ContainerAction
(*Container)(nil), // 1: protobuf.Container
(*ContainerStat)(nil), // 2: protobuf.ContainerStat
(*LogFragment)(nil), // 3: protobuf.LogFragment
(*LogEvent)(nil), // 4: protobuf.LogEvent
(*SingleMessage)(nil), // 5: protobuf.SingleMessage
(*GroupMessage)(nil), // 6: protobuf.GroupMessage
(*ComplexMessage)(nil), // 7: protobuf.ComplexMessage
(*ContainerEvent)(nil), // 8: protobuf.ContainerEvent
(*Host)(nil), // 9: protobuf.Host
(*NotificationSubscription)(nil), // 10: protobuf.NotificationSubscription
(*NotificationDispatcher)(nil), // 11: protobuf.NotificationDispatcher
(*NotificationSubscriptionStats)(nil), // 12: protobuf.NotificationSubscriptionStats
nil, // 13: protobuf.Container.LabelsEntry
nil, // 14: protobuf.Host.LabelsEntry
nil, // 15: protobuf.NotificationDispatcher.HeadersEntry
(*timestamppb.Timestamp)(nil), // 16: google.protobuf.Timestamp
(*anypb.Any)(nil), // 17: google.protobuf.Any
}
var file_types_proto_depIdxs = []int32{
15, // 0: protobuf.Container.created:type_name -> google.protobuf.Timestamp
15, // 1: protobuf.Container.started:type_name -> google.protobuf.Timestamp
12, // 2: protobuf.Container.labels:type_name -> protobuf.Container.LabelsEntry
16, // 0: protobuf.Container.created:type_name -> google.protobuf.Timestamp
16, // 1: protobuf.Container.started:type_name -> google.protobuf.Timestamp
13, // 2: protobuf.Container.labels:type_name -> protobuf.Container.LabelsEntry
2, // 3: protobuf.Container.stats:type_name -> protobuf.ContainerStat
15, // 4: protobuf.Container.finished:type_name -> google.protobuf.Timestamp
16, // 5: protobuf.LogEvent.message:type_name -> google.protobuf.Any
15, // 6: protobuf.LogEvent.timestamp:type_name -> google.protobuf.Timestamp
16, // 4: protobuf.Container.finished:type_name -> google.protobuf.Timestamp
17, // 5: protobuf.LogEvent.message:type_name -> google.protobuf.Any
16, // 6: protobuf.LogEvent.timestamp:type_name -> google.protobuf.Timestamp
3, // 7: protobuf.GroupMessage.fragments:type_name -> protobuf.LogFragment
15, // 8: protobuf.ContainerEvent.timestamp:type_name -> google.protobuf.Timestamp
13, // 9: protobuf.Host.labels:type_name -> protobuf.Host.LabelsEntry
14, // 10: protobuf.NotificationDispatcher.headers:type_name -> protobuf.NotificationDispatcher.HeadersEntry
11, // [11:11] is the sub-list for method output_type
11, // [11:11] is the sub-list for method input_type
11, // [11:11] is the sub-list for extension type_name
11, // [11:11] is the sub-list for extension extendee
0, // [0:11] is the sub-list for field type_name
16, // 8: protobuf.ContainerEvent.timestamp:type_name -> google.protobuf.Timestamp
14, // 9: protobuf.Host.labels:type_name -> protobuf.Host.LabelsEntry
15, // 10: protobuf.NotificationDispatcher.headers:type_name -> protobuf.NotificationDispatcher.HeadersEntry
16, // 11: protobuf.NotificationSubscriptionStats.lastTriggeredAt:type_name -> google.protobuf.Timestamp
12, // [12:12] is the sub-list for method output_type
12, // [12:12] is the sub-list for method input_type
12, // [12:12] is the sub-list for extension type_name
12, // [12:12] is the sub-list for extension extendee
0, // [0:12] is the sub-list for field type_name
}
func init() { file_types_proto_init() }
@@ -1179,7 +1254,7 @@ func file_types_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_types_proto_rawDesc), len(file_types_proto_rawDesc)),
NumEnums: 1,
NumMessages: 14,
NumMessages: 15,
NumExtensions: 0,
NumServices: 0,
},
+24
View File
@@ -30,6 +30,7 @@ import (
// NotificationConfigHandler handles notification config updates received from the main server
type NotificationConfigHandler interface {
HandleNotificationConfig(subscriptions []types.SubscriptionConfig, dispatchers []types.DispatcherConfig) error
GetNotificationStats() []types.SubscriptionStats
}
// ClientService is the interface for container operations used by the agent server
@@ -443,6 +444,29 @@ func (s *server) UpdateNotificationConfig(ctx context.Context, req *pb.UpdateNot
return &pb.UpdateNotificationConfigResponse{}, nil
}
func (s *server) GetNotificationStats(ctx context.Context, req *pb.GetNotificationStatsRequest) (*pb.GetNotificationStatsResponse, error) {
if s.notificationConfigHandler == nil {
return &pb.GetNotificationStatsResponse{}, nil
}
stats := s.notificationConfigHandler.GetNotificationStats()
pbStats := make([]*pb.NotificationSubscriptionStats, len(stats))
for i, s := range stats {
pbStat := &pb.NotificationSubscriptionStats{
SubscriptionId: int32(s.SubscriptionID),
TriggerCount: s.TriggerCount,
TriggeredContainerIds: s.TriggeredContainerIDs,
}
if s.LastTriggeredAt != nil {
pbStat.LastTriggeredAt = timestamppb.New(*s.LastTriggeredAt)
}
pbStats[i] = pbStat
}
return &pb.GetNotificationStatsResponse{Stats: pbStats}, nil
}
func NewServer(service ClientService, certificates tls.Certificate, dozzleVersion string, notificationHandler NotificationConfigHandler) (*grpc.Server, error) {
caCertPool := x509.NewCertPool()
c, err := x509.ParseCertificate(certificates.Certificate[0])
+46 -3
View File
@@ -72,8 +72,25 @@ func (m *Manager) LoadConfig(r io.Reader) error {
// HandleNotificationConfig implements agent.NotificationConfigHandler interface
// It atomically replaces all subscriptions and dispatchers with new state from the main server
func (m *Manager) HandleNotificationConfig(subscriptions []types.SubscriptionConfig, dispatchers []types.DispatcherConfig) error {
// Clear existing state
m.subscriptions.Clear()
// Snapshot existing subscriptions to preserve runtime stats
existing := make(map[int]*Subscription)
m.subscriptions.Range(func(id int, sub *Subscription) bool {
existing[id] = sub
return true
})
// Build set of incoming IDs and remove stale subscriptions
incomingIDs := make(map[int]struct{}, len(subscriptions))
for _, sub := range subscriptions {
incomingIDs[sub.ID] = struct{}{}
}
for id := range existing {
if _, ok := incomingIDs[id]; !ok {
m.subscriptions.Delete(id)
}
}
// Clear dispatchers (no stats to preserve)
m.dispatchers.Clear()
// Find max IDs to initialize counters
@@ -91,7 +108,7 @@ func (m *Manager) HandleNotificationConfig(subscriptions []types.SubscriptionCon
m.subscriptionCounter.Store(int32(maxSubID))
m.dispatcherCounter.Store(int32(maxDispatcherID))
// Load subscriptions (convert from types.SubscriptionConfig to Subscription)
// Load subscriptions, preserving runtime stats from existing ones
for _, sub := range subscriptions {
s := &Subscription{
ID: sub.ID,
@@ -104,6 +121,32 @@ func (m *Manager) HandleNotificationConfig(subscriptions []types.SubscriptionCon
Cooldown: sub.Cooldown,
SampleWindow: sub.SampleWindow,
}
if old, ok := existing[sub.ID]; ok {
s.TriggerCount.Store(old.TriggerCount.Load())
s.LastTriggeredAt.Store(old.LastTriggeredAt.Load())
// Clone TriggeredContainerIDs to avoid sharing with old subscription
s.TriggeredContainerIDs = xsync.NewMap[string, struct{}]()
if old.TriggeredContainerIDs != nil {
old.TriggeredContainerIDs.Range(func(id string, v struct{}) bool {
s.TriggeredContainerIDs.Store(id, v)
return true
})
}
// Clone MetricCooldowns to avoid sharing with old subscription
s.MetricCooldowns = xsync.NewMap[string, time.Time]()
if old.MetricCooldowns != nil {
old.MetricCooldowns.Range(func(id string, t time.Time) bool {
s.MetricCooldowns.Store(id, t)
return true
})
}
// MetricSampleBuffers: start fresh since ring buffers can't be safely cloned
}
if err := m.loadSubscription(s); err != nil {
return fmt.Errorf("failed to load subscription %s: %w", sub.Name, err)
}
+28
View File
@@ -298,6 +298,34 @@ func (m *Manager) Subscriptions() []*Subscription {
return result
}
// GetNotificationStats returns runtime stats for all subscriptions
func (m *Manager) GetNotificationStats() []types.SubscriptionStats {
var stats []types.SubscriptionStats
m.subscriptions.Range(func(_ int, sub *Subscription) bool {
var containerIDs []string
if sub.TriggeredContainerIDs != nil {
sub.TriggeredContainerIDs.Range(func(id string, _ struct{}) bool {
containerIDs = append(containerIDs, id)
return true
})
}
var lastTriggered *time.Time
if t := sub.LastTriggeredAt.Load(); t != nil && !t.IsZero() {
lastTriggered = t
}
stats = append(stats, types.SubscriptionStats{
SubscriptionID: sub.ID,
TriggerCount: sub.TriggerCount.Load(),
LastTriggeredAt: lastTriggered,
TriggeredContainerIDs: containerIDs,
})
return true
})
return stats
}
// Dispatchers returns all dispatchers as DispatcherConfig sorted by ID
func (m *Manager) Dispatchers() []DispatcherConfig {
result := make([]DispatcherConfig, 0)
+4
View File
@@ -29,6 +29,10 @@ type persistingNotificationHandler struct {
configPath string
}
func (h *persistingNotificationHandler) GetNotificationStats() []types.SubscriptionStats {
return h.manager.GetNotificationStats()
}
func (h *persistingNotificationHandler) HandleNotificationConfig(subscriptions []types.SubscriptionConfig, dispatchers []types.DispatcherConfig) error {
// Update the manager
if err := h.manager.HandleNotificationConfig(subscriptions, dispatchers); err != nil {
@@ -87,3 +87,7 @@ func (a *agentService) Exec(ctx context.Context, c container.Container, cmd []st
func (a *agentService) UpdateNotificationConfig(ctx context.Context, subscriptions []types.SubscriptionConfig, dispatchers []types.DispatcherConfig) error {
return a.client.UpdateNotificationConfig(ctx, subscriptions, dispatchers)
}
func (a *agentService) GetNotificationStats(ctx context.Context) ([]types.SubscriptionStats, error) {
return a.client.GetNotificationStats(ctx)
}
@@ -351,3 +351,78 @@ func (m *MultiHostService) Subscriptions() []*notification.Subscription {
func (m *MultiHostService) Dispatchers() []notification.DispatcherConfig {
return m.notificationManager.Dispatchers()
}
// NotificationStatsProvider is an interface for clients that can report notification stats
type NotificationStatsProvider interface {
GetNotificationStats(ctx context.Context) ([]types.SubscriptionStats, error)
}
// FetchAgentNotificationStats fetches and aggregates notification stats from all agent clients
func (m *MultiHostService) FetchAgentNotificationStats() map[int]types.SubscriptionStats {
// Collect providers
var providers []NotificationStatsProvider
for _, client := range m.manager.List() {
if provider, ok := client.(NotificationStatsProvider); ok {
providers = append(providers, provider)
}
}
if len(providers) == 0 {
return nil
}
// Fetch stats from all agents in parallel
allStats := lop.Map(providers, func(provider NotificationStatsProvider, _ int) []types.SubscriptionStats {
ctx, cancel := context.WithTimeout(context.Background(), m.timeout)
defer cancel()
stats, err := provider.GetNotificationStats(ctx)
if err != nil {
log.Debug().Err(err).Msg("Failed to fetch notification stats from agent")
return nil
}
return stats
})
// Aggregate sequentially
aggregated := make(map[int]types.SubscriptionStats)
for _, stats := range allStats {
for _, s := range stats {
existing, ok := aggregated[s.SubscriptionID]
if !ok {
// Dedup container IDs from this agent
seen := make(map[string]struct{}, len(s.TriggeredContainerIDs))
deduped := make([]string, 0, len(s.TriggeredContainerIDs))
for _, id := range s.TriggeredContainerIDs {
if _, exists := seen[id]; !exists {
seen[id] = struct{}{}
deduped = append(deduped, id)
}
}
s.TriggeredContainerIDs = deduped
aggregated[s.SubscriptionID] = s
continue
}
existing.TriggerCount += s.TriggerCount
if s.LastTriggeredAt != nil && (existing.LastTriggeredAt == nil || s.LastTriggeredAt.After(*existing.LastTriggeredAt)) {
existing.LastTriggeredAt = s.LastTriggeredAt
}
// Dedup container IDs across agents
seen := make(map[string]struct{}, len(existing.TriggeredContainerIDs))
for _, id := range existing.TriggeredContainerIDs {
seen[id] = struct{}{}
}
for _, id := range s.TriggeredContainerIDs {
if _, exists := seen[id]; !exists {
seen[id] = struct{}{}
existing.TriggeredContainerIDs = append(existing.TriggeredContainerIDs, id)
}
}
aggregated[s.SubscriptionID] = existing
}
}
return aggregated
}
@@ -10,6 +10,7 @@ import (
"github.com/amir20/dozzle/internal/notification"
"github.com/amir20/dozzle/internal/notification/dispatcher"
container_support "github.com/amir20/dozzle/internal/support/container"
"github.com/amir20/dozzle/types"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
@@ -173,3 +174,7 @@ func (m *K8sClusterService) RemoveDispatcher(id int) {
func (m *K8sClusterService) Dispatchers() []notification.DispatcherConfig {
return []notification.DispatcherConfig{}
}
func (m *K8sClusterService) FetchAgentNotificationStats() map[int]types.SubscriptionStats {
return nil
}
+37 -8
View File
@@ -106,12 +106,38 @@ type TestWebhookResult struct {
}
// Helper functions
func subscriptionToResponse(sub *notification.Subscription, dispatchers []notification.DispatcherConfig) *NotificationRuleResponse {
func subscriptionToResponse(sub *notification.Subscription, dispatchers []notification.DispatcherConfig, agentStats map[int]types.SubscriptionStats) *NotificationRuleResponse {
var lastTriggeredAt *time.Time
if t := sub.LastTriggeredAt.Load(); t != nil && !t.IsZero() {
lastTriggeredAt = t
}
triggerCount := sub.TriggerCount.Load()
triggeredContainers := sub.TriggeredContainersCount()
// Merge agent stats if available
if as, ok := agentStats[sub.ID]; ok {
triggerCount += as.TriggerCount
if as.LastTriggeredAt != nil && (lastTriggeredAt == nil || as.LastTriggeredAt.After(*lastTriggeredAt)) {
lastTriggeredAt = as.LastTriggeredAt
}
// Count unique container IDs from agents (deduplicated with local)
agentContainerSet := make(map[string]struct{}, len(as.TriggeredContainerIDs))
for _, id := range as.TriggeredContainerIDs {
agentContainerSet[id] = struct{}{}
}
// Subtract containers already counted locally
if sub.TriggeredContainerIDs != nil {
sub.TriggeredContainerIDs.Range(func(id string, _ struct{}) bool {
delete(agentContainerSet, id)
return true
})
}
triggeredContainers += len(agentContainerSet)
}
var disp *DispatcherResponse
for _, d := range dispatchers {
if d.ID == sub.DispatcherID {
@@ -130,9 +156,9 @@ func subscriptionToResponse(sub *notification.Subscription, dispatchers []notifi
MetricExpression: sub.MetricExpression,
Cooldown: sub.Cooldown,
SampleWindow: sub.SampleWindow,
TriggerCount: sub.TriggerCount.Load(),
TriggerCount: triggerCount,
LastTriggeredAt: lastTriggeredAt,
TriggeredContainers: sub.TriggeredContainersCount(),
TriggeredContainers: triggeredContainers,
}
}
@@ -183,9 +209,10 @@ func writeError(w http.ResponseWriter, status int, message string) {
func (h *handler) listNotificationRules(w http.ResponseWriter, r *http.Request) {
subscriptions := h.hostService.Subscriptions()
dispatchers := h.hostService.Dispatchers()
agentStats := h.hostService.FetchAgentNotificationStats()
rules := make([]*NotificationRuleResponse, len(subscriptions))
for i, sub := range subscriptions {
rules[i] = subscriptionToResponse(sub, dispatchers)
rules[i] = subscriptionToResponse(sub, dispatchers, agentStats)
}
writeJSON(w, http.StatusOK, rules)
}
@@ -198,9 +225,10 @@ func (h *handler) getNotificationRule(w http.ResponseWriter, r *http.Request) {
}
dispatchers := h.hostService.Dispatchers()
agentStats := h.hostService.FetchAgentNotificationStats()
for _, sub := range h.hostService.Subscriptions() {
if sub.ID == id {
writeJSON(w, http.StatusOK, subscriptionToResponse(sub, dispatchers))
writeJSON(w, http.StatusOK, subscriptionToResponse(sub, dispatchers, agentStats))
return
}
}
@@ -230,7 +258,7 @@ func (h *handler) createNotificationRule(w http.ResponseWriter, r *http.Request)
return
}
writeJSON(w, http.StatusCreated, subscriptionToResponse(sub, h.hostService.Dispatchers()))
writeJSON(w, http.StatusCreated, subscriptionToResponse(sub, h.hostService.Dispatchers(), nil))
}
func (h *handler) replaceNotificationRule(w http.ResponseWriter, r *http.Request) {
@@ -263,7 +291,7 @@ func (h *handler) replaceNotificationRule(w http.ResponseWriter, r *http.Request
return
}
writeJSON(w, http.StatusOK, subscriptionToResponse(sub, h.hostService.Dispatchers()))
writeJSON(w, http.StatusOK, subscriptionToResponse(sub, h.hostService.Dispatchers(), nil))
}
func (h *handler) updateNotificationRule(w http.ResponseWriter, r *http.Request) {
@@ -312,9 +340,10 @@ func (h *handler) updateNotificationRule(w http.ResponseWriter, r *http.Request)
// Fetch the updated subscription
dispatchers := h.hostService.Dispatchers()
agentStats := h.hostService.FetchAgentNotificationStats()
for _, sub := range h.hostService.Subscriptions() {
if sub.ID == id {
writeJSON(w, http.StatusOK, subscriptionToResponse(sub, dispatchers))
writeJSON(w, http.StatusOK, subscriptionToResponse(sub, dispatchers, agentStats))
return
}
}
+2
View File
@@ -13,6 +13,7 @@ import (
"github.com/amir20/dozzle/internal/notification"
"github.com/amir20/dozzle/internal/notification/dispatcher"
container_support "github.com/amir20/dozzle/internal/support/container"
"github.com/amir20/dozzle/types"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
@@ -85,6 +86,7 @@ type HostService interface {
UpdateDispatcher(id int, d dispatcher.Dispatcher)
RemoveDispatcher(id int)
Dispatchers() []notification.DispatcherConfig
FetchAgentNotificationStats() map[int]types.SubscriptionStats
}
type handler struct {
+7
View File
@@ -21,6 +21,7 @@ service AgentService {
rpc ContainerExec(stream ContainerExecRequest) returns (stream ContainerExecResponse) {}
rpc ContainerAttach(stream ContainerAttachRequest) returns (stream ContainerAttachResponse) {}
rpc UpdateNotificationConfig(UpdateNotificationConfigRequest) returns (UpdateNotificationConfigResponse) {}
rpc GetNotificationStats(GetNotificationStatsRequest) returns (GetNotificationStatsResponse) {}
}
message ListContainersRequest {
@@ -135,3 +136,9 @@ message UpdateNotificationConfigRequest {
}
message UpdateNotificationConfigResponse {}
message GetNotificationStatsRequest {}
message GetNotificationStatsResponse {
repeated NotificationSubscriptionStats stats = 1;
}
+7
View File
@@ -113,3 +113,10 @@ message NotificationDispatcher {
string template = 5;
map<string, string> headers = 6;
}
message NotificationSubscriptionStats {
int32 subscriptionId = 1;
int64 triggerCount = 2;
google.protobuf.Timestamp lastTriggeredAt = 3;
repeated string triggeredContainerIds = 4;
}
+8
View File
@@ -64,6 +64,14 @@ type SubscriptionConfig struct {
SampleWindow int `json:"sampleWindow,omitempty"`
}
// SubscriptionStats represents runtime stats for a notification subscription
type SubscriptionStats struct {
SubscriptionID int `json:"subscriptionId"`
TriggerCount int64 `json:"triggerCount"`
LastTriggeredAt *time.Time `json:"lastTriggeredAt,omitempty"`
TriggeredContainerIDs []string `json:"triggeredContainerIds"`
}
// DispatcherConfig represents a notification dispatcher configuration
type DispatcherConfig struct {
ID int