Skip to content

Commit

Permalink
feat: traceql v0.1; unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
akvlad committed Mar 21, 2024
1 parent 1d714e0 commit c1ddee9
Show file tree
Hide file tree
Showing 11 changed files with 314 additions and 71 deletions.
46 changes: 46 additions & 0 deletions test/traceql_parser.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
const parser = require('../traceql/parser')

it('traceql: one selector', () => {
const res = parser.ParseScript('{.testId="12345"}')
expect(res.rootToken.value).toEqual('{.testId="12345"}')
})

it('traceql: multiple selectors', () => {
const res = parser.ParseScript('{.testId="12345" &&.spanN=9}')
expect(res.rootToken.value).toEqual('{.testId="12345" &&.spanN=9}')
})

it('traceql: multiple selectors OR Brackets', () => {
const res = parser.ParseScript('{.testId="12345" && (.spanN=9 ||.spanN=8)}')
expect(res.rootToken.value).toEqual('{.testId="12345" && (.spanN=9 ||.spanN=8)}')
})

it('traceql: multiple selectors regexp', () => {
const res = parser.ParseScript('{.testId="12345" &&.spanN=~"(9|8)"}')
expect(res.rootToken.value).toEqual('{.testId="12345" &&.spanN=~"(9|8)"}')
})

it('traceql: duration', () => {
const res = parser.ParseScript('{.testId="12345" && duration>=9ms}')
expect(res.rootToken.value).toEqual('{.testId="12345" && duration>=9ms}')
})

it('traceql: float comparison', () => {
const res = parser.ParseScript('{.testId="12345" &&.spanN>=8.9}')
expect(res.rootToken.value).toEqual('{.testId="12345" &&.spanN>=8.9}')
})

it('traceql: count empty result', () => {
const res = parser.ParseScript('{.testId="12345" &&.spanN>=8.9} | count() > 1')
expect(res.rootToken.value).toEqual('{.testId="12345" &&.spanN>=8.9} | count() > 1')
})

it('traceql: max duration empty result', () => {
const res = parser.ParseScript('{.testId="12345" &&.spanN>=8.9} | max(duration) > 9ms')
expect(res.rootToken.value).toEqual('{.testId="12345" &&.spanN>=8.9} | max(duration) > 9ms')
})

it('traceql: max duration', () => {
const res = parser.ParseScript('{.testId="12345" &&.spanN>=8.9} | max(duration) > 8ms')
expect(res.rootToken.value).toEqual('{.testId="12345" &&.spanN>=8.9} | max(duration) > 8ms')
})
21 changes: 18 additions & 3 deletions traceql/clickhouse_transpiler/aggregator.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,23 @@ const { getCompareFn, durationToNs } = require('./shared')

