Skip to content

Commit

Permalink
fix: port traceql to nodejs
Browse files Browse the repository at this point in the history
  • Loading branch information
akvlad committed Mar 20, 2024
1 parent 7d0440a commit 1d714e0
Show file tree
Hide file tree
Showing 3 changed files with 314 additions and 16 deletions.
19 changes: 3 additions & 16 deletions traceql/clickhouse_transpiler/aggregator.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const Sql = require('@cloki/clickhouse-sql')
const {getCompareFn} = require('./shared')
const { getCompareFn, durationToNs } = require('./shared')

module.exports = class Builder {
constructor () {
Expand Down Expand Up @@ -64,22 +64,9 @@ module.exports = class Builder {
return res
}

cmpVal() {
cmpVal () {
if (this.attr === 'duration') {
const measurements = {
ns: 1,
us: 1000,
ms: 1000000,
s: 1000000000,
m: 1000000000 * 60,
h: 1000000000 * 3600,
d: 1000000000 * 3600 * 24
}
const durationRe = this.compareVal.match(/(\d+\.?\d*)(ns|us|ms|s|m|h|d)?/)
if (!durationRe) {
throw new Error('Invalid duration compare value')
}
return parseFloat(durationRe[1]) * measurements[durationRe[2].toLowerCase()]
return durationToNs(this.compareVal)
}
return parseFloat(this.compareVal)
}
Expand Down
283 changes: 283 additions & 0 deletions traceql/clickhouse_transpiler/attr_condition.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
const { getCompareFn, durationToNs, unquote } = require('./shared')
const Sql = require('@cloki/clickhouse-sql')
module.exports = class Builder {
constructor () {
this.terms = []
this.conds = null
this.aggregatedAttr = ''

this.sqlConditions = []
this.isAliased = false
this.alias = ''
this.where = []
}

/**
* @param terms {[]}
* @returns {Builder}
*/
withTerms (terms) {
this.terms = terms
return this
}

/**
* @param conds
* @returns {Builder}
*/
withConditions (conds) {
this.conds = conds
return this
}

/**
*
* @param aggregatedAttr {string}
* @returns {Builder}
*/
withAggregatedAttr (aggregatedAttr) {
this.aggregatedAttr = aggregatedAttr
return this
}

/**
* @returns {ProcessFn}
*/
build () {
const self = this
/** @type {ProcessFn} */
const res = (sel, ctx) => {
self.alias = 'bsCond'
for (const term of self.terms) {
const sqlTerm = self.getTerm(term)
self.sqlConditions.push(sqlTerm)
if (!term.Child('label_name').value.match(/^(\.|span\.|resource\.|name)/)) {
continue
}
self.where.push(sqlTerm)
}
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)
return sel
}
return res
}

/**
* @typedef {{simpleIdx: number, op: string, comlex: [Condition]}} Condition
*/
/**
* @param c {Condition}
*/
getCond (c) {
if (c.simpleIdx === -1) {
const subs = []
for (const s of c.comlex) {
subs.push(this.getCond(s))
}
switch (c.op) {
case '&&':
return Sql.And(...subs)
case '||':
return Sql.Or(...subs)
}
throw new Error(`unsupported condition operator ${c.op}`)
}
let left = new Sql.Raw(this.alias)
if (!this.isAliased) {
left = groupBitOr(bitSet(this.sqlConditions), this.alias)
}
return Sql.Ne(bitAnd(left, Sql.val(c.simpleIdx)), Sql.val(0))
}

/**
* @param sel {Select}
*/
aggregator (sel) {
if (!this.aggregatedAttr) {
return
}

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

if (this.aggregatedAttr.match(/^span./)) {
this.aggregatedAttr = this.aggregatedAttr.substr(5)
}
if (this.aggregatedAttr.match(/^resource\./)) {
this.aggregatedAttr = this.aggregatedAttr.substr(9)
}
if (this.aggregatedAttr.match(/^\.*/)) {
this.aggregatedAttr = this.aggregatedAttr.substr(1)
}
s.push([sqlAttrValue(this.aggregatedAttr), 'agg_val'])
sel.select(...s)
this.where.push(Sql.Eq(new Sql.Raw('key'), Sql.val(this.aggregatedAttr)))
}

getTerm (term) {
let key = term.Child('label_name').value
if (key.match(/^span\./)) {
key = key.substr(5)
} else if (key.match(/^resource\./)) {
key = key.substr(9)
} else if (key.match(/^.*/)) {
key = key.substr(1)
} else {
switch (key) {
case 'duration':
return this.getDurationCondition(key, term)
case 'name':
key = 'name'
break
default:
throw new Error(`unsupported attribute ${key}`)
}
}
if (term.Child('quoted_str')) {
return this.getStrCondition(key, term)
} else if (term.Child('number')) {
return this.getNumberCondition(key, term)
}
throw new Error(`unsupported term statement ${term.value}`)
}

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

getStrCondition (key, term) {
const strVal = this.getString(term)
switch (term.Child('op').value) {
case '=':
return Sql.And(
Sql.Eq(new Sql.Raw('key'), Sql.val(key)),
Sql.Eq(new Sql.Raw('val'), Sql.val(strVal))
)
case '!=':
return Sql.And(
Sql.Eq(new Sql.Raw('key'), Sql.val(key)),
Sql.Ne(new Sql.Raw('val'), Sql.val(strVal))
)
case '=~':
return Sql.And(
Sql.Eq(new Sql.Raw('key'), Sql.val(key)),
Sql.Eq(new Sql.Raw(`match(val, ${Sql.quoteVal(strVal)})`), 1)
)
case '!~':
return Sql.And(
Sql.Eq(new Sql.Raw('key'), Sql.val(key)),
Sql.Ne(new Sql.Raw(`match(val, ${Sql.quoteVal(strVal)})`), 1)
)
}
throw new Error(`unsupported term statement ${term.value}`)
}

getNumberCondition (key, term) {
const fn = getCompareFn(term.Child('op'))
if (!term.Child('value').value.match(/^\d+.?\d*$/)) {
throw new Error(`invalid value in ${term.value}`)
}
const fVal = parseFloat(term.Child('value').value)
return Sql.And(
Sql.Eq('key', Sql.val(key)),
Sql.Eq(new Sql.Raw('isNotNull(toFloat64OrNull(val))'), 1),
fn(new Sql.Raw('toFloat64OrZero(val)'), fVal)
)
}

getString (term) {
if (term.Child('quoted_str').value) {
return unquote(term.Child('quoted_str').value)
}
if (term.Child('number').value) {
return term.Child('number').value
}
throw new Error(`unsupported term statement ${term.value}`)
}
}

/**
*
* @param left
* @param right
* @returns {SQLObject}
*/
function bitAnd (left, right) {
const res = new Sql.Raw('')
res.toString = () => {
const strLeft = left.toString()
const strRight = right.toString()
return `bitAnd(${strLeft}, ${strRight})`
}
return res
}

/**
*
* @param left
* @param alias
* @returns {SQLObject}
*/
function groupBitOr (left, alias) {
const res = new Sql.Raw('')
res.toString = () => {
const strLeft = left.toString()
if (alias) {
return `groupBitOr(${strLeft}) as ${alias}`
}
return `groupBitOr(${strLeft})`
}
return res
}

/**
*
* @param terms
* @returns {SQLObject}
*/
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
}

/**
*
* @param attr {string}
* @returns {SQLObject}
*/
function sqlAttrValue (attr) {
const res = new Sql.Raw('')
res.toString = () => {
const strAttr = Sql.quoteVal(attr)
return `anyIf(toFloat64OrNull(val), key == ${strAttr})`
}
return res
}

/**
* type sqlAttrValue struct {
* attr string
* }
*
* func (s *sqlAttrValue) String(ctx *sql.Ctx, options ...int) (string, error) {
* attr, err := sql.NewStringVal(s.attr).String(ctx, options...)
* if err != nil {
* return "", err
* }
*
* return fmt.Sprintf("anyIf(toFloat64OrNull(val), key == %s)", attr), nil
* }
*/
28 changes: 28 additions & 0 deletions traceql/clickhouse_transpiler/shared.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const Sql = require('@cloki/clickhouse-sql')
const { json } = require('../../parser/registry/parser_registry')
/**
*
* @param op {string}
Expand All @@ -20,3 +21,30 @@ module.exports.getCompareFn = (op) => {
}
throw new Error('not supported operator: ' + op)
}

module.exports.durationToNs = (duration) => {
const measurements = {
ns: 1,
us: 1000,
ms: 1000000,
s: 1000000000,
m: 1000000000 * 60,
h: 1000000000 * 3600,
d: 1000000000 * 3600 * 24
}
const durationRe = duration.match(/(\d+\.?\d*)(ns|us|ms|s|m|h|d)?/)
if (!durationRe) {
throw new Error('Invalid duration compare value')
}
return parseFloat(durationRe[1]) * measurements[durationRe[2].toLowerCase()]
}

module.exports.unquote = (val) => {
if (val[0] === '"') {
return json.parse(val)
}
if (val[0] === '`') {
return val.substr(1, val.length - 2)
}
throw new Error('unquote not supported')
}

0 comments on commit 1d714e0

Please sign in to comment.