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:
@@ -232,7 +232,13 @@ func isIdentStart(c rune) bool {
|
||||
}
|
||||
|
||||
// Identifiers allow dots (e.g. winevt.event_id, a real attribute key
|
||||
// shape from Phase 1) and digits after the first character.
|
||||
// shape from Phase 1), digits, and internal hyphens (e.g. host-03,
|
||||
// api-service -- real, common bare-word values with no need for
|
||||
// quoting) after the first character. A leading hyphen is deliberately
|
||||
// NOT part of isIdentStart -- Minus has to stay its own token there so
|
||||
// `earliest=-1h` and `sort -count`'s leading sign still lex correctly;
|
||||
// this only affects a hyphen once a token has already started with a
|
||||
// real identifier character.
|
||||
func isIdentPart(c rune) bool {
|
||||
return isIdentStart(c) || isDigit(c) || c == '.' || c == '_'
|
||||
return isIdentStart(c) || isDigit(c) || c == '.' || c == '_' || c == '-'
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -26,6 +26,39 @@ func TestParseSimpleFilter(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseFilterWithHyphenatedValue is a regression test for a real
|
||||
// bug in the lexer (isIdentPart excluded '-'): this exact query is
|
||||
// /docs/query-language-reference.md's own canonical unquoted example
|
||||
// and used to fail with "unexpected MINUS after query".
|
||||
func TestParseFilterWithHyphenatedValue(t *testing.T) {
|
||||
q, err := Parse(`host!=host-03`)
|
||||
if err != nil {
|
||||
t.Fatalf("Parse() error = %v", err)
|
||||
}
|
||||
cmp, ok := q.Base.Terms[0].(ast.Comparison)
|
||||
if !ok {
|
||||
t.Fatalf("expected Comparison, got %T", q.Base.Terms[0])
|
||||
}
|
||||
if cmp.Field != "host" || cmp.Op != "!=" || cmp.Value != "host-03" {
|
||||
t.Fatalf("unexpected comparison: %+v", cmp)
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseNegativeTimeExprStillWorks guards against the hyphenated-
|
||||
// identifier fix above accidentally swallowing the leading sign
|
||||
// earliest=/latest= depend on -- a leading '-' must stay its own token.
|
||||
func TestParseNegativeTimeExprStillWorks(t *testing.T) {
|
||||
q, err := Parse(`earliest=-1h`)
|
||||
if err != nil {
|
||||
t.Fatalf("Parse() error = %v", err)
|
||||
}
|
||||
earliest := q.Base.Terms[0].(ast.TimeBound)
|
||||
if earliest.Kind != "earliest" || !earliest.Expr.IsRelative || earliest.Expr.RelativeSign != -1 ||
|
||||
earliest.Expr.RelativeN != 1 || earliest.Expr.RelativeUnit != "h" {
|
||||
t.Fatalf("unexpected earliest: %+v", earliest)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseFullPipeline(t *testing.T) {
|
||||
q, err := Parse(`service=api | where status>=500 | stats count(*) as errors by host | sort -errors`)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user