feat(cloud): add GetAlerts to the cloud tool service (#4913)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Amir Raminfar
2026-08-16 14:53:33 -07:00
committed by GitHub
co-authored by Claude Opus 5
parent a5ef6a77e3
commit 0d03442e58
3 changed files with 533 additions and 16 deletions
+395 -16
View File
@@ -2404,6 +2404,347 @@ func (x *SearchLogHit) GetLogId() uint32 {
return 0
}
// Cloud alert history, merged into the local log stream on scrollback.
type GetAlertsRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Containers currently in the viewer. Batched into one call because the
// viewer fans out over N containers per scroll and must not pay N round
// trips to cloud. Empty -> empty result.
ContainerIds []string `protobuf:"bytes,1,rep,name=container_ids,json=containerIds,proto3" json:"container_ids,omitempty"`
// Optional filter — narrow to a specific Docker host inside the instance.
// Empty = all hosts under this instance.
HostId string `protobuf:"bytes,2,opt,name=host_id,json=hostId,proto3" json:"host_id,omitempty"`
// Window to fetch, matching the [from, to] the viewer just loaded logs for.
FromTsNs int64 `protobuf:"varint,3,opt,name=from_ts_ns,json=fromTsNs,proto3" json:"from_ts_ns,omitempty"`
ToTsNs int64 `protobuf:"varint,4,opt,name=to_ts_ns,json=toTsNs,proto3" json:"to_ts_ns,omitempty"`
// Result cap. Default 50, server-capped at 200. A single scroll window
// covering a noisy incident can legitimately hold many alerts, so this
// ceiling is higher than the log-search one.
Limit int32 `protobuf:"varint,5,opt,name=limit,proto3" json:"limit,omitempty"`
// Include follow-up anchors — points where an already-open incident was
// still firing, as opposed to where it first fired. See AlertHit.is_origin.
// Default false keeps a long-running incident to a single card.
IncludeFollowUps bool `protobuf:"varint,6,opt,name=include_follow_ups,json=includeFollowUps,proto3" json:"include_follow_ups,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetAlertsRequest) Reset() {
*x = GetAlertsRequest{}
mi := &file_cloud_proto_msgTypes[27]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetAlertsRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetAlertsRequest) ProtoMessage() {}
func (x *GetAlertsRequest) ProtoReflect() protoreflect.Message {
mi := &file_cloud_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 GetAlertsRequest.ProtoReflect.Descriptor instead.
func (*GetAlertsRequest) Descriptor() ([]byte, []int) {
return file_cloud_proto_rawDescGZIP(), []int{27}
}
func (x *GetAlertsRequest) GetContainerIds() []string {
if x != nil {
return x.ContainerIds
}
return nil
}
func (x *GetAlertsRequest) GetHostId() string {
if x != nil {
return x.HostId
}
return ""
}
func (x *GetAlertsRequest) GetFromTsNs() int64 {
if x != nil {
return x.FromTsNs
}
return 0
}
func (x *GetAlertsRequest) GetToTsNs() int64 {
if x != nil {
return x.ToTsNs
}
return 0
}
func (x *GetAlertsRequest) GetLimit() int32 {
if x != nil {
return x.Limit
}
return 0
}
func (x *GetAlertsRequest) GetIncludeFollowUps() bool {
if x != nil {
return x.IncludeFollowUps
}
return false
}
type AlertHit struct {
state protoimpl.MessageState `protogen:"open.v1"`
AlertId int64 `protobuf:"varint,1,opt,name=alert_id,json=alertId,proto3" json:"alert_id,omitempty"`
ContainerId string `protobuf:"bytes,2,opt,name=container_id,json=containerId,proto3" json:"container_id,omitempty"`
HostId string `protobuf:"bytes,3,opt,name=host_id,json=hostId,proto3" json:"host_id,omitempty"`
// FNV-32a hash of the log line that triggered this alert — the same id
// Dozzle stamps on LogEvent.Id. Lets the viewer splice the alert in
// directly after its trigger line instead of guessing from a timestamp.
// 0 when the alert is not log-anchored (metric and event alerts).
LogId uint32 `protobuf:"varint,4,opt,name=log_id,json=logId,proto3" json:"log_id,omitempty"`
// Timestamp to anchor on when log_id is 0, or when the trigger line isn't
// in the loaded window.
AnchorTsNs int64 `protobuf:"varint,5,opt,name=anchor_ts_ns,json=anchorTsNs,proto3" json:"anchor_ts_ns,omitempty"`
// Triage's one-line summary. Falls back to the alert title when no
// summarizer LLM was involved.
Headline string `protobuf:"bytes,6,opt,name=headline,proto3" json:"headline,omitempty"`
Level string `protobuf:"bytes,7,opt,name=level,proto3" json:"level,omitempty"`
// Raw events folded into this alert, and follow-up batches triage absorbed
// silently after it was first sent.
EventCount int32 `protobuf:"varint,8,opt,name=event_count,json=eventCount,proto3" json:"event_count,omitempty"`
SuppressedCount int32 `protobuf:"varint,9,opt,name=suppressed_count,json=suppressedCount,proto3" json:"suppressed_count,omitempty"`
// Distinct containers this alert spans. > 1 means the incident is wider
// than the container currently being viewed.
ContainerCount int32 `protobuf:"varint,10,opt,name=container_count,json=containerCount,proto3" json:"container_count,omitempty"`
// Triage's longer write-up. May be empty. Rendered collapsed.
Investigation string `protobuf:"bytes,11,opt,name=investigation,proto3" json:"investigation,omitempty"`
// 'send_new' | 'fallback' | ”. Never 'suppress' or 'update_existing':
// cloud folds those into the incident's original row rather than inserting
// one, so every hit is an incident, not a triage decision.
TriageAction string `protobuf:"bytes,12,opt,name=triage_action,json=triageAction,proto3" json:"triage_action,omitempty"`
// True when this hit anchors where the incident FIRST fired. False for a
// follow-up anchor: cloud appends every folded batch's events to the
// original alert, so one incident can have activity in many scroll windows.
// The viewer renders a full card at the origin and a compact "still
// happening" marker at follow-ups.
IsOrigin bool `protobuf:"varint,16,opt,name=is_origin,json=isOrigin,proto3" json:"is_origin,omitempty"`
CreatedAtNs int64 `protobuf:"varint,13,opt,name=created_at_ns,json=createdAtNs,proto3" json:"created_at_ns,omitempty"`
// Bumped every time a follow-up batch folds into this alert. Drives the
// "still happening" state on the card.
LastActivityAtNs int64 `protobuf:"varint,14,opt,name=last_activity_at_ns,json=lastActivityAtNs,proto3" json:"last_activity_at_ns,omitempty"`
// Deep link to the alert page in Dozzle Cloud, built from the API key's
// app_url. Empty when the key has no app_url configured.
Url string `protobuf:"bytes,15,opt,name=url,proto3" json:"url,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *AlertHit) Reset() {
*x = AlertHit{}
mi := &file_cloud_proto_msgTypes[28]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *AlertHit) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*AlertHit) ProtoMessage() {}
func (x *AlertHit) ProtoReflect() protoreflect.Message {
mi := &file_cloud_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 AlertHit.ProtoReflect.Descriptor instead.
func (*AlertHit) Descriptor() ([]byte, []int) {
return file_cloud_proto_rawDescGZIP(), []int{28}
}
func (x *AlertHit) GetAlertId() int64 {
if x != nil {
return x.AlertId
}
return 0
}
func (x *AlertHit) GetContainerId() string {
if x != nil {
return x.ContainerId
}
return ""
}
func (x *AlertHit) GetHostId() string {
if x != nil {
return x.HostId
}
return ""
}
func (x *AlertHit) GetLogId() uint32 {
if x != nil {
return x.LogId
}
return 0
}
func (x *AlertHit) GetAnchorTsNs() int64 {
if x != nil {
return x.AnchorTsNs
}
return 0
}
func (x *AlertHit) GetHeadline() string {
if x != nil {
return x.Headline
}
return ""
}
func (x *AlertHit) GetLevel() string {
if x != nil {
return x.Level
}
return ""
}
func (x *AlertHit) GetEventCount() int32 {
if x != nil {
return x.EventCount
}
return 0
}
func (x *AlertHit) GetSuppressedCount() int32 {
if x != nil {
return x.SuppressedCount
}
return 0
}
func (x *AlertHit) GetContainerCount() int32 {
if x != nil {
return x.ContainerCount
}
return 0
}
func (x *AlertHit) GetInvestigation() string {
if x != nil {
return x.Investigation
}
return ""
}
func (x *AlertHit) GetTriageAction() string {
if x != nil {
return x.TriageAction
}
return ""
}
func (x *AlertHit) GetIsOrigin() bool {
if x != nil {
return x.IsOrigin
}
return false
}
func (x *AlertHit) GetCreatedAtNs() int64 {
if x != nil {
return x.CreatedAtNs
}
return 0
}
func (x *AlertHit) GetLastActivityAtNs() int64 {
if x != nil {
return x.LastActivityAtNs
}
return 0
}
func (x *AlertHit) GetUrl() string {
if x != nil {
return x.Url
}
return ""
}
type GetAlertsResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Hits []*AlertHit `protobuf:"bytes,1,rep,name=hits,proto3" json:"hits,omitempty"`
// The window held more alerts than the limit allowed. The viewer shows a
// "more alerts in this range" affordance rather than paginating — unlike
// log search, scrollback windows are already small and bounded.
Truncated bool `protobuf:"varint,2,opt,name=truncated,proto3" json:"truncated,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetAlertsResponse) Reset() {
*x = GetAlertsResponse{}
mi := &file_cloud_proto_msgTypes[29]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetAlertsResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetAlertsResponse) ProtoMessage() {}
func (x *GetAlertsResponse) ProtoReflect() protoreflect.Message {
mi := &file_cloud_proto_msgTypes[29]
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 GetAlertsResponse.ProtoReflect.Descriptor instead.
func (*GetAlertsResponse) Descriptor() ([]byte, []int) {
return file_cloud_proto_rawDescGZIP(), []int{29}
}
func (x *GetAlertsResponse) GetHits() []*AlertHit {
if x != nil {
return x.Hits
}
return nil
}
func (x *GetAlertsResponse) GetTruncated() bool {
if x != nil {
return x.Truncated
}
return false
}
var File_cloud_proto protoreflect.FileDescriptor
const file_cloud_proto_rawDesc = "" +
@@ -2605,17 +2946,49 @@ const file_cloud_proto_rawDesc = "" +
"\amessage\x18\x05 \x01(\tR\amessage\x12\x16\n" +
"\x06stream\x18\x06 \x01(\tR\x06stream\x12\x14\n" +
"\x05level\x18\a \x01(\tR\x05level\x12\x15\n" +
"\x06log_id\x18\b \x01(\rR\x05logId*o\n" +
"\x06log_id\x18\b \x01(\rR\x05logId\"\xcc\x01\n" +
"\x10GetAlertsRequest\x12#\n" +
"\rcontainer_ids\x18\x01 \x03(\tR\fcontainerIds\x12\x17\n" +
"\ahost_id\x18\x02 \x01(\tR\x06hostId\x12\x1c\n" +
"\n" +
"from_ts_ns\x18\x03 \x01(\x03R\bfromTsNs\x12\x18\n" +
"\bto_ts_ns\x18\x04 \x01(\x03R\x06toTsNs\x12\x14\n" +
"\x05limit\x18\x05 \x01(\x05R\x05limit\x12,\n" +
"\x12include_follow_ups\x18\x06 \x01(\bR\x10includeFollowUps\"\x8e\x04\n" +
"\bAlertHit\x12\x19\n" +
"\balert_id\x18\x01 \x01(\x03R\aalertId\x12!\n" +
"\fcontainer_id\x18\x02 \x01(\tR\vcontainerId\x12\x17\n" +
"\ahost_id\x18\x03 \x01(\tR\x06hostId\x12\x15\n" +
"\x06log_id\x18\x04 \x01(\rR\x05logId\x12 \n" +
"\fanchor_ts_ns\x18\x05 \x01(\x03R\n" +
"anchorTsNs\x12\x1a\n" +
"\bheadline\x18\x06 \x01(\tR\bheadline\x12\x14\n" +
"\x05level\x18\a \x01(\tR\x05level\x12\x1f\n" +
"\vevent_count\x18\b \x01(\x05R\n" +
"eventCount\x12)\n" +
"\x10suppressed_count\x18\t \x01(\x05R\x0fsuppressedCount\x12'\n" +
"\x0fcontainer_count\x18\n" +
" \x01(\x05R\x0econtainerCount\x12$\n" +
"\rinvestigation\x18\v \x01(\tR\rinvestigation\x12#\n" +
"\rtriage_action\x18\f \x01(\tR\ftriageAction\x12\x1b\n" +
"\tis_origin\x18\x10 \x01(\bR\bisOrigin\x12\"\n" +
"\rcreated_at_ns\x18\r \x01(\x03R\vcreatedAtNs\x12-\n" +
"\x13last_activity_at_ns\x18\x0e \x01(\x03R\x10lastActivityAtNs\x12\x10\n" +
"\x03url\x18\x0f \x01(\tR\x03url\"V\n" +
"\x11GetAlertsResponse\x12#\n" +
"\x04hits\x18\x01 \x03(\v2\x0f.cloud.AlertHitR\x04hits\x12\x1c\n" +
"\ttruncated\x18\x02 \x01(\bR\ttruncated*o\n" +
"\tToolScope\x12\x1a\n" +
"\x16TOOL_SCOPE_UNSPECIFIED\x10\x00\x12\x17\n" +
"\x13TOOL_SCOPE_INSTANCE\x10\x01\x12\x13\n" +
"\x0fTOOL_SCOPE_HOST\x10\x02\x12\x18\n" +
"\x14TOOL_SCOPE_CONTAINER\x10\x032\x90\x01\n" +
"\x14TOOL_SCOPE_CONTAINER\x10\x032\xd0\x01\n" +
"\x10CloudToolService\x129\n" +
"\n" +
"ToolStream\x12\x13.cloud.ToolResponse\x1a\x12.cloud.ToolRequest(\x010\x01\x12A\n" +
"\n" +
"SearchLogs\x12\x18.cloud.SearchLogsRequest\x1a\x19.cloud.SearchLogsResponseB&Z$github.com/amir20/dozzle/proto/cloudb\x06proto3"
"SearchLogs\x12\x18.cloud.SearchLogsRequest\x1a\x19.cloud.SearchLogsResponse\x12>\n" +
"\tGetAlerts\x12\x17.cloud.GetAlertsRequest\x1a\x18.cloud.GetAlertsResponseB&Z$github.com/amir20/dozzle/proto/cloudb\x06proto3"
var (
file_cloud_proto_rawDescOnce sync.Once
@@ -2630,7 +3003,7 @@ func file_cloud_proto_rawDescGZIP() []byte {
}
var file_cloud_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_cloud_proto_msgTypes = make([]protoimpl.MessageInfo, 28)
var file_cloud_proto_msgTypes = make([]protoimpl.MessageInfo, 31)
var file_cloud_proto_goTypes = []any{
(ToolScope)(0), // 0: cloud.ToolScope
(*ToolRequest)(nil), // 1: cloud.ToolRequest
@@ -2660,7 +3033,10 @@ var file_cloud_proto_goTypes = []any{
(*SearchLogsRequest)(nil), // 25: cloud.SearchLogsRequest
(*SearchLogsResponse)(nil), // 26: cloud.SearchLogsResponse
(*SearchLogHit)(nil), // 27: cloud.SearchLogHit
nil, // 28: cloud.InspectContainerResult.LabelsEntry
(*GetAlertsRequest)(nil), // 28: cloud.GetAlertsRequest
(*AlertHit)(nil), // 29: cloud.AlertHit
(*GetAlertsResponse)(nil), // 30: cloud.GetAlertsResponse
nil, // 31: cloud.InspectContainerResult.LabelsEntry
}
var file_cloud_proto_depIdxs = []int32{
7, // 0: cloud.ToolRequest.list_tools:type_name -> cloud.ListToolsRequest
@@ -2686,17 +3062,20 @@ var file_cloud_proto_depIdxs = []int32{
15, // 20: cloud.ListContainersResult.containers:type_name -> cloud.ContainerInfo
17, // 21: cloud.ContainerStatsResult.stats:type_name -> cloud.ContainerStatEntry
19, // 22: cloud.FetchLogsResult.entries:type_name -> cloud.LogEntry
28, // 23: cloud.InspectContainerResult.labels:type_name -> cloud.InspectContainerResult.LabelsEntry
31, // 23: cloud.InspectContainerResult.labels:type_name -> cloud.InspectContainerResult.LabelsEntry
27, // 24: cloud.SearchLogsResponse.hits:type_name -> cloud.SearchLogHit
2, // 25: cloud.CloudToolService.ToolStream:input_type -> cloud.ToolResponse
25, // 26: cloud.CloudToolService.SearchLogs:input_type -> cloud.SearchLogsRequest
1, // 27: cloud.CloudToolService.ToolStream:output_type -> cloud.ToolRequest
26, // 28: cloud.CloudToolService.SearchLogs:output_type -> cloud.SearchLogsResponse
27, // [27:29] is the sub-list for method output_type
25, // [25:27] is the sub-list for method input_type
25, // [25:25] is the sub-list for extension type_name
25, // [25:25] is the sub-list for extension extendee
0, // [0:25] is the sub-list for field type_name
29, // 25: cloud.GetAlertsResponse.hits:type_name -> cloud.AlertHit
2, // 26: cloud.CloudToolService.ToolStream:input_type -> cloud.ToolResponse
25, // 27: cloud.CloudToolService.SearchLogs:input_type -> cloud.SearchLogsRequest
28, // 28: cloud.CloudToolService.GetAlerts:input_type -> cloud.GetAlertsRequest
1, // 29: cloud.CloudToolService.ToolStream:output_type -> cloud.ToolRequest
26, // 30: cloud.CloudToolService.SearchLogs:output_type -> cloud.SearchLogsResponse
30, // 31: cloud.CloudToolService.GetAlerts:output_type -> cloud.GetAlertsResponse
29, // [29:32] is the sub-list for method output_type
26, // [26:29] is the sub-list for method input_type
26, // [26:26] is the sub-list for extension type_name
26, // [26:26] is the sub-list for extension extendee
0, // [0:26] is the sub-list for field type_name
}
func init() { file_cloud_proto_init() }
@@ -2731,7 +3110,7 @@ func file_cloud_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_cloud_proto_rawDesc), len(file_cloud_proto_rawDesc)),
NumEnums: 1,
NumMessages: 28,
NumMessages: 31,
NumExtensions: 0,
NumServices: 1,
},
+54
View File
@@ -21,6 +21,7 @@ const _ = grpc.SupportPackageIsVersion9
const (
CloudToolService_ToolStream_FullMethodName = "/cloud.CloudToolService/ToolStream"
CloudToolService_SearchLogs_FullMethodName = "/cloud.CloudToolService/SearchLogs"
CloudToolService_GetAlerts_FullMethodName = "/cloud.CloudToolService/GetAlerts"
)
// CloudToolServiceClient is the client API for CloudToolService service.
@@ -34,6 +35,15 @@ type CloudToolServiceClient interface {
// to the (user_id, api_key_id) derived from the authenticated connection;
// Dozzle does NOT pass any identity fields in the request.
SearchLogs(ctx context.Context, in *SearchLogsRequest, opts ...grpc.CallOption) (*SearchLogsResponse, error)
// Dozzle-initiated unary call: fetch alerts that fired for these containers
// inside a time window, so the log viewer can merge them into the stream as
// the user scrolls back. Like SearchLogs, cloud scopes the query server-side
// to the (user_id, api_key_id) derived from the authenticated connection;
// Dozzle passes no identity fields. Unlike SearchLogs this does NOT require
// the streamLogs opt-in — alerts live in cloud's own database rather than
// the log store, so they exist for anyone who linked cloud and configured a
// subscription.
GetAlerts(ctx context.Context, in *GetAlertsRequest, opts ...grpc.CallOption) (*GetAlertsResponse, error)
}
type cloudToolServiceClient struct {
@@ -67,6 +77,16 @@ func (c *cloudToolServiceClient) SearchLogs(ctx context.Context, in *SearchLogsR
return out, nil
}
func (c *cloudToolServiceClient) GetAlerts(ctx context.Context, in *GetAlertsRequest, opts ...grpc.CallOption) (*GetAlertsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetAlertsResponse)
err := c.cc.Invoke(ctx, CloudToolService_GetAlerts_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// CloudToolServiceServer is the server API for CloudToolService service.
// All implementations must embed UnimplementedCloudToolServiceServer
// for forward compatibility.
@@ -78,6 +98,15 @@ type CloudToolServiceServer interface {
// to the (user_id, api_key_id) derived from the authenticated connection;
// Dozzle does NOT pass any identity fields in the request.
SearchLogs(context.Context, *SearchLogsRequest) (*SearchLogsResponse, error)
// Dozzle-initiated unary call: fetch alerts that fired for these containers
// inside a time window, so the log viewer can merge them into the stream as
// the user scrolls back. Like SearchLogs, cloud scopes the query server-side
// to the (user_id, api_key_id) derived from the authenticated connection;
// Dozzle passes no identity fields. Unlike SearchLogs this does NOT require
// the streamLogs opt-in — alerts live in cloud's own database rather than
// the log store, so they exist for anyone who linked cloud and configured a
// subscription.
GetAlerts(context.Context, *GetAlertsRequest) (*GetAlertsResponse, error)
mustEmbedUnimplementedCloudToolServiceServer()
}
@@ -94,6 +123,9 @@ func (UnimplementedCloudToolServiceServer) ToolStream(grpc.BidiStreamingServer[T
func (UnimplementedCloudToolServiceServer) SearchLogs(context.Context, *SearchLogsRequest) (*SearchLogsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method SearchLogs not implemented")
}
func (UnimplementedCloudToolServiceServer) GetAlerts(context.Context, *GetAlertsRequest) (*GetAlertsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetAlerts not implemented")
}
func (UnimplementedCloudToolServiceServer) mustEmbedUnimplementedCloudToolServiceServer() {}
func (UnimplementedCloudToolServiceServer) testEmbeddedByValue() {}
@@ -140,6 +172,24 @@ func _CloudToolService_SearchLogs_Handler(srv interface{}, ctx context.Context,
return interceptor(ctx, in, info, handler)
}
func _CloudToolService_GetAlerts_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetAlertsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(CloudToolServiceServer).GetAlerts(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: CloudToolService_GetAlerts_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(CloudToolServiceServer).GetAlerts(ctx, req.(*GetAlertsRequest))
}
return interceptor(ctx, in, info, handler)
}
// CloudToolService_ServiceDesc is the grpc.ServiceDesc for CloudToolService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@@ -151,6 +201,10 @@ var CloudToolService_ServiceDesc = grpc.ServiceDesc{
MethodName: "SearchLogs",
Handler: _CloudToolService_SearchLogs_Handler,
},
{
MethodName: "GetAlerts",
Handler: _CloudToolService_GetAlerts_Handler,
},
},
Streams: []grpc.StreamDesc{
{
+84
View File
@@ -13,6 +13,16 @@ service CloudToolService {
// to the (user_id, api_key_id) derived from the authenticated connection;
// Dozzle does NOT pass any identity fields in the request.
rpc SearchLogs(SearchLogsRequest) returns (SearchLogsResponse);
// Dozzle-initiated unary call: fetch alerts that fired for these containers
// inside a time window, so the log viewer can merge them into the stream as
// the user scrolls back. Like SearchLogs, cloud scopes the query server-side
// to the (user_id, api_key_id) derived from the authenticated connection;
// Dozzle passes no identity fields. Unlike SearchLogs this does NOT require
// the streamLogs opt-in — alerts live in cloud's own database rather than
// the log store, so they exist for anyone who linked cloud and configured a
// subscription.
rpc GetAlerts(GetAlertsRequest) returns (GetAlertsResponse);
}
message ToolRequest {
@@ -329,3 +339,77 @@ message SearchLogHit {
// straight to the matching line.
uint32 log_id = 8;
}
// Cloud alert history, merged into the local log stream on scrollback.
message GetAlertsRequest {
// Containers currently in the viewer. Batched into one call because the
// viewer fans out over N containers per scroll and must not pay N round
// trips to cloud. Empty -> empty result.
repeated string container_ids = 1;
// Optional filter — narrow to a specific Docker host inside the instance.
// Empty = all hosts under this instance.
string host_id = 2;
// Window to fetch, matching the [from, to] the viewer just loaded logs for.
int64 from_ts_ns = 3;
int64 to_ts_ns = 4;
// Result cap. Default 50, server-capped at 200. A single scroll window
// covering a noisy incident can legitimately hold many alerts, so this
// ceiling is higher than the log-search one.
int32 limit = 5;
// Include follow-up anchors — points where an already-open incident was
// still firing, as opposed to where it first fired. See AlertHit.is_origin.
// Default false keeps a long-running incident to a single card.
bool include_follow_ups = 6;
}
message AlertHit {
int64 alert_id = 1;
string container_id = 2;
string host_id = 3;
// FNV-32a hash of the log line that triggered this alert — the same id
// Dozzle stamps on LogEvent.Id. Lets the viewer splice the alert in
// directly after its trigger line instead of guessing from a timestamp.
// 0 when the alert is not log-anchored (metric and event alerts).
uint32 log_id = 4;
// Timestamp to anchor on when log_id is 0, or when the trigger line isn't
// in the loaded window.
int64 anchor_ts_ns = 5;
// Triage's one-line summary. Falls back to the alert title when no
// summarizer LLM was involved.
string headline = 6;
string level = 7;
// Raw events folded into this alert, and follow-up batches triage absorbed
// silently after it was first sent.
int32 event_count = 8;
int32 suppressed_count = 9;
// Distinct containers this alert spans. > 1 means the incident is wider
// than the container currently being viewed.
int32 container_count = 10;
// Triage's longer write-up. May be empty. Rendered collapsed.
string investigation = 11;
// 'send_new' | 'fallback' | ''. Never 'suppress' or 'update_existing':
// cloud folds those into the incident's original row rather than inserting
// one, so every hit is an incident, not a triage decision.
string triage_action = 12;
// True when this hit anchors where the incident FIRST fired. False for a
// follow-up anchor: cloud appends every folded batch's events to the
// original alert, so one incident can have activity in many scroll windows.
// The viewer renders a full card at the origin and a compact "still
// happening" marker at follow-ups.
bool is_origin = 16;
int64 created_at_ns = 13;
// Bumped every time a follow-up batch folds into this alert. Drives the
// "still happening" state on the card.
int64 last_activity_at_ns = 14;
// Deep link to the alert page in Dozzle Cloud, built from the API key's
// app_url. Empty when the key has no app_url configured.
string url = 15;
}
message GetAlertsResponse {
repeated AlertHit hits = 1;
// The window held more alerts than the limit allowed. The viewer shows a
// "more alerts in this range" affordance rather than paginating — unlike
// log search, scrollback windows are already small and bounded.
bool truncated = 2;
}