mirror of https://github.com/grafana/loki
feat(logql): Supporting prettifying LogQL expressions (#7906)
**What this PR does / why we need it**: Changes 1. Added `Pretty()` method to `Expr` interface. So that every expression would know how to render themselfs as pretty :) 2. Implemented `Pretty()` for every single LogQL expressions 3. Exposed `/api/v1/format_query` endpoint that takes `query` argument and returns formatted version. 4. Integerated into `logcli`. echo `'<query>' | logcli fmt` would format the query. Why? 1. Readability, helpful in debugging and **more importantly, to understand the execution flow** example **before** ``` quantile_over_time(0.99,{container="ingress-nginx",service="hosted-grafana"}| json| unwrap response_latency_seconds| __error__=""[1m]) by (cluster) ``` ``` sum(rate({job="loki", namespace="loki", cluster="loki-dev-us"} |= "err" [5m])) + sum(rate({job="loki-dev", namespace="loki", cluster="loki-dev-eu"}|logfmt | level != "info" [5m])) / sum(rate({job="loki-prod", namespace="loki", cluster="loki-prod-us"} |logfmt | level="error"[5m])) ``` ``` label_replace(rate({job="api-server",service="a:c"}|= "err" [5m]), "foo", "$1", "service", "(.*):.*") ``` **after** ``` quantile_over_time( 0.99, {container="ingress-nginx", service="hosted-grafana"} | json | unwrap response_latency_seconds | __error__="" [1m] ) by (cluster) ``` ``` sum( rate( {job="loki", namespace="loki", cluster="loki-dev-us"} |= "err" [5m] ) ) + sum( rate( {job="loki-dev", namespace="loki", cluster="loki-dev-eu"} | logfmt | level!="info" [5m] ) ) / sum( rate( {job="loki-prod", namespace="loki", cluster="loki-prod-us"} | logfmt | level="error" [5m] ) ) ``` ``` label_replace( rate({job="api-server", service="a:c"} |= "err"[5m]), "foo", "$1", "service", "(.*):.*" ) ``` You can find more examples in the `prettier_test.go` Future plans * Integerate into LogQL analyzer * Integrate into Grafana UI. **Which issue(s) this PR fixes**: Fixes # NA **Special notes for your reviewer**: This whole idea was inspired from last [PromCon lighting talk](https://youtu.be/pjkWzDVxWk4?t=24469) **Checklist** - [x] Reviewed the `CONTRIBUTING.md` guide - [x] Documentation added - [x] Tests updated - [x] `CHANGELOG.md` updated Signed-off-by: Kaviraj <kavirajkanagaraj@gmail.com> Co-authored-by: Christian Haudum <christian.haudum@gmail.com> Co-authored-by: Christian Simon <simon@swine.de>pull/7937/head^2
parent
c71620ae94
commit
cbd6ec15ce
@ -0,0 +1,406 @@ |
||||
// LogQL formatter is inspired from PromQL formatter
|
||||
// https://github.com/prometheus/prometheus/blob/release-2.40/promql/parser/prettier.go
|
||||
// https://youtu.be/pjkWzDVxWk4?t=24469
|
||||
|
||||
package syntax |
||||
|
||||
import ( |
||||
"fmt" |
||||
"strconv" |
||||
"strings" |
||||
|
||||
"github.com/prometheus/common/model" |
||||
) |
||||
|
||||
// How LogQL formatter works?
|
||||
// =========================
|
||||
// General idea is to parse the LogQL query(string) and converts it into AST(expressions) first, then format each expression from bottom up (from leaf expressions to the root expression). Every expression in AST has a level/depth (distance from the root), that is passed by it's parent.
|
||||
//
|
||||
// While prettifying an expression, we consider two things:
|
||||
// 1. Did the current expression's parent add a new line?
|
||||
// 2. Does the current expression exceeds `maxCharsPerLine` limit?
|
||||
//
|
||||
// The level of a expression determines if it should be indented or not.
|
||||
// The answer to the 1 is NO if the level passed is 0. This means, the
|
||||
// parent expression did not apply a new line, so the current Node must not
|
||||
// apply any indentation as prefix.
|
||||
// If level > 1, a new line is applied by the parent. So, the current expression
|
||||
// should prefix an indentation before writing any of its content. This indentation
|
||||
// will be ([level/depth of current expression] * " ").
|
||||
//
|
||||
// The answer to 2 is YES if the normalized length of the current expression exceeds
|
||||
// the `maxCharsPerLine` limit. Hence, it applies the indentation equal to
|
||||
// its depth and increments the level by 1 before passing down the child.
|
||||
// If the answer is NO, the current expression returns the normalized string value of itself.
|
||||
//
|
||||
|
||||
var ( |
||||
// maxCharsPerLine is used to qualify whether some LogQL expressions are worth `splitting` into new lines.
|
||||
maxCharsPerLine = 100 |
||||
) |
||||
|
||||
func Prettify(e Expr) string { |
||||
return e.Pretty(0) |
||||
} |
||||
|
||||
// e.g: `{foo="bar"}`
|
||||
func (e *MatchersExpr) Pretty(level int) string { |
||||
return commonPrefixIndent(level, e) |
||||
} |
||||
|
||||
// e.g: `{foo="bar"} | logfmt | level="error"`
|
||||
// Here, left = `{foo="bar"}` and multistages would collection of each stage in pipeline, here `logfmt` and `level="error"`
|
||||
func (e *PipelineExpr) Pretty(level int) string { |
||||
if !needSplit(e) { |
||||
return indent(level) + e.String() |
||||
} |
||||
|
||||
s := fmt.Sprintf("%s\n", e.Left.Pretty(level)) |
||||
for i, ms := range e.MultiStages { |
||||
s += ms.Pretty(level + 1) |
||||
//NOTE: Needed because, we tend to format multiple stage in pipeline as each stage in single line
|
||||
// e.g:
|
||||
// | logfmt
|
||||
// | level = "error"
|
||||
// But all the stages will have same indent level. So here we don't increase level.
|
||||
if i < len(e.MultiStages)-1 { |
||||
s += "\n" |
||||
} |
||||
} |
||||
return s |
||||
} |
||||
|
||||
// e.g: `|= "error" != "memcache" |= ip("192.168.0.1")`
|
||||
// NOTE: here `ip` is Op in this expression.
|
||||
func (e *LineFilterExpr) Pretty(level int) string { |
||||
if !needSplit(e) { |
||||
return indent(level) + e.String() |
||||
} |
||||
|
||||
var s string |
||||
|
||||
if e.Left != nil { |
||||
// s += indent(level)
|
||||
s += e.Left.Pretty(level) |
||||
// NOTE: Similar to PiplelinExpr, we also have to format every LineFilterExpr in new line. But with same indendation level.
|
||||
// e.g:
|
||||
// |= "error"
|
||||
// != "memcached"
|
||||
// |= ip("192.168.0.1")
|
||||
s += "\n" |
||||
} |
||||
|
||||
s += indent(level) |
||||
|
||||
// We re-use LineFilterExpr's String() implementation to avoid duplication.
|
||||
// We create new LineFilterExpr without `Left`.
|
||||
ne := newLineFilterExpr(e.Ty, e.Op, e.Match) |
||||
s += ne.String() |
||||
|
||||
return s |
||||
} |
||||
|
||||
// e.g:
|
||||
// `| logfmt`
|
||||
// `| json`
|
||||
// `| regexp`
|
||||
// `| pattern`
|
||||
// `| unpack`
|
||||
func (e *LabelParserExpr) Pretty(level int) string { |
||||
return commonPrefixIndent(level, e) |
||||
} |
||||
|
||||
// e.g: | level!="error"
|
||||
func (e *LabelFilterExpr) Pretty(level int) string { |
||||
return commonPrefixIndent(level, e) |
||||
} |
||||
|
||||
// e.g: | line_format "{{ .label }}"
|
||||
func (e *LineFmtExpr) Pretty(level int) string { |
||||
return commonPrefixIndent(level, e) |
||||
} |
||||
|
||||
// e.g: | decolorize
|
||||
func (e *DecolorizeExpr) Pretty(level int) string { |
||||
return e.String() |
||||
} |
||||
|
||||
// e.g: | label_format dst="{{ .src }}"
|
||||
func (e *LabelFmtExpr) Pretty(level int) string { |
||||
return commonPrefixIndent(level, e) |
||||
} |
||||
|
||||
// e.g: | json label="expression", another="expression"
|
||||
func (e *JSONExpressionParser) Pretty(level int) string { |
||||
return commonPrefixIndent(level, e) |
||||
} |
||||
|
||||
// e.g: sum_over_time({foo="bar"} | logfmt | unwrap bytes_processed [5m])
|
||||
func (e *UnwrapExpr) Pretty(level int) string { |
||||
s := indent(level) |
||||
|
||||
if e.Operation != "" { |
||||
s += fmt.Sprintf("%s %s %s(%s)", OpPipe, OpUnwrap, e.Operation, e.Identifier) |
||||
} else { |
||||
s += fmt.Sprintf("%s %s %s", OpPipe, OpUnwrap, e.Identifier) |
||||
} |
||||
for _, f := range e.PostFilters { |
||||
s += fmt.Sprintf("\n%s%s %s", indent(level), OpPipe, f) |
||||
} |
||||
return s |
||||
} |
||||
|
||||
// e.g: `{foo="bar"}|logfmt[5m]`
|
||||
// TODO(kavi): Rename `LogRange` -> `LogRangeExpr` (to be consistent with other expressions?)
|
||||
func (e *LogRange) Pretty(level int) string { |
||||
s := e.Left.Pretty(level) |
||||
|
||||
if e.Unwrap != nil { |
||||
// NOTE: | unwrap should go to newline
|
||||
s += "\n" |
||||
s += e.Unwrap.Pretty(level + 1) |
||||
} |
||||
|
||||
// TODO: this will put [1m] on the same line, not in new line as people used to now.
|
||||
s = fmt.Sprintf("%s [%s]", s, model.Duration(e.Interval)) |
||||
|
||||
if e.Offset != 0 { |
||||
oe := OffsetExpr{Offset: e.Offset} |
||||
s += oe.Pretty(level) |
||||
} |
||||
|
||||
return s |
||||
} |
||||
|
||||
// e.g: count_over_time({foo="bar"}[5m] offset 3h)
|
||||
// TODO(kavi): why does offset not work in log queries? e.g: `{foo="bar"} offset 1h`? is it bug? or anything else?
|
||||
// NOTE: Also offset expression never to be indented. It always goes with its parent expression (usually RangeExpr).
|
||||
func (e *OffsetExpr) Pretty(level int) string { |
||||
// using `model.Duration` as it can format ignoring zero units.
|
||||
// e.g: time.Duration(2 * Hour) -> "2h0m0s"
|
||||
// but model.Duration(2 * Hour) -> "2h"
|
||||
return fmt.Sprintf(" %s %s", OpOffset, model.Duration(e.Offset)) |
||||
} |
||||
|
||||
// e.g: count_over_time({foo="bar"}[5m])
|
||||
func (e *RangeAggregationExpr) Pretty(level int) string { |
||||
s := indent(level) |
||||
if !needSplit(e) { |
||||
return s + e.String() |
||||
} |
||||
|
||||
s += e.Operation // e.g: quantile_over_time
|
||||
|
||||
s += "(\n" |
||||
|
||||
// print args to the function.
|
||||
if e.Params != nil { |
||||
s = fmt.Sprintf("%s%s%s,", s, indent(level+1), fmt.Sprint(*e.Params)) |
||||
s += "\n" |
||||
} |
||||
|
||||
s += e.Left.Pretty(level + 1) |
||||
|
||||
s += "\n" + indent(level) + ")" |
||||
|
||||
if e.Grouping != nil { |
||||
s += e.Grouping.Pretty(level) |
||||
} |
||||
|
||||
return s |
||||
} |
||||
|
||||
// e.g:
|
||||
// sum(count_over_time({foo="bar"}[5m])) by (container)
|
||||
// topk(10, count_over_time({foo="bar"}[5m])) by (container)
|
||||
|
||||
// Syntax: <aggr-op>([parameter,] <vector expression>) [without|by (<label list>)]
|
||||
// <aggr-op> - sum, avg, bottomk, topk, etc.
|
||||
// [parameters,] - optional params, used only by bottomk and topk for now.
|
||||
// <vector expression> - vector on which aggregation is done.
|
||||
// [without|by (<label list)] - optional labels to aggregate either with `by` or `without` clause.
|
||||
func (e *VectorAggregationExpr) Pretty(level int) string { |
||||
s := indent(level) |
||||
|
||||
if !needSplit(e) { |
||||
return s + e.String() |
||||
} |
||||
|
||||
var params []string |
||||
|
||||
// level + 1 because arguments to function will be in newline.
|
||||
left := e.Left.Pretty(level + 1) |
||||
switch e.Operation { |
||||
// e.Params default value (0) can mean a legit param for topk and bottomk
|
||||
case OpTypeBottomK, OpTypeTopK: |
||||
params = []string{fmt.Sprintf("%s%d", indent(level+1), e.Params), left} |
||||
|
||||
default: |
||||
if e.Params != 0 { |
||||
params = []string{fmt.Sprintf("%s%d", indent(level+1), e.Params), left} |
||||
} else { |
||||
params = []string{left} |
||||
} |
||||
} |
||||
|
||||
s += e.Operation |
||||
if e.Grouping != nil { |
||||
s += e.Grouping.Pretty(level) |
||||
} |
||||
|
||||
// (\n [params,\n])
|
||||
s += "(\n" |
||||
for i, v := range params { |
||||
s += v |
||||
// LogQL doesn't allow `,` at the end of last argument.
|
||||
if i < len(params)-1 { |
||||
s += "," |
||||
} |
||||
s += "\n" |
||||
} |
||||
s += indent(level) + ")" |
||||
|
||||
return s |
||||
} |
||||
|
||||
// e.g: Any operations involving
|
||||
// "or", "and" and "unless" (logical/set)
|
||||
// "+", "-", "*", "/", "%", "^" (arithmetic)
|
||||
// "==", "!=", ">", ">=", "<", "<=" (comparison)
|
||||
func (e *BinOpExpr) Pretty(level int) string { |
||||
|
||||
s := indent(level) |
||||
if !needSplit(e) { |
||||
return s + e.String() |
||||
} |
||||
|
||||
s = e.SampleExpr.Pretty(level+1) + "\n" |
||||
|
||||
op := formatBinaryOp(e.Op, e.Opts) |
||||
s += indent(level) + op + "\n" |
||||
s += e.RHS.Pretty(level + 1) |
||||
|
||||
return s |
||||
} |
||||
|
||||
// e.g: 4.6
|
||||
func (e *LiteralExpr) Pretty(level int) string { |
||||
return commonPrefixIndent(level, e) |
||||
} |
||||
|
||||
// e.g: label_replace(rate({job="api-server",service="a:c"}[5m]), "foo", "$1", "service", "(.*):.*")
|
||||
func (e *LabelReplaceExpr) Pretty(level int) string { |
||||
s := indent(level) |
||||
|
||||
if !needSplit(e) { |
||||
return s + e.String() |
||||
} |
||||
|
||||
s += OpLabelReplace |
||||
|
||||
s += "(\n" |
||||
|
||||
params := []string{ |
||||
e.Left.Pretty(level + 1), |
||||
indent(level+1) + strconv.Quote(e.Dst), |
||||
indent(level+1) + strconv.Quote(e.Replacement), |
||||
indent(level+1) + strconv.Quote(e.Src), |
||||
indent(level+1) + strconv.Quote(e.Regex), |
||||
} |
||||
|
||||
for i, v := range params { |
||||
s += v |
||||
// LogQL doesn't allow `,` at the end of last argument.
|
||||
if i < len(params)-1 { |
||||
s += "," |
||||
} |
||||
s += "\n" |
||||
} |
||||
|
||||
s += indent(level) + ")" |
||||
|
||||
return s |
||||
} |
||||
|
||||
// e.g: vector(5)
|
||||
func (e *VectorExpr) Pretty(level int) string { |
||||
return commonPrefixIndent(level, e) |
||||
} |
||||
|
||||
// Grouping is techincally not expression type. But used in both range and vector aggregations (`by` and `without` clause)
|
||||
// So by implenting `Pretty` for Grouping, we can re use it for both.
|
||||
// NOTE: indent is ignored for `Grouping`, because grouping always stays in the same line of it's parent expression.
|
||||
|
||||
// e.g:
|
||||
// by(container,namespace) -> by (container, namespace)
|
||||
func (g *Grouping) Pretty(_ int) string { |
||||
var s string |
||||
|
||||
if g.Without { |
||||
s += " without" |
||||
} else if len(g.Groups) > 0 { |
||||
s += " by" |
||||
} |
||||
|
||||
if len(g.Groups) > 0 { |
||||
s += " (" |
||||
s += strings.Join(g.Groups, ", ") |
||||
s += ")" |
||||
} |
||||
return s |
||||
} |
||||
|
||||
// Helpers
|
||||
|
||||
func commonPrefixIndent(level int, current Expr) string { |
||||
return fmt.Sprintf("%s%s", indent(level), current.String()) |
||||
} |
||||
|
||||
func needSplit(e Expr) bool { |
||||
if e == nil { |
||||
return false |
||||
} |
||||
return len(e.String()) > maxCharsPerLine |
||||
} |
||||
|
||||
const indentString = " " |
||||
|
||||
func indent(level int) string { |
||||
return strings.Repeat(indentString, level) |
||||
} |
||||
|
||||
func formatBinaryOp(op string, opts *BinOpOptions) string { |
||||
if opts == nil { |
||||
return op |
||||
} |
||||
|
||||
if opts.ReturnBool { |
||||
// e.g: ">= bool 1"
|
||||
op += " bool" |
||||
} |
||||
|
||||
if opts.VectorMatching != nil { |
||||
group := "" // default one-to-one
|
||||
if opts.VectorMatching.Card == CardManyToOne { |
||||
group = OpGroupLeft |
||||
} |
||||
if opts.VectorMatching.Card == CardOneToMany { |
||||
group = OpGroupRight |
||||
} |
||||
|
||||
if len(opts.VectorMatching.Include) > 0 { |
||||
// e.g: group_left (node, name)
|
||||
group = fmt.Sprintf("%s (%s)", group, strings.Join(opts.VectorMatching.Include, ", ")) |
||||
} |
||||
|
||||
if len(opts.VectorMatching.MatchingLabels) > 0 { |
||||
on := OpOn |
||||
if !opts.VectorMatching.On { |
||||
on = OpIgnoring |
||||
} |
||||
// e.g: * on (cluster, namespace) group_left
|
||||
op = fmt.Sprintf("%s %s (%s) %s", op, on, strings.Join(opts.VectorMatching.MatchingLabels, ", "), group) |
||||
} |
||||
} |
||||
return op |
||||
} |
@ -0,0 +1,406 @@ |
||||
package syntax |
||||
|
||||
import ( |
||||
"testing" |
||||
|
||||
"github.com/stretchr/testify/assert" |
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
func TestFormat(t *testing.T) { |
||||
maxCharsPerLine = 20 |
||||
|
||||
cases := []struct { |
||||
name string |
||||
in string |
||||
exp string |
||||
}{ |
||||
{ |
||||
name: "basic stream selector", |
||||
in: `{job="loki", instance="localhost"}`, |
||||
exp: `{job="loki", instance="localhost"}`, |
||||
}, |
||||
{ |
||||
name: "pipeline_label_filter", |
||||
in: `{job="loki", instance="localhost"}|logfmt|level="error" `, |
||||
exp: `{job="loki", instance="localhost"} |
||||
| logfmt |
||||
| level="error"`, |
||||
}, |
||||
{ |
||||
name: "pipeline_line_filter", |
||||
in: `{job="loki", instance="localhost"}|= "error" != "memcached" |= ip("192.168.0.1") |logfmt`, |
||||
exp: `{job="loki", instance="localhost"} |
||||
|= "error" |
||||
!= "memcached" |
||||
|= ip("192.168.0.1") |
||||
| logfmt`, |
||||
}, |
||||
{ |
||||
name: "pipeline_line_format", |
||||
in: `{job="loki", instance="localhost"}|logfmt|line_format "{{.error}}"`, |
||||
exp: `{job="loki", instance="localhost"} |
||||
| logfmt |
||||
| line_format "{{.error}}"`, |
||||
}, |
||||
{ |
||||
name: "pipeline_label_format", |
||||
in: `{job="loki", instance="localhost"}|logfmt|label_format dst="{{.src}}"`, |
||||
exp: `{job="loki", instance="localhost"} |
||||
| logfmt |
||||
| label_format dst="{{.src}}"`, |
||||
}, |
||||
{ |
||||
name: "aggregation", |
||||
in: `count_over_time({job="loki", instance="localhost"}|logfmt[1m])`, |
||||
exp: `count_over_time( |
||||
{job="loki", instance="localhost"} |
||||
| logfmt [1m] |
||||
)`, |
||||
}, |
||||
{ |
||||
name: "aggregation_with_offset", |
||||
in: `count_over_time({job="loki", instance="localhost"}|= "error"[5m] offset 20m)`, |
||||
exp: `count_over_time( |
||||
{job="loki", instance="localhost"} |
||||
|= "error" [5m] offset 20m |
||||
)`, |
||||
}, |
||||
{ |
||||
name: "unwrap", |
||||
in: `quantile_over_time(0.99,{container="ingress-nginx",service="hosted-grafana"}| json| unwrap response_latency_seconds| __error__=""[1m]) by (cluster)`, |
||||
exp: `quantile_over_time( |
||||
0.99, |
||||
{container="ingress-nginx", service="hosted-grafana"} |
||||
| json |
||||
| unwrap response_latency_seconds |
||||
| __error__="" [1m] |
||||
) by (cluster)`, |
||||
}, |
||||
{ |
||||
name: "pipeline_aggregation_line_filter", |
||||
in: `count_over_time({job="loki", instance="localhost"}|= "error" != "memcached" |= ip("192.168.0.1") |logfmt[1m])`, |
||||
exp: `count_over_time( |
||||
{job="loki", instance="localhost"} |
||||
|= "error" |
||||
!= "memcached" |
||||
|= ip("192.168.0.1") |
||||
| logfmt [1m] |
||||
)`, |
||||
}, |
||||
{ |
||||
name: "jsonparserExpr", |
||||
in: `{job="loki", namespace="loki-prod", container="nginx-ingress"}| json first_server="servers[0]", ua="request.headers[\"User-Agent\"]" | level="error"`, |
||||
exp: `{job="loki", namespace="loki-prod", container="nginx-ingress"} |
||||
| json first_server="servers[0]",ua="request.headers[\"User-Agent\"]" |
||||
| level="error"`, |
||||
}, |
||||
} |
||||
|
||||
for _, c := range cases { |
||||
t.Run(c.name, func(t *testing.T) { |
||||
expr, err := ParseExpr(c.in) |
||||
require.NoError(t, err) |
||||
got := Prettify(expr) |
||||
assert.Equal(t, c.exp, got) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func TestFormat_VectorAggregation(t *testing.T) { |
||||
maxCharsPerLine = 20 |
||||
|
||||
cases := []struct { |
||||
name string |
||||
in string |
||||
exp string |
||||
}{ |
||||
{ |
||||
name: "sum", |
||||
in: `sum(count_over_time({foo="bar",namespace="loki",instance="localhost"}[5m])) by (container)`, |
||||
exp: `sum by (container)( |
||||
count_over_time( |
||||
{foo="bar", namespace="loki", instance="localhost"} [5m] |
||||
) |
||||
)`, |
||||
}, |
||||
{ |
||||
name: "topk", |
||||
in: `topk(5, count_over_time({foo="bar",namespace="loki",instance="localhost"}[5m])) by (container)`, |
||||
exp: `topk by (container)( |
||||
5, |
||||
count_over_time( |
||||
{foo="bar", namespace="loki", instance="localhost"} [5m] |
||||
) |
||||
)`, |
||||
}, |
||||
} |
||||
|
||||
for _, c := range cases { |
||||
t.Run(c.name, func(t *testing.T) { |
||||
expr, err := ParseExpr(c.in) |
||||
require.NoError(t, err) |
||||
got := Prettify(expr) |
||||
assert.Equal(t, c.exp, got) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func TestFormat_LabelReplace(t *testing.T) { |
||||
maxCharsPerLine = 20 |
||||
|
||||
cases := []struct { |
||||
name string |
||||
in string |
||||
exp string |
||||
}{ |
||||
{ |
||||
name: "label_replace", |
||||
in: `label_replace(rate({job="api-server",service="a:c"}|= "err" [5m]), "foo", "$1", "service", "(.*):.*")`, |
||||
exp: `label_replace( |
||||
rate( |
||||
{job="api-server", service="a:c"} |
||||
|= "err" [5m] |
||||
), |
||||
"foo", |
||||
"$1", |
||||
"service", |
||||
"(.*):.*" |
||||
)`, |
||||
}, |
||||
{ |
||||
name: "label_replace_nested", |
||||
in: `label_replace(label_replace(rate({job="api-server",service="a:c"}|= "err" [5m]), "foo", "$1", "service", "(.*):.*"), "foo", "$1", "service", "(.*):.*")`, |
||||
exp: `label_replace( |
||||
label_replace( |
||||
rate( |
||||
{job="api-server", service="a:c"} |
||||
|= "err" [5m] |
||||
), |
||||
"foo", |
||||
"$1", |
||||
"service", |
||||
"(.*):.*" |
||||
), |
||||
"foo", |
||||
"$1", |
||||
"service", |
||||
"(.*):.*" |
||||
)`, |
||||
}, |
||||
} |
||||
|
||||
for _, c := range cases { |
||||
t.Run(c.name, func(t *testing.T) { |
||||
expr, err := ParseExpr(c.in) |
||||
require.NoError(t, err) |
||||
got := Prettify(expr) |
||||
assert.Equal(t, c.exp, got) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func TestFormat_BinOp(t *testing.T) { |
||||
maxCharsPerLine = 20 |
||||
|
||||
cases := []struct { |
||||
name string |
||||
in string |
||||
exp string |
||||
}{ |
||||
{ |
||||
name: "single binop", |
||||
in: `sum(rate({job="loki", namespace="loki-prod", instance="localhost"}[5m]))/sum(count_over_time({job="loki", namespace="loki-prod", instance="localhost"}[5m]))`, |
||||
exp: ` sum( |
||||
rate( |
||||
{job="loki", namespace="loki-prod", instance="localhost"} [5m] |
||||
) |
||||
) |
||||
/ |
||||
sum( |
||||
count_over_time( |
||||
{job="loki", namespace="loki-prod", instance="localhost"} [5m] |
||||
) |
||||
)`, |
||||
}, |
||||
{ |
||||
name: "multiple binops", |
||||
in: `sum(rate({job="loki"}[5m])) + sum(rate({job="loki-dev"}[5m])) / sum(rate({job="loki-prod"}[5m]))`, |
||||
exp: ` sum( |
||||
rate( |
||||
{job="loki"} [5m] |
||||
) |
||||
) |
||||
+ |
||||
sum( |
||||
rate( |
||||
{job="loki-dev"} [5m] |
||||
) |
||||
) |
||||
/ |
||||
sum( |
||||
rate( |
||||
{job="loki-prod"} [5m] |
||||
) |
||||
)`, |
||||
}, |
||||
// NOTE: LogQL binary arithmetic ops have following precedences rules
|
||||
// 1. * / % - higher priority
|
||||
// 2. + - - lower priority.
|
||||
// 3. Between same priority ops, whichever comes first takes precedence.
|
||||
// Following `_precedence*` tests makes sure LogQL formatter respects that.
|
||||
{ |
||||
name: "multiple binops check precedence", |
||||
in: `sum(rate({job="loki"}[5m])) / sum(rate({job="loki-dev"}[5m])) + sum(rate({job="loki-prod"}[5m]))`, |
||||
exp: ` sum( |
||||
rate( |
||||
{job="loki"} [5m] |
||||
) |
||||
) |
||||
/ |
||||
sum( |
||||
rate( |
||||
{job="loki-dev"} [5m] |
||||
) |
||||
) |
||||
+ |
||||
sum( |
||||
rate( |
||||
{job="loki-prod"} [5m] |
||||
) |
||||
)`, |
||||
}, |
||||
{ |
||||
name: "multiple binops check precedence2", |
||||
in: `sum(rate({job="loki"}[5m])) - sum(rate({job="loki-stage"}[5m])) / sum(rate({job="loki-dev"}[5m])) + sum(rate({job="loki-prod"}[5m]))`, |
||||
exp: ` sum( |
||||
rate( |
||||
{job="loki"} [5m] |
||||
) |
||||
) |
||||
- |
||||
sum( |
||||
rate( |
||||
{job="loki-stage"} [5m] |
||||
) |
||||
) |
||||
/ |
||||
sum( |
||||
rate( |
||||
{job="loki-dev"} [5m] |
||||
) |
||||
) |
||||
+ |
||||
sum( |
||||
rate( |
||||
{job="loki-prod"} [5m] |
||||
) |
||||
)`, |
||||
}, |
||||
{ |
||||
name: "multiple binops check precedence3", |
||||
in: `sum(rate({job="loki"}[5m])) - sum(rate({job="loki-stage"}[5m])) % sum(rate({job="loki-dev"}[5m])) + sum(rate({job="loki-prod"}[5m]))`, |
||||
exp: ` sum( |
||||
rate( |
||||
{job="loki"} [5m] |
||||
) |
||||
) |
||||
- |
||||
sum( |
||||
rate( |
||||
{job="loki-stage"} [5m] |
||||
) |
||||
) |
||||
% |
||||
sum( |
||||
rate( |
||||
{job="loki-dev"} [5m] |
||||
) |
||||
) |
||||
+ |
||||
sum( |
||||
rate( |
||||
{job="loki-prod"} [5m] |
||||
) |
||||
)`, |
||||
}, |
||||
{ |
||||
name: "multiple binops check precedence4", |
||||
in: `sum(rate({job="loki"}[5m])) / sum(rate({job="loki-stage"}[5m])) % sum(rate({job="loki-dev"}[5m])) + sum(rate({job="loki-prod"}[5m]))`, |
||||
exp: ` sum( |
||||
rate( |
||||
{job="loki"} [5m] |
||||
) |
||||
) |
||||
/ |
||||
sum( |
||||
rate( |
||||
{job="loki-stage"} [5m] |
||||
) |
||||
) |
||||
% |
||||
sum( |
||||
rate( |
||||
{job="loki-dev"} [5m] |
||||
) |
||||
) |
||||
+ |
||||
sum( |
||||
rate( |
||||
{job="loki-prod"} [5m] |
||||
) |
||||
)`, |
||||
}, |
||||
{ |
||||
name: "multiple binops check precedence5", |
||||
in: `sum(rate({job="loki"}[5m])) / sum(rate({job="loki-stage"}[5m])) % sum(rate({job="loki-dev"}[5m])) * sum(rate({job="loki-prod"}[5m]))`, |
||||
exp: ` sum( |
||||
rate( |
||||
{job="loki"} [5m] |
||||
) |
||||
) |
||||
/ |
||||
sum( |
||||
rate( |
||||
{job="loki-stage"} [5m] |
||||
) |
||||
) |
||||
% |
||||
sum( |
||||
rate( |
||||
{job="loki-dev"} [5m] |
||||
) |
||||
) |
||||
* |
||||
sum( |
||||
rate( |
||||
{job="loki-prod"} [5m] |
||||
) |
||||
)`, |
||||
}, |
||||
{ |
||||
name: "binops with options", // options - on, ignoring, group_left, group_right
|
||||
in: `sum(rate({job="loki"}[5m])) * on(instance, job) group_left (node) sum(rate({job="loki-prod"}[5m]))`, |
||||
exp: ` sum( |
||||
rate( |
||||
{job="loki"} [5m] |
||||
) |
||||
) |
||||
* on (instance, job) group_left (node) |
||||
sum( |
||||
rate( |
||||
{job="loki-prod"} [5m] |
||||
) |
||||
)`, |
||||
}, |
||||
} |
||||
|
||||
for _, c := range cases { |
||||
t.Run(c.name, func(t *testing.T) { |
||||
expr, err := ParseExpr(c.in) |
||||
require.NoError(t, err) |
||||
got := Prettify(expr) |
||||
assert.Equal(t, c.exp, got) |
||||
}) |
||||
} |
||||
} |
@ -0,0 +1,24 @@ |
||||
package loki |
||||
|
||||
import ( |
||||
"fmt" |
||||
"net/http" |
||||
|
||||
"github.com/grafana/loki/pkg/logql/syntax" |
||||
serverutil "github.com/grafana/loki/pkg/util/server" |
||||
) |
||||
|
||||
func formatQueryHandler() http.HandlerFunc { |
||||
return func(w http.ResponseWriter, r *http.Request) { |
||||
expr, err := syntax.ParseExpr(r.FormValue("query")) |
||||
if err != nil { |
||||
serverutil.WriteError(err, w) |
||||
return |
||||
} |
||||
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8") |
||||
w.WriteHeader(http.StatusOK) |
||||
|
||||
fmt.Fprintf(w, "%s", syntax.Prettify(expr)) |
||||
} |
||||
} |
Loading…
Reference in new issue