From 70f459c06b14b5fe75be5f51730141faa09462c2 Mon Sep 17 00:00:00 2001 From: Brian Harrington Date: Tue, 27 Feb 2024 10:44:32 -0600 Subject: [PATCH] add span-time-series operator Just using span-filter for both pass-through and time series use-cases on traces creates some ambiguity for some use-cases in particular with implicit conversions. --- .../atlas/core/model/CustomVocabulary.scala | 7 +++-- .../netflix/atlas/core/model/TraceQuery.scala | 5 ++- .../atlas/core/model/TraceVocabulary.scala | 31 ++++++++++++++++--- .../core/model/TraceVocabularySuite.scala | 21 ++++++++++++- .../netflix/atlas/eval/model/ExprType.java | 12 ++++--- 5 files changed, 63 insertions(+), 13 deletions(-) diff --git a/atlas-core/src/main/scala/com/netflix/atlas/core/model/CustomVocabulary.scala b/atlas-core/src/main/scala/com/netflix/atlas/core/model/CustomVocabulary.scala index ed53509b9..802bbf225 100644 --- a/atlas-core/src/main/scala/com/netflix/atlas/core/model/CustomVocabulary.scala +++ b/atlas-core/src/main/scala/com/netflix/atlas/core/model/CustomVocabulary.scala @@ -78,15 +78,18 @@ import com.typesafe.config.Config * `:\$name`. * - `base-query`: query for the denominator. * - `keys`: tag keys that are available for use on the denominator. + * + * @param dependencies + * Other vocabularies to depend on, defaults to the `StyleVocabulary`. */ -class CustomVocabulary(config: Config) extends Vocabulary { +class CustomVocabulary(config: Config, dependencies: List[Vocabulary] = List(StyleVocabulary)) extends Vocabulary { import CustomVocabulary.* import scala.jdk.CollectionConverters.* val name: String = "custom" - val dependsOn: List[Vocabulary] = List(StyleVocabulary) + val dependsOn: List[Vocabulary] = dependencies val words: List[Word] = { val vocab = config.getConfig("atlas.core.vocabulary") diff --git a/atlas-core/src/main/scala/com/netflix/atlas/core/model/TraceQuery.scala b/atlas-core/src/main/scala/com/netflix/atlas/core/model/TraceQuery.scala index 8a641dcc3..c7df49e66 100644 --- a/atlas-core/src/main/scala/com/netflix/atlas/core/model/TraceQuery.scala +++ b/atlas-core/src/main/scala/com/netflix/atlas/core/model/TraceQuery.scala @@ -39,5 +39,8 @@ object TraceQuery { case class Child(q1: Query, q2: Query) extends TraceQuery /** Filter to select the set of spans from a trace to forward as events. */ - case class SpanFilter(q: TraceQuery, f: DataExpr) extends Expr + case class SpanFilter(q: TraceQuery, f: Query) extends Expr + + /** Time series based on data from a set of matching traces. */ + case class SpanTimeSeries(q: TraceQuery, expr: StyleExpr) extends Expr } diff --git a/atlas-core/src/main/scala/com/netflix/atlas/core/model/TraceVocabulary.scala b/atlas-core/src/main/scala/com/netflix/atlas/core/model/TraceVocabulary.scala index 4d306e825..e0be14dbd 100644 --- a/atlas-core/src/main/scala/com/netflix/atlas/core/model/TraceVocabulary.scala +++ b/atlas-core/src/main/scala/com/netflix/atlas/core/model/TraceVocabulary.scala @@ -25,12 +25,13 @@ object TraceVocabulary extends Vocabulary { val name: String = "trace" - val dependsOn: List[Vocabulary] = List(DataVocabulary) + val dependsOn: List[Vocabulary] = List(StyleVocabulary) override def words: List[Word] = List( SpanAndWord, SpanOrWord, SpanFilterWord, + SpanTimeSeriesWord, ChildWord ) @@ -109,14 +110,11 @@ object TraceVocabulary extends Vocabulary { override def name: String = "span-filter" override protected def matcher: PartialFunction[List[Any], Boolean] = { - case (_: Query) :: TraceQueryType(_) :: _ => true - case (_: DataExpr) :: TraceQueryType(_) :: _ => true + case (_: Query) :: TraceQueryType(_) :: _ => true } override protected def executor: PartialFunction[List[Any], List[Any]] = { case (f: Query) :: TraceQueryType(q) :: stack => - TraceQuery.SpanFilter(q, DataExpr.All(f)) :: stack - case (f: DataExpr) :: TraceQueryType(q) :: stack => TraceQuery.SpanFilter(q, f) :: stack } @@ -129,4 +127,27 @@ object TraceVocabulary extends Vocabulary { override def examples: List[String] = List("app,foo,:eq,app,bar,:eq") } + + case object SpanTimeSeriesWord extends SimpleWord { + + override def name: String = "span-time-series" + + override protected def matcher: PartialFunction[List[Any], Boolean] = { + case PresentationType(_) :: TraceQueryType(_) :: _ => true + } + + override protected def executor: PartialFunction[List[Any], List[Any]] = { + case PresentationType(f: StyleExpr) :: TraceQueryType(q) :: stack => + TraceQuery.SpanTimeSeries(q, f) :: stack + } + + override def signature: String = "q:TraceQuery f:Query -- SpanFilter" + + override def summary: String = + """ + |Time series based on data from a set of matching traces. + |""".stripMargin + + override def examples: List[String] = List("app,foo,:eq,app,bar,:eq,:sum,ts,:legend") + } } diff --git a/atlas-core/src/test/scala/com/netflix/atlas/core/model/TraceVocabularySuite.scala b/atlas-core/src/test/scala/com/netflix/atlas/core/model/TraceVocabularySuite.scala index a564441b5..5651b1cfd 100644 --- a/atlas-core/src/test/scala/com/netflix/atlas/core/model/TraceVocabularySuite.scala +++ b/atlas-core/src/test/scala/com/netflix/atlas/core/model/TraceVocabularySuite.scala @@ -37,6 +37,13 @@ class TraceVocabularySuite extends FunSuite { } } + private def parseTimeSeries(str: String): TraceQuery.SpanTimeSeries = { + interpreter.execute(str).stack match { + case (t: TraceQuery.SpanTimeSeries) :: Nil => t + case _ => throw new IllegalArgumentException(str) + } + } + test("simple Query coerced to TraceQuery") { val q = parseTraceQuery("app,foo,:eq") assertEquals(q, TraceQuery.Simple(Query.Equal("app", "foo"))) @@ -76,7 +83,19 @@ class TraceVocabularySuite extends FunSuite { Query.Equal("app", "foo"), Query.Equal("app", "bar") ), - DataExpr.All(Query.Equal("app", "foo")) + Query.Equal("app", "foo") + ) + assertEquals(q, expected) + } + + test("span-time-series") { + val q = parseTimeSeries("app,foo,:eq,app,bar,:eq,:child,app,foo,:eq,:span-time-series") + val expected = TraceQuery.SpanTimeSeries( + TraceQuery.Child( + Query.Equal("app", "foo"), + Query.Equal("app", "bar") + ), + StyleExpr(DataExpr.Sum(Query.Equal("app", "foo")), Map.empty) ) assertEquals(q, expected) } diff --git a/atlas-eval/src/main/scala/com/netflix/atlas/eval/model/ExprType.java b/atlas-eval/src/main/scala/com/netflix/atlas/eval/model/ExprType.java index 667466feb..b56290ace 100644 --- a/atlas-eval/src/main/scala/com/netflix/atlas/eval/model/ExprType.java +++ b/atlas-eval/src/main/scala/com/netflix/atlas/eval/model/ExprType.java @@ -17,15 +17,19 @@ /** Indicates the type of expression for a subscription. */ public enum ExprType { + + /** Expression to select a set of events to be passed through. */ + EVENTS, + /** * Time series expression such as used with Atlas Graph API. Can also be used for analytics * queries on top of event data. */ TIME_SERIES, - /** Expression to select a set of events to be passed through. */ - EVENTS, - /** Expression to select a set of traces to be passed through. */ - TRACES + TRACE_EVENTS, + + /** Time series expression based on data extraced from traces. */ + TRACE_TIME_SERIES }