refactor(cloud): treat alert ids as opaque strings (#4924)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
82dea58f40
commit
11d2894f04
@@ -17,7 +17,7 @@ const ns = (n: number) => n * 1_000_000;
|
||||
|
||||
function mountAlert(overrides: Partial<CloudAlert> = {}) {
|
||||
const alert: CloudAlert = {
|
||||
alertId: 1,
|
||||
alertId: "6teOh3RH",
|
||||
containerId: "abc",
|
||||
hostId: "h",
|
||||
ts: ns(1_000_000),
|
||||
|
||||
@@ -23,7 +23,7 @@ function log(id: number, at: number, containerID = "abc"): LogEntry<LogMessage>
|
||||
|
||||
function alert(overrides: Partial<CloudAlert> = {}): CloudAlert {
|
||||
return {
|
||||
alertId: 1,
|
||||
alertId: "1",
|
||||
containerId: "abc",
|
||||
hostId: "h",
|
||||
ts: ns(100),
|
||||
@@ -91,7 +91,7 @@ describe("mergeAlerts", () => {
|
||||
const logs = [log(10, 500)];
|
||||
const merged = mergeAlerts(
|
||||
logs,
|
||||
[alert({ alertId: 2, ts: ns(300) }), alert({ alertId: 1, ts: ns(100) })],
|
||||
[alert({ alertId: "2", ts: ns(300) }), alert({ alertId: "1", ts: ns(100) })],
|
||||
new Set(),
|
||||
);
|
||||
|
||||
@@ -138,7 +138,7 @@ describe("mergeAlerts", () => {
|
||||
const logs = [log(10, 100)];
|
||||
const merged = mergeAlerts(
|
||||
logs,
|
||||
[alert({ alertId: 1, isOrigin: false, ts: ns(50) }), alert({ alertId: 1, isOrigin: true, ts: ns(100) })],
|
||||
[alert({ alertId: "1", isOrigin: false, ts: ns(50) }), alert({ alertId: "1", isOrigin: true, ts: ns(100) })],
|
||||
new Set(),
|
||||
);
|
||||
expect(shapeOf(merged)).toEqual(["alert:1", "log:10"]);
|
||||
@@ -236,7 +236,7 @@ describe("attachEvents", () => {
|
||||
// badging its line too would say the same thing twice.
|
||||
test("does not badge a line whose event produced an alert", () => {
|
||||
const logs = [log(10, 100)];
|
||||
expect(attachEvents(logs, [event({ suppressed: false, alertId: 7 })])).toBe(false);
|
||||
expect(attachEvents(logs, [event({ suppressed: false, alertId: "7" })])).toBe(false);
|
||||
expect(logs[0].matchedEvent).toBeUndefined();
|
||||
});
|
||||
|
||||
|
||||
@@ -6,7 +6,8 @@ import { AlertLogEntry, CloudEventLogEntry, LogEntry, type LogMessage } from "@/
|
||||
* /api/cloud/alerts. Mirrors cloud.AlertHit on the Go side.
|
||||
*/
|
||||
export interface CloudAlert {
|
||||
alertId: number;
|
||||
/** Opaque, sqids-encoded. Never a raw row id. */
|
||||
alertId: string;
|
||||
containerId: string;
|
||||
hostId: string;
|
||||
/**
|
||||
@@ -49,7 +50,8 @@ export interface CloudEvent {
|
||||
type?: string;
|
||||
/** Human-readable summary; the only renderable text a non-log event has. */
|
||||
detail?: string;
|
||||
alertId?: number;
|
||||
/** Opaque id of the alert this event reached, empty when it reached none. */
|
||||
alertId?: string;
|
||||
suppressed: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ export function useLogLoader(
|
||||
) {
|
||||
const { fetchAlerts, available: alertsAvailable } = useCloudAlerts();
|
||||
// Anchor keys already placed, so overlapping scroll windows don't duplicate.
|
||||
// Keyed on (alertId, anchor) rather than alertId: one incident legitimately
|
||||
// Keyed on (alert, anchor) rather than the alert alone: one incident legitimately
|
||||
// marks every window it was active in.
|
||||
const placedAlerts = new Set<string>();
|
||||
// Newest log timestamp the poll has already asked Cloud about. Events only
|
||||
|
||||
@@ -227,7 +227,10 @@ export class AlertLogEntry extends LogEntry<string> {
|
||||
// std/level feed the shared LogItem chrome. Alerts are not stream output,
|
||||
// but they are unmistakably not stdout either, and the level is the one
|
||||
// triage assigned.
|
||||
super(alert.headline, alert.containerId, alert.alertId, date, "stderr", alert.headline, alertLevel(alert.level));
|
||||
// Keyed on the anchor timestamp rather than the alert id: ids from Cloud
|
||||
// are opaque strings now, and LogList needs a number for its v-for key.
|
||||
// The anchor is unique per row here — one block per incident per window.
|
||||
super(alert.headline, alert.containerId, alert.ts, date, "stderr", alert.headline, alertLevel(alert.level));
|
||||
}
|
||||
|
||||
getComponent(): Component {
|
||||
@@ -237,7 +240,7 @@ export class AlertLogEntry extends LogEntry<string> {
|
||||
/**
|
||||
* Stable identity for dedupe across overlapping scroll windows. Keyed on the
|
||||
* ANCHOR as well as the alert: one incident legitimately marks every window
|
||||
* it was active in, and keying on alertId alone would let a follow-up anchor
|
||||
* it was active in, and keying on the alert alone would let a follow-up anchor
|
||||
* loaded first swallow the origin loaded later.
|
||||
*/
|
||||
public get anchorKey(): string {
|
||||
|
||||
@@ -26,7 +26,7 @@ type AlertResult struct {
|
||||
// server-side to the connecting instance's (user_id, api_key_id) — Cloud
|
||||
// derives those from the auth metadata, never the request body.
|
||||
type AlertHit struct {
|
||||
AlertID int64 `json:"alertId"`
|
||||
AlertID string `json:"alertId"`
|
||||
ContainerID string `json:"containerId"`
|
||||
HostID string `json:"hostId"`
|
||||
// LogID is Cloud's copy of Dozzle's FNV-32a hash of the line that
|
||||
@@ -74,7 +74,7 @@ type EventHit struct {
|
||||
Message string `json:"message,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Detail string `json:"detail,omitempty"`
|
||||
AlertID int64 `json:"alertId,omitempty"`
|
||||
AlertID string `json:"alertId,omitempty"`
|
||||
// Suppressed means the event produced no notification of its own: it was
|
||||
// folded into an alert already sent, or never reached one at all.
|
||||
Suppressed bool `json:"suppressed"`
|
||||
|
||||
@@ -46,7 +46,7 @@ func doAlerts(h *handler, query string) *httptest.ResponseRecorder {
|
||||
}
|
||||
|
||||
func okResult(*alertCall) (*cloud.AlertResult, error) {
|
||||
return &cloud.AlertResult{Hits: []cloud.AlertHit{{AlertID: 1, ContainerID: "abc"}}}, nil
|
||||
return &cloud.AlertResult{Hits: []cloud.AlertHit{{AlertID: "6teOh3RH", ContainerID: "abc"}}}, nil
|
||||
}
|
||||
|
||||
func TestCloudAlerts_UnwiredIs503(t *testing.T) {
|
||||
|
||||
+16
-11
@@ -2513,10 +2513,13 @@ func (x *GetAlertsRequest) GetIncludeEvents() bool {
|
||||
}
|
||||
|
||||
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"`
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// Opaque, sqids-encoded — never the raw BIGSERIAL, which would leak total
|
||||
// alert counts to anything reading a response or a deep link. Decoding it is
|
||||
// Cloud's business; to Dozzle it is a string to compare and nothing more.
|
||||
AlertId string `protobuf:"bytes,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.
|
||||
@@ -2593,11 +2596,11 @@ func (*AlertHit) Descriptor() ([]byte, []int) {
|
||||
return file_cloud_proto_rawDescGZIP(), []int{28}
|
||||
}
|
||||
|
||||
func (x *AlertHit) GetAlertId() int64 {
|
||||
func (x *AlertHit) GetAlertId() string {
|
||||
if x != nil {
|
||||
return x.AlertId
|
||||
}
|
||||
return 0
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AlertHit) GetContainerId() string {
|
||||
@@ -2727,7 +2730,9 @@ type EventHit struct {
|
||||
// 'log' | 'metric' | 'event' — the notification's own type.
|
||||
Type string `protobuf:"bytes,7,opt,name=type,proto3" json:"type,omitempty"`
|
||||
// The incident that absorbed this event; 0 when it never reached an alert.
|
||||
AlertId int64 `protobuf:"varint,8,opt,name=alert_id,json=alertId,proto3" json:"alert_id,omitempty"`
|
||||
// Opaque, sqids-encoded — see AlertHit.alert_id. Empty when the event never
|
||||
// reached an alert.
|
||||
AlertId string `protobuf:"bytes,8,opt,name=alert_id,json=alertId,proto3" json:"alert_id,omitempty"`
|
||||
// True when this event produced no delivered notification of its own: it
|
||||
// arrived after its alert had already been sent and was folded in silently,
|
||||
// or it never became an alert at all.
|
||||
@@ -2820,11 +2825,11 @@ func (x *EventHit) GetType() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *EventHit) GetAlertId() int64 {
|
||||
func (x *EventHit) GetAlertId() string {
|
||||
if x != nil {
|
||||
return x.AlertId
|
||||
}
|
||||
return 0
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *EventHit) GetSuppressed() bool {
|
||||
@@ -3117,7 +3122,7 @@ const file_cloud_proto_rawDesc = "" +
|
||||
"\x12include_follow_ups\x18\x06 \x01(\bR\x10includeFollowUps\x12%\n" +
|
||||
"\x0einclude_events\x18\a \x01(\bR\rincludeEvents\"\xa8\x04\n" +
|
||||
"\bAlertHit\x12\x19\n" +
|
||||
"\balert_id\x18\x01 \x01(\x03R\aalertId\x12!\n" +
|
||||
"\balert_id\x18\x01 \x01(\tR\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" +
|
||||
@@ -3145,7 +3150,7 @@ const file_cloud_proto_rawDesc = "" +
|
||||
"\x05level\x18\x05 \x01(\tR\x05level\x12\x18\n" +
|
||||
"\amessage\x18\x06 \x01(\tR\amessage\x12\x12\n" +
|
||||
"\x04type\x18\a \x01(\tR\x04type\x12\x19\n" +
|
||||
"\balert_id\x18\b \x01(\x03R\aalertId\x12\x1e\n" +
|
||||
"\balert_id\x18\b \x01(\tR\aalertId\x12\x1e\n" +
|
||||
"\n" +
|
||||
"suppressed\x18\t \x01(\bR\n" +
|
||||
"suppressed\x12\x16\n" +
|
||||
|
||||
+7
-2
@@ -367,7 +367,10 @@ message GetAlertsRequest {
|
||||
}
|
||||
|
||||
message AlertHit {
|
||||
int64 alert_id = 1;
|
||||
// Opaque, sqids-encoded — never the raw BIGSERIAL, which would leak total
|
||||
// alert counts to anything reading a response or a deep link. Decoding it is
|
||||
// Cloud's business; to Dozzle it is a string to compare and nothing more.
|
||||
string 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
|
||||
@@ -428,7 +431,9 @@ message EventHit {
|
||||
// 'log' | 'metric' | 'event' — the notification's own type.
|
||||
string type = 7;
|
||||
// The incident that absorbed this event; 0 when it never reached an alert.
|
||||
int64 alert_id = 8;
|
||||
// Opaque, sqids-encoded — see AlertHit.alert_id. Empty when the event never
|
||||
// reached an alert.
|
||||
string alert_id = 8;
|
||||
// True when this event produced no delivered notification of its own: it
|
||||
// arrived after its alert had already been sent and was folded in silently,
|
||||
// or it never became an alert at all.
|
||||
|
||||
Reference in New Issue
Block a user