module.exports = class Builder {
constructor () {
this.main = null
this.fn = ''
this.attr = ''
this.compareFn = ''
this.compareVal = ''
}

/**
*
* @param main {BuiltProcessFn}
* @returns {Builder}
*/
withMain (main) {
this.main = main
return this
}

/**
*
* @param fn {string}
Expand Down Expand Up @@ -54,12 +65,16 @@ module.exports = class Builder {
*/
build () {
const self = this
/** @type {ProcessFn} */
const res = (sel, ctx) => {
/** @type {BuiltProcessFn} */
const res = (ctx) => {
const sel = this.main(ctx)
const fCmpVal = self.cmpVal()
const agg = self.aggregator()
const compareFn = getCompareFn(self.compareFn)
return sel.having(compareFn(agg, Sql.val(fCmpVal)))
const comparreExp = compareFn(agg, Sql.val(fCmpVal))
// .having is broken
sel.having_conditions = Sql.And([...sel.having_conditions.args, comparreExp])
return sel
}
return res
}
Expand Down
41 changes: 25 additions & 16 deletions traceql/clickhouse_transpiler/attr_condition.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const { getCompareFn, durationToNs, unquote } = require('./shared')
const Sql = require('@cloki/clickhouse-sql')
module.exports = class Builder {
constructor () {
this.main = null
this.terms = []
this.conds = null
this.aggregatedAttr = ''
Expand All @@ -12,6 +13,16 @@ module.exports = class Builder {
this.where = []
}

/**
*
* @param main {BuiltProcessFn}
* @returns {Builder}
*/
withMain (main) {
this.main = main
return this
}

/**
* @param terms {[]}
* @returns {Builder}
Expand Down Expand Up @@ -45,8 +56,9 @@ module.exports = class Builder {
*/
build () {
const self = this
/** @type {ProcessFn} */
const res = (sel, ctx) => {
/** @type {BuiltProcessFn} */
const res = (ctx) => {
const sel = this.main(ctx)
self.alias = 'bsCond'
for (const term of self.terms) {
const sqlTerm = self.getTerm(term)
Expand All @@ -59,22 +71,22 @@ module.exports = class Builder {
const having = self.getCond(self.conds)
self.aggregator(sel)
sel.conditions = Sql.And(sel.conditions, Sql.Or(...self.where))
sel.having_conditions = Sql.And(sel.having_conditions, having)
sel.having(having)
return sel
}
return res
}

/**
* @typedef {{simpleIdx: number, op: string, comlex: [Condition]}} Condition
* @typedef {{simpleIdx: number, op: string, complex: [Condition]}} Condition
*/
/**
* @param c {Condition}
*/
getCond (c) {
if (c.simpleIdx === -1) {
const subs = []
for (const s of c.comlex) {
for (const s of c.complex) {
subs.push(this.getCond(s))
}
switch (c.op) {
Expand All @@ -89,7 +101,7 @@ module.exports = class Builder {
if (!this.isAliased) {
left = groupBitOr(bitSet(this.sqlConditions), this.alias)
}
return Sql.Ne(bitAnd(left, Sql.val(c.simpleIdx)), Sql.val(0))
return Sql.Ne(bitAnd(left, new Sql.Raw((BigInt(1) << BigInt(c.simpleIdx)).toString())), Sql.val(0))
}

/**
Expand All @@ -100,10 +112,8 @@ module.exports = class Builder {
return
}

const s = sel.select()
if (this.aggregatedAttr === 'duration') {
s.push([new Sql.Raw('toFloat64(duration)'), 'agg_val'])
sel.select(...s)
sel.select([new Sql.Raw('toFloat64(any(traces_idx.duration))'), 'agg_val'])
return
}

Expand All @@ -113,11 +123,10 @@ module.exports = class Builder {
if (this.aggregatedAttr.match(/^resource\./)) {
this.aggregatedAttr = this.aggregatedAttr.substr(9)
}
if (this.aggregatedAttr.match(/^\.*/)) {
if (this.aggregatedAttr.match(/^\./)) {
this.aggregatedAttr = this.aggregatedAttr.substr(1)
}
s.push([sqlAttrValue(this.aggregatedAttr), 'agg_val'])
sel.select(...s)
sel.select([sqlAttrValue(this.aggregatedAttr), 'agg_val'])
this.where.push(Sql.Eq(new Sql.Raw('key'), Sql.val(this.aggregatedAttr)))
}

Expand All @@ -127,7 +136,7 @@ module.exports = class Builder {
key = key.substr(5)
} else if (key.match(/^resource\./)) {
key = key.substr(9)
} else if (key.match(/^.*/)) {
} else if (key.match(/^\./)) {
key = key.substr(1)
} else {
switch (key) {
Expand All @@ -150,7 +159,7 @@ module.exports = class Builder {

getDurationCondition (key, term) {
const fVal = durationToNs(term.Child('value').value)
const fn = getCompareFn(term.Child('op'))
const fn = getCompareFn(term.Child('op').value)
return fn(new Sql.Raw('traces_idx.duration'), Math.floor(fVal))
}

Expand Down Expand Up @@ -182,7 +191,7 @@ module.exports = class Builder {
}

getNumberCondition (key, term) {
const fn = getCompareFn(term.Child('op'))
const fn = getCompareFn(term.Child('op').value)
if (!term.Child('value').value.match(/^\d+.?\d*$/)) {
throw new Error(`invalid value in ${term.value}`)
}
Expand Down Expand Up @@ -248,7 +257,7 @@ function bitSet (terms) {
const res = new Sql.Raw('')
res.terms = terms
res.toString = () => {
return terms.map((t, i) => `bitShiftLeft(toUint64(${t.toString()}), ${i})`).join('+')
return res.terms.map((t, i) => `bitShiftLeft(toUInt64(${t.toString()}), ${i})`).join('+')
}
return res
}
Expand Down
9 changes: 4 additions & 5 deletions traceql/clickhouse_transpiler/group_by.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
const Sql = require('@cloki/clickhouse-sql')
/**
* @type {ProcessFn}
*/
module.exports = (sel, ctx) => {
const { standardBuilder } = require('./shared')

module.exports = standardBuilder((sel, ctx) => {
const withMain = new Sql.With('index_search', sel)
return (new Sql.Select())
.with(withMain)
Expand All @@ -14,4 +13,4 @@ module.exports = (sel, ctx) => {
).from(new Sql.WithReference(withMain))
.groupBy('trace_id')
.orderBy([new Sql.Raw('max(index_search.timestamp_ns)'), 'desc'])
}
})
109 changes: 109 additions & 0 deletions traceql/clickhouse_transpiler/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
const AttrConditionPlanner = require('./attr_condition')
const InitIndexPlanner = require('./init')
const IndexGroupByPlanner = require('./group_by')
const AggregatorPlanner = require('./aggregator')
const IndexLimitPlanner = require('./limit')
const TracesDataPlanner = require('./traces_data')

/**
* @param script {Token}
*/
module.exports = (script) => {
return new Planner(script).plan()
}

class Planner {
/**
*
* @param script {Token}
*/
constructor (script) {
this.script = script
this.termIdx = []
this.cond = null
this.aggregatedAttr = ''
this.cmpVal = ''
this.terms = {}
this.aggFn = ''
}

plan () {
this.check()
this.analyze()
let res = (new AttrConditionPlanner())
.withTerms(this.termIdx)
.withConditions(this.cond)
.withAggregatedAttr(this.aggregatedAttr)
.withMain((new InitIndexPlanner()).build())
.build()
res = (new IndexGroupByPlanner()).withMain(res).build()
if (this.aggFn) {
res = (new AggregatorPlanner())
.withFn(this.aggFn)
.withAttr(this.aggregatedAttr)
.withCompareFn(this.script.Child('cmp').value)
.withCompareVal(this.script.Child('cmp_val').value)
.withMain(res)
.build()
}
res = (new IndexLimitPlanner()).withMain(res).build()
res = (new TracesDataPlanner()).withMain(res).build()
res = (new IndexLimitPlanner()).withMain(res).build()

return res
}

check () {
if (this.script.Children('SYNTAX').length > 1) {
throw new Error('more than one selector is not supported')
}
}

analyze () {
this.terms = {}
this.cond = this.analyzeCond(this.script.Child('attr_selector_exp'))
this.analyzeAgg()
}

/**
*
* @param token {Token}
*/
analyzeCond (token) {
let res = {}
const complexHead = token.tokens.find(x => x.name === 'complex_head')
const simpleHead = token.tokens.find(x => x.name === 'attr_selector')
if (complexHead) {
res = this.analyzeCond(complexHead.tokens.find(x => x.name === 'attr_selector_exp'))
} else if (simpleHead) {
const term = simpleHead.value
if (this.terms[term]) {
res = { simpleIdx: this.terms[term] - 1, op: '', complex: [] }
} else {
this.termIdx.push(simpleHead)
this.terms[term] = this.termIdx.length
res = { simpleIdx: this.termIdx.length - 1, op: '', complex: [] }
}
}
const tail = token.tokens.find(x => x.name === 'tail')
if (tail) {
res = {
simpleIdx: -1,
op: token.tokens.find(x => x.name === 'and_or').value,
complex: [res, this.analyzeCond(tail.tokens.find(x => x.name === 'attr_selector_exp'))]
}
}
return res
}

analyzeAgg () {
const agg = this.script.Child('aggregator')
if (!agg) {
return
}
this.aggFn = agg.Child('fn').value
const labelName = agg.Child('attr').Child('label_name')
this.aggregatedAttr = labelName ? labelName.value : ''
this.cmpVal = agg.Child('cmp_val').value
}
}
26 changes: 18 additions & 8 deletions traceql/clickhouse_transpiler/init.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,35 @@
const Sql = require('@cloki/clickhouse-sql')
const { format } = require('date-fns')
const { standardBuilder } = require('./shared')
/**
* @typedef {function(Sql.Select, {
* @typedef {{
* from: Date,
* to: Date,
* tracesAttrsTable: string,
* limit: number
* }): Select} ProcessFn
* @type ProcessFn
* limit: number,
* isCluster: boolean,
* tracesTable: string,
* tracesDistTable: string
* }} Context
*/
module.exports.process = (sel, ctx) => {
/**
* @typedef {function(Sql.Select, Context): Select} ProcessFn
*/

/**
* @type {ProcessFn}
*/
module.exports = standardBuilder((sel, ctx) => {
return (new Sql.Select()).select(['trace_id', 'trace_id'],
[new Sql.Raw('lower(hex(span_id))'), 'span_id'],
[new Sql.Raw('any(duration)'), 'duration'],
[new Sql.Raw('any(timestamp_ns)', 'timestamp_ns')])
[new Sql.Raw('any(timestamp_ns)'), 'timestamp_ns'])
.from([ctx.tracesAttrsTable, 'traces_idx'])
.where(Sql.And(
Sql.Gte('date', Sql.val(format(ctx.from, 'yyyy-MM-dd'))),
Sql.Lt('date', Sql.val(format(ctx.to, 'yyyy-MM-dd'))),
Sql.Lte('date', Sql.val(format(ctx.to, 'yyyy-MM-dd'))),
Sql.Gte('traces_idx.timestamp_ns', new Sql.Raw(ctx.from.getTime() + '000000')),
Sql.Lt('traces_idx.timestamp_ns', new Sql.Raw(ctx.to.getTime() + '000000'))
)).groupBy('trace_id', 'span_id')
.orderBy(['timestamp_ns', 'desc'])
}
})
Loading

0 comments on commit c1ddee9

Please sign in to comment.