Add agent heartbeat monitoring and fix a query-language lexer bug

Agents now send an independent "still alive" record on a configurable
schedule (seconds/minutes/hours, [heartbeat] in agent.toml), separate
from real log traffic and tagged with a sentry.heartbeat attribute.
No new wire protocol -- it's an ordinary record through the same
PushBatch RPC/mTLS identity every log line already uses. Unavailability
alerting reuses the existing absence-condition alert rule type
unchanged; no new alerting code was needed. See
/docs/agent-heartbeat-monitoring.md for the design and how to build the
alert rule.

While verifying the alert rule live, found that the query language's
lexer never treated '-' as part of an identifier, so any unquoted
hyphenated filter value -- including the reference doc's own canonical
example, `host!=host-03` -- failed to parse at all. Fixed in
api/internal/querylang/lexer/lexer.go with regression tests; a leading
'-' still lexes as its own token so earliest=-1h/sort -count are
unaffected.
This commit is contained in:
2026-08-16 18:08:05 -07:00
parent 7d316f92db
commit 4df6931869
8 changed files with 367 additions and 3 deletions
@@ -76,6 +76,34 @@ func TestLexFieldWithDots(t *testing.T) {
}
}
// TestLexIdentWithInternalHyphens is a regression test for a real bug:
// isIdentPart didn't include '-', so a bare (unquoted) hyphenated value
// like host-03 -- the exact example /docs/query-language-reference.md
// itself uses (`host!=host-03`) -- lexed as IDENT("host") MINUS
// IDENT("03") and failed to parse at all. A leading hyphen must still
// lex as its own MINUS token (earliest=-1h, sort -count depend on it) --
// only an internal hyphen, once a real identifier character has already
// started the token, should be absorbed.
func TestLexIdentWithInternalHyphens(t *testing.T) {
cases := []string{"host-03", "api-service", "heartbeat-test-host", "multi-hyphen-value"}
for _, c := range cases {
l := New(c)
tok := l.Next()
if tok.Kind != Ident || tok.Value != c {
t.Errorf("lexing %q: got %v, want Ident(%s)", c, tok, c)
}
if end := l.Next(); end.Kind != EOF {
t.Errorf("lexing %q: expected EOF after the identifier, got %v", c, end)
}
}
}
func TestLexFilterWithHyphenatedValue(t *testing.T) {
got := collectKinds(`host!=host-03`)
want := []Kind{Ident, Neq, Ident, EOF}
assertKinds(t, got, want)
}
func TestLexNumber(t *testing.T) {
cases := []string{"123", "1.5", "0"}
for _, c := range cases {