Like Prometheus, but for logs.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
loki/pkg/logql/syntax/prettier.go

416 lines
11 KiB

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>
3 years ago
// 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)
}
func (e *DropLabelsExpr) Pretty(level int) string {
return commonPrefixIndent(level, e)
}
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>
3 years ago
// 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)
}
Add logfmt selective label extraction (#6675) <!-- Thanks for sending a pull request! Before submitting: 1. Read our CONTRIBUTING.md guide 2. Name your PR as `<Feature Area>: Describe your change`. a. Do not end the title with punctuation. It will be added in the changelog. b. Start with an imperative verb. Example: Fix the latency between System A and System B. c. Use sentence case, not title case. d. Use a complete phrase or sentence. The PR title will appear in a changelog, so help other people understand what your change will be. 3. Rebase your PR if it gets out of sync with main --> **What this PR does / why we need it**: This PR introduces extracting labels from a log line in `logfmt`, with an extra option to rename them. For example, this query: ``` {app=”foo”} | logfmt msg,status ``` will extract the labels `msg` and `status` from the following logfmt line: ``` level=error ts=2021-02-12T19:18:10.037940878Z caller=client.go:294 component=client host=observability-loki-gateway msg="final error sending batch" status=400 error="server returned HTTP status 400 Bad Request (400): entry with timestamp 2021-02-12 19:18:08.917452 +0000 UTC ignored, reason: 'entry out of order' for stream..." ``` With the following results: ``` msg="final error sending batch" status=”400” ``` -------------- Another possible scenario with label renaming: ``` {app=”foo”} | logfmt message="msg", status ``` That produces the following results: ``` message="final error sending batch" status=”400” ``` **Which issue(s) this PR fixes**: Fixes #3355 **Special notes for your reviewer**: <!-- Note about CHANGELOG entries, if a change adds: * an important feature * fixes an issue present in a previous release, * causes a change in operation that would be useful for an operator of Loki to know then please add a CHANGELOG entry. For documentation changes, build changes, simple fixes etc please skip this step. We are attempting to curate a changelog of the most relevant and important changes to be easier to ingest by end users of Loki. Note about the upgrade guide, if this changes: * default configuration values * metric names or label names * changes existing log lines such as the metrics.go query output line * configuration parameters * anything to do with any API * any other change that would require special attention or extra steps to upgrade Please document clearly what changed AND what needs to be done in the upgrade guide. --> **Checklist** - [x] Documentation added - [x] Tests updated - [x] Is this an important fix or new feature? Add an entry in the `CHANGELOG.md`. - [x] Changes that require user attention or interaction to upgrade are documented in `docs/sources/upgrading/_index.md` --------- Signed-off-by: Christian Haudum <christian.haudum@gmail.com> Co-authored-by: Christian Haudum <christian.haudum@gmail.com>
3 years ago
// e.g: | logfmt label="expression", another="expression"
func (e *LogfmtExpressionParser) Pretty(level int) string {
return commonPrefixIndent(level, e)
}
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>
3 years ago
// 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 technically not expression type. But used in both range and vector aggregations (`by` and `without` clause)
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>
3 years ago
// 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
}