Skip to content

Commit

Permalink
Add a :contains predicate filter for compatibility with internal lo…
Browse files Browse the repository at this point in the history
…g queries.

Not meant to be used for metrics as it executes a full scan.
Bump Spectator to 1.3.8
  • Loading branch information
manolama committed Sep 27, 2022
1 parent 07cb339 commit 3ccbac3
Show file tree
Hide file tree
Showing 14 changed files with 154 additions and 3 deletions.
13 changes: 13 additions & 0 deletions atlas-core/src/main/scala/com/netflix/atlas/core/model/Query.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ package com.netflix.atlas.core.model
import com.netflix.atlas.core.util.SmallHashMap
import com.netflix.spectator.impl.PatternMatcher

import com.netflix.spectator.impl.matcher.PatternUtils

sealed trait Query extends Expr {

/** Returns true if the query expression matches the tags. */
Expand Down Expand Up @@ -396,6 +398,17 @@ object Query {
override def toString: String = s"$k,$v,:reic"
}

case class Contains(k: String, v: String) extends PatternQuery {
override def pattern: PatternMatcher = PatternMatcher.compile(PatternUtils.escape(v))

def check(s: String): Boolean = pattern.matches(s)

def labelString: String = s"$k contains $v"

override def toString: String = s"$k,$v,:contains"

}

case class In(k: String, vs: List[String]) extends KeyValueQuery {
private val values = vs.toSet

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ object QueryVocabulary extends Vocabulary {
GreaterThanEqual,
Regex,
RegexIgnoreCase,
Contains,
In,
And,
Or,
Expand Down Expand Up @@ -329,6 +330,38 @@ object QueryVocabulary extends Vocabulary {
List("name,DiscoveryStatus_(UP|DOWN)", "name,discoverystatus_(Up|Down)", "ERROR:name")
}

case object Contains extends KeyValueWord {

override def name: String = "contains"

def newInstance(k: String, v: String): Query = Query.Contains(k, v)

override def summary: String =
"""
|Query expression that matches time series with a value that contains the given
|sequence of characters. This version is case sensitive.
|
|> :warning: This operation always requires a full scan and should be avoided if at all
|possible. Queries using this operation may be de-priortized.
|
|Suppose you have four time series:
|
|* `name=http.requests, status=200, nf.app=server`
|* `name=sys.cpu, type=user, nf.app=foo`
|* `name=sys.cpu, type=user, nf.app=bar`
|* `name=sys.cpu, type=user, nf.app=foobar`
|
|The query `nf.app,bar,:contains` would match series with "bar" anywhere in
|the string:
|
|* `name=sys.cpu, type=user, nf.app=bar`
|* `name=sys.cpu, type=user, nf.app=foobar`
""".stripMargin.trim

override def examples: List[String] =
List("name,request", "result,error")
}

case object In extends SimpleWord {

override def name: String = "in"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ class MemoryDatabaseSuite extends FunSuite {
assertEquals(exec("name,[ab]$,:re"), List(ts("sum(name~/^[ab]$/)", 1, 4.0, 4.0, 4.0)))
}

test(":contains query") {
assertEquals(exec("name,a,:contains"), List(ts("sum(name contains a)", 1, 1.0, 2.0, 3.0)))
}

test(":has query") {
assertEquals(exec("name,:has"), List(ts("sum(has(name))", 1, 19.0, 22.0, 25.0)))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,25 @@ class QueryIndexSuite extends FunSuite {
assert(!matches(index, Map("a" -> "1", "b" -> "3", "c" -> "3")))
}

test("single query: simple :Contains") {
val q = Query.And(Query.Equal("a", "1"), Query.Contains("b", "bar"))
val index = QueryIndex(List(q))

// Not all tags are present
assert(!matches(index, Map.empty))
assert(!matches(index, Map("a" -> "1")))

// matches
assert(matches(index, Map("a" -> "1", "b" -> "bar")))
assert(matches(index, Map("a" -> "1", "b" -> "bar", "c" -> "3")))

// a doesn't match
assert(!matches(index, Map("a" -> "2", "b" -> "bar", "c" -> "3")))

// b doesn't match
assert(!matches(index, Map("a" -> "1", "b" -> "foo", "c" -> "3")))
}

test("matchingEntries single query: simple") {
val q = Query.And(Query.Equal("a", "1"), Query.Equal("b", "2"))
val index = QueryIndex(List(q))
Expand Down Expand Up @@ -276,6 +295,8 @@ class QueryIndexSuite extends FunSuite {
interner.getOrElseUpdate(q, Query.Regex(q.k.intern(), q.v.intern()))
case q: Query.RegexIgnoreCase =>
interner.getOrElseUpdate(q, Query.RegexIgnoreCase(q.k.intern(), q.v.intern()))
case q: Query.Contains =>
interner.getOrElseUpdate(q, Query.Contains(q.k.intern(), q.v.intern()))
case q: Query.In =>
interner.getOrElseUpdate(q, Query.In(q.k.intern(), q.vs.map(_.intern())))
case q: Query.HasKey =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,15 @@ abstract class TagIndexSuite extends FunSuite {
assertEquals(result.size, 7640)
}

test("contains query") {
val q = Query.Regex("nf.cluster", "nccp-silver")
val result = index.findItems(TagQuery(Some(q)))
result.foreach { m =>
assertEquals(m.tags("nf.cluster"), "nccp-silverlight")
}
assertEquals(result.size, 3000)
}

test("haskey query") {
val q = Query.HasKey("nf.cluster")
val result = index.findItems(TagQuery(Some(q)))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,15 @@ class TaggedItemIndexSuite extends FunSuite {
assertEquals(result.size, 7640)
}

test("contains query") {
val q = Query.Contains("nf.cluster", "nccp-silver")
val result = findItems(q)
result.foreach { m =>
assertEquals(m.tags("nf.cluster"), "nccp-silverlight")
}
assertEquals(result.size, 3000)
}

test("haskey query") {
val q = Query.HasKey("nf.cluster")
val result = findItems(q)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class ModelExtractorsSuite extends FunSuite {
}

completionTest("name", 8)
completionTest("name,sps", 19)
completionTest("name,sps", 20)
completionTest("name,sps,:eq", 20)
completionTest("name,sps,:eq,app,foo,:eq", 41)
completionTest("name,sps,:eq,app,foo,:eq,:and,(,asg,)", 12)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,14 @@ class QuerySuite extends FunSuite {
assert(!matches(q, Map("foo2" -> "bar")))
}

test("matches contains") {
val q = Contains("foo", "a")
assert(matches(q, Map("foo" -> "bar")))
assert(matches(q, Map("foo" -> "baz")))
assert(!matches(q, Map("foo" -> "bbb")))
assert(!matches(q, Map("foo2" -> "bar")))
}

test("matches in") {
val q = In("foo", List("bar", "baz"))
assert(matches(q, Map("foo" -> "bar")))
Expand Down Expand Up @@ -233,7 +241,7 @@ class QuerySuite extends FunSuite {
}

test("matchesAny re with key match") {
val q = GreaterThan("foo", "b")
val q = Regex("foo", "b")
assert(matchesAny(q, Map("foo" -> List("bar"), "bar" -> List("foo"))))
assert(matchesAny(q, Map("foo" -> List("foo", "bar"), "bar" -> List("foo"))))
assert(matchesAny(q, Map("foo" -> List("bar", "baz"), "bar" -> List("foo"))))
Expand All @@ -250,6 +258,18 @@ class QuerySuite extends FunSuite {
assert(!matchesAny(q, Map("foo" -> List("bar"), "bar" -> List("foo"))))
}

test("matchesAny contains with key match") {
val q = Contains("foo", "b")
assert(matchesAny(q, Map("foo" -> List("bar"), "bar" -> List("foo"))))
assert(matchesAny(q, Map("foo" -> List("foo", "bar"), "bar" -> List("foo"))))
assert(matchesAny(q, Map("foo" -> List("bar", "baz"), "bar" -> List("foo"))))
}

test("matchesAny contains without key no match") {
val q = Contains("foo2", "a")
assert(!matchesAny(q, Map("foo" -> List("bar"), "bar" -> List("foo"))))
}

test("matchesAny has with key match") {
val q = HasKey("foo")
assert(matchesAny(q, Map("foo" -> List("bar"), "bar" -> List("foo"))))
Expand Down Expand Up @@ -340,6 +360,21 @@ class QuerySuite extends FunSuite {
assert(couldMatch(q, Map("foo" -> "bar", "bar" -> "foo")))
}

test("couldMatch contains with key match") {
val q = Contains("foo", "b")
assert(couldMatch(q, Map("foo" -> "bar", "bar" -> "foo")))
}

test("couldMatch contains with key no match") {
val q = Contains("foo", "z")
assert(!couldMatch(q, Map("foo" -> "bar", "bar" -> "foo")))
}

test("couldMatch contains without key") {
val q = Contains("foo2", "bar")
assert(couldMatch(q, Map("foo" -> "bar", "bar" -> "foo")))
}

test("couldMatch has with key match") {
val q = HasKey("foo")
assert(couldMatch(q, Map("foo" -> "bar", "bar" -> "foo")))
Expand Down Expand Up @@ -650,4 +685,22 @@ class QuerySuite extends FunSuite {
val q = Or(Equal("a", "1"), In("b", List("1", "2")))
assertEquals(Query.expandInClauses(q, 1), List(q))
}

test("contains, escape") {
assertEquals(
Contains("a", "^$.?*+[](){}\\#&!%").pattern.toString,
".*\\^\\$\\.\\?\\*\\+\\[\\]\\(\\)\\{\\}\\\\#&!%"
)
assertEquals(
Contains("a", "space and ~").pattern.toString,
".*space\\u0020and\\u0020~"
)
}

test("contains, matches escaped") {
val q = Contains("foo", "my $var. [work-in-progress]")
assert(matches(q, Map("foo" -> "my $var. [work-in-progress]")))
assert(matches(q, Map("foo" -> "initialize my $var. [work-in-progress], not a range")))
assert(!matches(q, Map("foo" -> "my $var. [work-in progress]")))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ class TimeSeriesExprSuite extends FunSuite {
"name,1,:eq" -> const(ts(Map("name" -> "1"), 1)),
"name,1,:re" -> const(ts(unknownTag, 11)),
"name,2,:re" -> const(ts(unknownTag, 2)),
"name,2,:contains" -> const(ts(unknownTag, 2)),
"name,(,1,10,),:in" -> const(ts(unknownTag, 11)),
"name,1,:eq,name,10,:eq,:or" -> const(ts(unknownTag, 11)),
":true,:abs" -> const(ts(unknownTag, "abs(name=unknown)", 55.0)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ private[graph] object SimpleLegends extends StrictLogging {
case Query.GreaterThanEqual(k, v) => Map(k -> v)
case Query.Regex(k, v) => Map(k -> v)
case Query.RegexIgnoreCase(k, v) => Map(k -> v)
case Query.Contains(k, v) => Map(k -> v)
case Query.Not(q: KeyValueQuery) => keyValues(q).map(t => t._1 -> s"!${t._2}")
case _ => Map.empty
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ class SimpleLegendsSuite extends FunSuite {
assertEquals(legends("name,cpu,:re,:sum"), List("cpu"))
}

test("name contains") {
assertEquals(legends("name,cpu,:contains,:sum"), List("cpu"))
}

test("name not present") {
assertEquals(legends("id,user,:eq,:sum"), List("user"))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ class ExpressionSplitter(config: Config) {
internQuery(q, Query.Regex(q.k.intern(), q.v.intern()))
case q: Query.RegexIgnoreCase =>
internQuery(q, Query.RegexIgnoreCase(q.k.intern(), q.v.intern()))
case q: Query.Contains =>
internQuery(q, Query.Contains(q.k.intern(), q.v.intern()))
case q: Query.In =>
internQuery(q, Query.In(q.k.intern(), q.vs.map(_.intern())))
case q: Query.HasKey =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ class ExpressionSplitterSuite extends FunSuite {
Query.GreaterThanEqual("a", "123"),
Query.Regex("a", "b"),
Query.RegexIgnoreCase("a", "b"),
Query.Contains("a", "b"),
Query.In("a", List("b", "c")),
Query.HasKey("a"),
Query.And(Query.True, Query.True),
Expand Down
2 changes: 1 addition & 1 deletion project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ object Dependencies {
val log4j = "2.18.0"
val scala = "2.13.8"
val slf4j = "1.7.36"
val spectator = "1.3.7"
val spectator = "1.3.8"
val spring = "5.3.22"

val crossScala = Seq(scala)
Expand Down

0 comments on commit 3ccbac3

Please sign in to comment.