From e2aee2a9c32817486acb601e773d9d08090e19b9 Mon Sep 17 00:00:00 2001 From: Cyril Tovena Date: Fri, 6 Nov 2020 02:11:52 -0500 Subject: [PATCH] LogQL: Improve template format (#2822) * Add error labels on failure and new functions for templates. Signed-off-by: Cyril Tovena * Updates docs. Signed-off-by: Cyril Tovena * Update docs/sources/logql/_index.md Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> * Update docs/sources/logql/_index.md Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> * Update docs/sources/logql/functions.md Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> * Update docs/sources/logql/functions.md Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> * Update docs/sources/logql/functions.md Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> * Update docs/sources/logql/functions.md Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> * Update docs/sources/logql/functions.md Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> * Update docs/sources/logql/functions.md Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> * Update docs/sources/logql/functions.md Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> * Update docs/sources/logql/functions.md Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> * Update docs/sources/logql/functions.md Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> * Update docs/sources/logql/functions.md Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> * Update docs/sources/logql/functions.md Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> * Update docs/sources/logql/functions.md Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> * Update docs/sources/logql/functions.md Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> * Update docs/sources/logql/functions.md Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> * Update docs/sources/logql/functions.md Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> * Update docs/sources/logql/functions.md Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> * Update docs/sources/logql/functions.md Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> * Update docs/sources/logql/functions.md Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> * Update docs/sources/logql/functions.md Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> * Update docs/sources/logql/functions.md Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> * Update docs/sources/logql/functions.md Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> * Improve docs based off review feedback. Signed-off-by: Cyril Tovena * Review feedback. Signed-off-by: Cyril Tovena * Fixes panic out of boundary Signed-off-by: Cyril Tovena * Update docs/sources/logql/functions.md Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> * Update docs/sources/logql/functions.md Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> * Update docs/sources/logql/functions.md Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> * Update docs/sources/logql/functions.md Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> * Update docs/sources/logql/_index.md Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> * Update docs/sources/logql/functions.md Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> * Update docs/sources/logql/functions.md Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> * Update docs/sources/logql/_index.md Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> --- docs/sources/logql/_index.md | 81 +------ docs/sources/logql/functions.md | 370 ++++++++++++++++++++++++++++++++ pkg/logql/log/error.go | 1 + pkg/logql/log/fmt.go | 95 +++++++- pkg/logql/log/fmt_test.go | 161 ++++++++++++++ 5 files changed, 625 insertions(+), 83 deletions(-) create mode 100644 docs/sources/logql/functions.md diff --git a/docs/sources/logql/_index.md b/docs/sources/logql/_index.md index 51bb049a753e..f353bc676dd8 100644 --- a/docs/sources/logql/_index.md +++ b/docs/sources/logql/_index.md @@ -295,7 +295,7 @@ Will extract and rewrite the log line to only contains the query and the duratio You can use double quoted string for the template or single backtick \``\{{.label_name}}`\` to avoid the need to escape special characters. -See [functions](#Template-functions) to learn about available functions in the template format. +See [functions](functions/) to learn about available functions in the template format. #### Labels Format Expression @@ -303,89 +303,14 @@ The `| label_format` expression can renamed, modify or add labels. It takes as p When both side are label identifiers, for example `dst=src`, the operation will rename the `src` label into `dst`. -The left side can alternatively be a template string (double quoted or backtick), for example `dst="{{.status}} {{.query}}"`, in which case the `dst` label value will be replace by the result of the [text/template](https://golang.org/pkg/text/template/) evaluation. This is the same template engine as the `| line_format` expression, this mean labels are available as variables and you can use the same list of [functions](#Template-functions). +The left side can alternatively be a template string (double quoted or backtick), for example `dst="{{.status}} {{.query}}"`, in which case the `dst` label value is replaced by the result of the [text/template](https://golang.org/pkg/text/template/) evaluation. This is the same template engine as the `| line_format` expression, which means labels are available as variables and you can use the same list of [functions](functions/). -In both case if the destination label doesn't exist a new one will be created. +In both cases, if the destination label doesn't exist, then a new one is created. The renaming form `dst=src` will _drop_ the `src` label after remapping it to the `dst` label. However, the _template_ form will preserve the referenced labels, such that `dst="{{.src}}"` results in both `dst` and `src` having the same value. > A single label name can only appear once per expression. This means `| label_format foo=bar,foo="new"` is not allowed but you can use two expressions for the desired effect: `| label_format foo=bar | label_format foo="new"` -#### Template functions - -The text template format used in `| line_format` and `| label_format` support functions the following list of functions. - -##### ToLower & ToUpper - -Convert the entire string to lowercase or uppercase: - -Examples: - -```template -"{{.request_method | ToLower}}" -"{{.request_method | ToUpper}}" -`{{ToUpper "This is a string" | ToLower}}` -``` - -##### Replace - -Perform simple string replacement. - -It takes three arguments: - -- string to replace -- string to replace with -- source string - -Example: - -```template -`"This is a string" | Replace " " "-"` -``` - -The above will produce `This-is-a-string` - -##### Trim - -`Trim` returns a slice of the string s with all leading and -trailing Unicode code points contained in cutset removed. - -`TrimLeft` and `TrimRight` are the same as `Trim` except that it respectively trim only leading and trailing characters. - -```template -`{{ Trim .query ",. " }}` -`{{ TrimLeft .uri ":" }}` -`{{ TrimRight .path "/" }}` -``` - -`TrimSpace` TrimSpace returns string s with all leading -and trailing white space removed, as defined by Unicode. - -```template -{{ TrimSpace .latency }} -``` - -`TrimPrefix` and `TrimSuffix` will trim respectively the prefix or suffix supplied. - -```template -{{ TrimPrefix .path "/" }} -``` - -##### Regex - -`regexReplaceAll` returns a copy of the input string, replacing matches of the Regexp with the replacement string replacement. Inside string replacement, $ signs are interpreted as in Expand, so for instance $1 represents the text of the first sub-match. See the golang [docs](https://golang.org/pkg/regexp/#Regexp.ReplaceAll) for detailed examples. - -```template -`{{ regexReplaceAllLiteral "(a*)bc" .some_label "${1}a" }}` -``` - -`regexReplaceAllLiteral` returns a copy of the input string, replacing matches of the Regexp with the replacement string replacement The replacement string is substituted directly, without using Expand. - -```template -`{{ regexReplaceAllLiteral "(ts=)" .timestamp "timestamp=" }}` -``` - -You can combine multiple function using pipe, for example if you want to strip out spaces and make the request method in capital you would write the following template `{{ .request_method | TrimSpace | ToUpper }}`. ### Log Queries Examples diff --git a/docs/sources/logql/functions.md b/docs/sources/logql/functions.md new file mode 100644 index 000000000000..1728aea06053 --- /dev/null +++ b/docs/sources/logql/functions.md @@ -0,0 +1,370 @@ +--- +title: Template functions +--- + +The [text template](https://golang.org/pkg/text/template) format used in `| line_format` and `| label_format` support the usage of functions. + +All labels are added as variables in the template engine. They can be referenced using they label name prefixed by a `.`(e.g `.label_name`). For example the following template will output the value of the path label: + +```template +{{ .path }} +``` + +You can take advantage of [pipeline](https://golang.org/pkg/text/template/#hdr-Pipelines) to join together multiple functions. +In a chained pipeline, the result of each command is passed as the last argument of the following command. + +Example: + +```template +{{ .path | replace " " "_" | trunc 5 | upper }} +``` + +## ToLower and ToUpper + +This function converts the entire string to lowercase or uppercase. + +Signatures: + +- `ToLower(string) string` +- `ToUpper(string) string` + +Examples: + +```template +"{{.request_method | ToLower}}" +"{{.request_method | ToUpper}}" +`{{ToUpper "This is a string" | ToLower}}` +``` + +> **Note:** In Loki 2.1 you can also use respectively [`lower`](#lower) and [`upper`](#upper) shortcut, e.g `{{.request_method | lower }}`. + +## Replace string + +> **Note:** In Loki 2.1 [`replace`](#replace) (as opposed to `Replace`) is available with a different signature but easier to chain within pipeline. + +Use this function to perform a simple string replacement. + +Signature: + +`Replace(s, old, new string, n int) string` + +It takes four arguments: + +- `s` source string +- `old` string to replace +- `new` string to replace with +- `n` the maximun amount of replacement (-1 for all) + +Example: + +```template +`{{ Replace "This is a string" " " "-" -1 }}` +``` + +The results in `This-is-a-string`. + +## Trim, TrimLeft, TrimRight, and TrimSpace + +> **Note:** In Loki 2.1 [trim](#trim), [trimAll](#trimAll), [trimSuffix](#trimSuffix) and [trimPrefix](trimPrefix) have been added with a different signature for better pipeline chaining. + +`Trim` returns a slice of the string s with all leading and +trailing Unicode code points contained in cutset removed. + +Signature: `Trim(value, cutset string) string` + +`TrimLeft` and `TrimRight` are the same as `Trim` except that it trims only leading and trailing characters respectively. + +```template +`{{ Trim .query ",. " }}` +`{{ TrimLeft .uri ":" }}` +`{{ TrimRight .path "/" }}` +``` + +`TrimSpace` TrimSpace returns string s with all leading +and trailing white space removed, as defined by Unicode. + +Signature: `TrimSpace(value string) string` + +```template +{{ TrimSpace .latency }} +``` + +`TrimPrefix` and `TrimSuffix` will trim respectively the prefix or suffix supplied. + +Signature: + +- `TrimPrefix(value string, prefix string) string` +- `TrimSuffix(value string, suffix string) string` + +```template +{{ TrimPrefix .path "/" }} +``` + +## regexReplaceAll and regexReplaceAllLiteral + +`regexReplaceAll` returns a copy of the input string, replacing matches of the Regexp with the replacement string replacement. Inside string replacement, $ signs are interpreted as in Expand, so for instance $1 represents the text of the first sub-match. See the golang [Regexp.replaceAll documentation](https://golang.org/pkg/regexp/#Regexp.ReplaceAll) for more examples. + +```template +`{{ regexReplaceAllLiteral "(a*)bc" .some_label "${1}a" }}` +``` + +`regexReplaceAllLiteral` function returns a copy of the input string and replaces matches of the Regexp with the replacement string replacement. The replacement string is substituted directly, without using Expand. + +```template +`{{ regexReplaceAllLiteral "(ts=)" .timestamp "timestamp=" }}` +``` + +You can combine multiple functions using pipe. For example, to strip out spaces and make the request method in capital, you would write the following template: `{{ .request_method | TrimSpace | ToUpper }}`. + +## lower + +> Added in Loki 2.1 + +Use this function to convert to lower case. + +Signature: + +`lower(string) string` + +Examples: + +```template +"{{ .request_method | lower }}" +`{{ lower "HELLO"}}` +``` + +The last example will return `hello`. + +## upper + +> Added in Loki 2.1 + +Use this function to convert to upper case. + +Signature: + +`upper(string) string` + +Examples: + +```template +"{{ .request_method | upper }}" +`{{ upper "hello"}}` +``` + +This results in `HELLO`. + +## title + +> **Note:** Added in Loki 2.1. + +Convert to title case. + +Signature: + +`title(string) string` + +Examples: + +```template +"{{.request_method | title}}" +`{{ title "hello world"}}` +``` + +The last example will return `Hello World`. + +## trunc + +> **Note:** Added in Loki 2.1. + +Truncate a string and add no suffix. + +Signature: + +`trunc(count int,value string) string` + +Examples: + +```template +"{{ .path | trunc 2 }}" +`{{ trunc 5 "hello world"}}` // output: hello +`{{ trunc -5 "hello world"}}` // output: world +``` + +## substr + +> **Note:** Added in Loki 2.1. + +Get a substring from a string. + +Signature: + +`trunc(start int,end int,value string) string` + +If start is < 0, this calls value[:end]. +If start is >= 0 and end < 0 or end bigger than s length, this calls value[start:] +Otherwise, this calls value[start, end]. + +Examples: + +```template +"{{ .path | substr 2 5 }}" +`{{ substr 0 5 "hello world"}}` // output: hello +`{{ substr 6 11 "hello world"}}` // output: world +``` + +## replace + +> **Note:** Added in Loki 2.1. + +This function performs simple string replacement. + +Signature: `replace(old string, new string, src string) string` + +It takes three arguments: + +- `old` string to replace +- `new` string to replace with +- `src` source string + +Examples: + +```template +{{ .cluster | replace "-cluster" "" }} +{{ replace "hello" "world" "hello world" }} +``` + +The last example will return `world world`. + +## trim + +> **Note:** Added in Loki 2.1. + +The trim function removes space from either side of a string. + +Signature: `trim(string) string` + +Examples: + +```template +{{ .ip | trim }} +{{ trim " hello " }} // output: hello +``` + +## trimAll + +> **Note:** Added in Loki 2.1. + +Use this function to remove given characters from the front or back of a string. + +Signature: `trimAll(chars string,src string) string` + +Examples: + +```template +{{ .path | trimAll "/" }} +{{ trimAll "$" "$5.00" }} // output: 5.00 +``` + +## trimSuffix + +> **Note:** Added in Loki 2.1. + +Use this function to trim just the suffix from a string. + +Signature: `trimSuffix(suffix string, src string) string` + +Examples: + +```template +{{ .path | trimSuffix "/" }} +{{ trimSuffix "-" "hello-" }} // output: hello +``` + +## trimPrefix + +> **Note:** Added in Loki 2.1. + +Use this function to trim just the prefix from a string. + +Signature: `trimPrefix(suffix string, src string) string` + +Examples: + +```template +{{ .path | trimPrefix "/" }} +{{ trimPrefix "-" "-hello" }} // output: hello +``` + +## indent + +> **Note:** Added in Loki 2.1. + +The indent function indents every line in a given string to the specified indent width. This is useful when aligning multi-line strings. + +Signature: `indent(spaces int,src string) string` + +```template +{{ indent 4 .query }} +``` + +This indents each line contained in the `.query` by four (4) spaces. + +## nindent + +> **Note:** Added in Loki 2.1. + +The nindent function is the same as the indent function, but prepends a new line to the beginning of the string. + +Signature: `nindent(spaces int,src string) string` + +```template +{{ nindent 4 .query }} +``` + +This will indent every line of text by 4 space characters and add a new line to the beginning. + +## repeat + +> **Note:** Added in Loki 2.1. + +Use this function to repeat a string multiple times. + +Signature: `repeat(c int,value string) string` + +```template +{{ repeat 3 "hello" }} // output: hellohellohello +``` + +## contains + +> **Note:** Added in Loki 2.1. + +Use this function to test to see if one string is contained inside of another. + +Signature: `contains(s string, src string) bool` + +Examples: + +```template +{{ if .err contains "ErrTimeout" }} timeout {{end}} +{{ if contains "he" "hello" }} yes {{end}} +``` + +## hasPrefix and hasSuffix + +> **Note:** Added in Loki 2.1. + +The `hasPrefix` and `hasSuffix` functions test whether a string has a given prefix or suffix. + +Signatures: + +- `hasPrefix(prefix string, src string) bool` +- `hasSuffix(suffix string, src string) bool` + +Examples: + +```template +{{ if .err hasSuffix "Timeout" }} timeout {{end}} +{{ if hasPrefix "he" "hello" }} yes {{end}} +``` diff --git a/pkg/logql/log/error.go b/pkg/logql/log/error.go index 04dd20ecdcd1..a518b08c5e59 100644 --- a/pkg/logql/log/error.go +++ b/pkg/logql/log/error.go @@ -6,6 +6,7 @@ var ( errLogfmt = "LogfmtParserErr" errSampleExtraction = "SampleExtractionErr" errLabelFilter = "LabelFilterErr" + errTemplateFormat = "TemplateFormatErr" ErrorLabel = "__error__" ) diff --git a/pkg/logql/log/fmt.go b/pkg/logql/log/fmt.go index 231fa3771904..745aed711974 100644 --- a/pkg/logql/log/fmt.go +++ b/pkg/logql/log/fmt.go @@ -14,6 +14,7 @@ var ( // Available map of functions for the text template engine. functionMap = template.FuncMap{ + // olds function deprecated. "ToLower": strings.ToLower, "ToUpper": strings.ToUpper, "Replace": strings.Replace, @@ -23,6 +24,28 @@ var ( "TrimPrefix": strings.TrimPrefix, "TrimSuffix": strings.TrimSuffix, "TrimSpace": strings.TrimSpace, + + // New function ported from https://github.com/Masterminds/sprig/ + // Those function takes the string as the last parameter, allowing pipe chaining. + // Example: .mylabel | lower | substring 0 5 + "lower": strings.ToLower, + "upper": strings.ToUpper, + "title": strings.Title, + "trunc": trunc, + "substr": substring, + "contains": contains, + "hasPrefix": hasPrefix, + "hasSuffix": hasSuffix, + "indent": indent, + "nindent": nindent, + "replace": replace, + "repeat": repeat, + "trim": strings.TrimSpace, + "trimAll": trimAll, + "trimSuffix": trimSuffix, + "trimPrefix": trimPrefix, + + // regex functions "regexReplaceAll": func(regex string, s string, repl string) string { r := regexp.MustCompile(regex) return r.ReplaceAllString(s, repl) @@ -51,10 +74,12 @@ func NewFormatter(tmpl string) (*LineFormatter, error) { }, nil } -func (lf *LineFormatter) Process(_ []byte, lbs *LabelsBuilder) ([]byte, bool) { +func (lf *LineFormatter) Process(line []byte, lbs *LabelsBuilder) ([]byte, bool) { lf.buf.Reset() - // todo(cyriltovena): handle error - _ = lf.Template.Execute(lf.buf, lbs.Labels().Map()) + if err := lf.Template.Execute(lf.buf, lbs.Labels().Map()); err != nil { + lbs.SetErr(errTemplateFormat) + return line, true + } // todo(cyriltovena): we might want to reuse the input line or a bytes buffer. res := make([]byte, len(lf.buf.Bytes())) copy(res, lf.buf.Bytes()) @@ -150,12 +175,72 @@ func (lf *LabelsFormatter) Process(l []byte, lbs *LabelsBuilder) ([]byte, bool) continue } lf.buf.Reset() - //todo (cyriltovena): handle error if data == nil { data = lbs.Labels().Map() } - _ = f.tmpl.Execute(lf.buf, data) + if err := f.tmpl.Execute(lf.buf, data); err != nil { + lbs.SetErr(errTemplateFormat) + continue + } lbs.Set(f.Name, lf.buf.String()) } return l, true } + +func trunc(c int, s string) string { + runes := []rune(s) + l := len(runes) + if c < 0 && l+c > 0 { + return string(runes[l+c:]) + } + if c >= 0 && l > c { + return string(runes[:c]) + } + return s +} + +// substring creates a substring of the given string. +// +// If start is < 0, this calls string[:end]. +// +// If start is >= 0 and end < 0 or end bigger than s length, this calls string[start:] +// +// Otherwise, this calls string[start, end]. +func substring(start, end int, s string) string { + runes := []rune(s) + l := len(runes) + if end > l { + end = l + } + if start > l { + start = l + } + if start < 0 { + if end < 0 { + return "" + } + return string(runes[:end]) + } + if end < 0 { + return string(runes[start:]) + } + if start > end { + return "" + } + return string(runes[start:end]) +} + +func contains(substr string, str string) bool { return strings.Contains(str, substr) } +func hasPrefix(substr string, str string) bool { return strings.HasPrefix(str, substr) } +func hasSuffix(substr string, str string) bool { return strings.HasSuffix(str, substr) } +func repeat(count int, str string) string { return strings.Repeat(str, count) } +func replace(old, new, src string) string { return strings.Replace(src, old, new, -1) } +func trimAll(a, b string) string { return strings.Trim(b, a) } +func trimSuffix(a, b string) string { return strings.TrimSuffix(b, a) } +func trimPrefix(a, b string) string { return strings.TrimPrefix(b, a) } +func indent(spaces int, v string) string { + pad := strings.Repeat(" ", spaces) + return pad + strings.Replace(v, "\n", "\n"+pad, -1) +} + +func nindent(spaces int, v string) string { return "\n" + indent(spaces, v) } diff --git a/pkg/logql/log/fmt_test.go b/pkg/logql/log/fmt_test.go index a7bbf8889f08..784c1434983f 100644 --- a/pkg/logql/log/fmt_test.go +++ b/pkg/logql/log/fmt_test.go @@ -1,6 +1,7 @@ package log import ( + "fmt" "sort" "testing" @@ -24,6 +25,115 @@ func Test_lineFormatter_Format(t *testing.T) { []byte("fooblipbuzzblop"), labels.Labels{{Name: "foo", Value: "blip"}, {Name: "bar", Value: "blop"}}, }, + { + "Replace", + newMustLineFormatter(`foo{{.foo}}buzz{{ Replace .bar "blop" "bar" -1 }}`), + labels.Labels{{Name: "foo", Value: "blip"}, {Name: "bar", Value: "blop"}}, + []byte("fooblipbuzzbar"), + labels.Labels{{Name: "foo", Value: "blip"}, {Name: "bar", Value: "blop"}}, + }, + { + "replace", + newMustLineFormatter(`foo{{.foo}}buzz{{ .bar | replace "blop" "bar" }}`), + labels.Labels{{Name: "foo", Value: "blip"}, {Name: "bar", Value: "blop"}}, + []byte("fooblipbuzzbar"), + labels.Labels{{Name: "foo", Value: "blip"}, {Name: "bar", Value: "blop"}}, + }, + { + "title", + newMustLineFormatter(`{{.foo | title }}`), + labels.Labels{{Name: "foo", Value: "blip"}, {Name: "bar", Value: "blop"}}, + []byte("Blip"), + labels.Labels{{Name: "foo", Value: "blip"}, {Name: "bar", Value: "blop"}}, + }, + { + "substr and trunc", + newMustLineFormatter( + `{{.foo | substr 1 3 }} {{ .bar | trunc 1 }} {{ .bar | trunc 3 }}`, + ), + labels.Labels{{Name: "foo", Value: "blip"}, {Name: "bar", Value: "blop"}}, + []byte("li b blo"), + labels.Labels{{Name: "foo", Value: "blip"}, {Name: "bar", Value: "blop"}}, + }, + { + "trim", + newMustLineFormatter( + `{{.foo | trim }} {{ .bar | trimAll "op" }} {{ .bar | trimPrefix "b" }} {{ .bar | trimSuffix "p" }}`, + ), + labels.Labels{{Name: "foo", Value: " blip "}, {Name: "bar", Value: "blop"}}, + []byte("blip bl lop blo"), + labels.Labels{{Name: "foo", Value: " blip "}, {Name: "bar", Value: "blop"}}, + }, + { + "lower and upper", + newMustLineFormatter(`{{.foo | lower }} {{ .bar | upper }}`), + labels.Labels{{Name: "foo", Value: "BLIp"}, {Name: "bar", Value: "blop"}}, + []byte("blip BLOP"), + labels.Labels{{Name: "foo", Value: "BLIp"}, {Name: "bar", Value: "blop"}}, + }, + { + "repeat", + newMustLineFormatter(`{{ "foo" | repeat 3 }}`), + labels.Labels{{Name: "foo", Value: "BLIp"}, {Name: "bar", Value: "blop"}}, + []byte("foofoofoo"), + labels.Labels{{Name: "foo", Value: "BLIp"}, {Name: "bar", Value: "blop"}}, + }, + { + "indent", + newMustLineFormatter(`{{ "foo\n bar" | indent 4 }}`), + labels.Labels{{Name: "foo", Value: "BLIp"}, {Name: "bar", Value: "blop"}}, + []byte(" foo\n bar"), + labels.Labels{{Name: "foo", Value: "BLIp"}, {Name: "bar", Value: "blop"}}, + }, + { + "nindent", + newMustLineFormatter(`{{ "foo" | nindent 2 }}`), + labels.Labels{{Name: "foo", Value: "BLIp"}, {Name: "bar", Value: "blop"}}, + []byte("\n foo"), + labels.Labels{{Name: "foo", Value: "BLIp"}, {Name: "bar", Value: "blop"}}, + }, + { + "contains", + newMustLineFormatter(`{{ if .foo | contains "p"}}yes{{end}}-{{ if .foo | contains "z"}}no{{end}}`), + labels.Labels{{Name: "foo", Value: "BLIp"}, {Name: "bar", Value: "blop"}}, + []byte("yes-"), + labels.Labels{{Name: "foo", Value: "BLIp"}, {Name: "bar", Value: "blop"}}, + }, + { + "hasPrefix", + newMustLineFormatter(`{{ if .foo | hasPrefix "BL" }}yes{{end}}-{{ if .foo | hasPrefix "p"}}no{{end}}`), + labels.Labels{{Name: "foo", Value: "BLIp"}, {Name: "bar", Value: "blop"}}, + []byte("yes-"), + labels.Labels{{Name: "foo", Value: "BLIp"}, {Name: "bar", Value: "blop"}}, + }, + { + "hasSuffix", + newMustLineFormatter(`{{ if .foo | hasSuffix "Ip" }}yes{{end}}-{{ if .foo | hasSuffix "pw"}}no{{end}}`), + labels.Labels{{Name: "foo", Value: "BLIp"}, {Name: "bar", Value: "blop"}}, + []byte("yes-"), + labels.Labels{{Name: "foo", Value: "BLIp"}, {Name: "bar", Value: "blop"}}, + }, + { + "regexReplaceAll", + newMustLineFormatter(`{{ regexReplaceAll "(p)" .foo "t" }}`), + labels.Labels{{Name: "foo", Value: "BLIp"}, {Name: "bar", Value: "blop"}}, + []byte("BLIt"), + labels.Labels{{Name: "foo", Value: "BLIp"}, {Name: "bar", Value: "blop"}}, + }, + { + "regexReplaceAllLiteral", + newMustLineFormatter(`{{ regexReplaceAllLiteral "(p)" .foo "${1}" }}`), + labels.Labels{{Name: "foo", Value: "BLIp"}, {Name: "bar", Value: "blop"}}, + []byte("BLI${1}"), + labels.Labels{{Name: "foo", Value: "BLIp"}, {Name: "bar", Value: "blop"}}, + }, + { + "err", + newMustLineFormatter(`{{.foo Replace "foo"}}`), + labels.Labels{{Name: "foo", Value: "blip"}, {Name: "bar", Value: "blop"}}, + nil, + labels.Labels{{Name: ErrorLabel, Value: errTemplateFormat}, {Name: "foo", Value: "blip"}, {Name: "bar", Value: "blop"}}, + }, { "missing", newMustLineFormatter("foo {{.foo}}buzz{{ .bar }}"), @@ -41,6 +151,8 @@ func Test_lineFormatter_Format(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + sort.Sort(tt.lbs) + sort.Sort(tt.wantLbs) builder := NewBaseLabelsBuilder().ForLabels(tt.lbs, tt.lbs.Hash()) builder.Reset() outLine, _ := tt.fmter.Process(nil, builder) @@ -129,3 +241,52 @@ func Test_validate(t *testing.T) { }) } } + +func Test_trunc(t *testing.T) { + tests := []struct { + s string + c int + want string + }{ + {"Hello, 世界", -1, "界"}, + {"Hello, 世界", 1, "H"}, + {"Hello, 世界", 0, ""}, + {"Hello, 世界", 20, "Hello, 世界"}, + {"Hello, 世界", -20, "Hello, 世界"}, + } + for _, tt := range tests { + t.Run(fmt.Sprintf("%s%d", tt.s, tt.c), func(t *testing.T) { + if got := trunc(tt.c, tt.s); got != tt.want { + t.Errorf("trunc() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_substring(t *testing.T) { + + tests := []struct { + start int + end int + s string + want string + }{ + {1, 8, "Hello, 世界", "ello, 世"}, + {-10, 8, "Hello, 世界", "Hello, 世"}, + {1, 10, "Hello, 世界", "ello, 世界"}, + {-1, 10, "Hello, 世界", "Hello, 世界"}, + {-1, 1, "Hello, 世界", "H"}, + {-1, -1, "Hello, 世界", ""}, + {20, -1, "Hello, 世界", ""}, + {1, 1, "Hello, 世界", ""}, + {5, 1, "Hello, 世界", ""}, + {3, -1, "Hello, 世界", "lo, 世界"}, + } + for _, tt := range tests { + t.Run(tt.s, func(t *testing.T) { + if got := substring(tt.start, tt.end, tt.s); got != tt.want { + t.Errorf("substring() = %v, want %v", got, tt.want) + } + }) + } +}