From 4c8f13c321acc9e33a7f03462e0782466f1ad3fa Mon Sep 17 00:00:00 2001 From: Masahiro Nagano Date: Sun, 11 Oct 2026 12:23:10 +0900 Subject: [PATCH 1/5] feat: add multiply function and numeric modifiers support to JSON key parsing --- README.md | 15 +++--- cmd/mackerel-plugin-jsonl/jsonkey.go | 59 +++++++++++++++------ cmd/mackerel-plugin-jsonl/jsonkey_test.go | 64 ++++++++++++++++------- cmd/mackerel-plugin-jsonl/reader.go | 61 ++++++++++++--------- 4 files changed, 134 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index 7a63c26..85bed88 100644 --- a/README.md +++ b/README.md @@ -98,16 +98,19 @@ command = "/path/to/mackerel-plugin-jsonl --key-name count --json-key foo.bar -- | 関数 | 説明 | 利用できる aggregator | | --- | --- | --- | -| `tolower` | 値を小文字に変換します。 | `group_by`, `group_by_with_percentage` | -| `toupper` | 値を大文字に変換します。 | `group_by`, `group_by_with_percentage` | -| `trimspace` | 値の前後の空白を除去します。 | `group_by`, `group_by_with_percentage` | -| `replace('pattern','repl')` | 正規表現で値を置換します。 | `group_by`, `group_by_with_percentage` | +| `tolower` | 値を小文字に変換します。 | `count`, `group_by`, `group_by_with_percentage`, `percentile` | +| `toupper` | 値を大文字に変換します。 | `count`, `group_by`, `group_by_with_percentage`, `percentile` | +| `trimspace` | 値の前後の空白を除去します。 | `count`, `group_by`, `group_by_with_percentage`, `percentile` | +| `replace('pattern','repl')` | 正規表現で値を置換します。 | `count`, `group_by`, `group_by_with_percentage`, `percentile` | | `have('key1','key2','key3')` | 集計結果に指定したキーをあらかじめ `0` で追加します。集計した値がない場合でもメトリックを出力できます。 | `group_by`, `group_by_with_percentage` | +| `multiply(factor)` | 数値に指定した係数を掛けます。 | `count`, `group_by`, `group_by_with_percentage`, `percentile` | ### 注意事項 -- 変換関数・初期化関数は `group_by` および `group_by_with_percentage` のみで利用できます。 -- `count` や `percentile` では、変換関数・初期化関数は指定できません。 +- 文字列変換関数(`tolower`、`toupper`、`trimspace`、`replace`)と数値変換関数(`multiply`)は、すべての aggregator で利用できます。 +- ただし、`have()` は `group_by` および `group_by_with_percentage` のみで利用できます。 +- `count` や `percentile` で `have()` は指定できません。 +- `multiply()` は `count` では件数、`group_by`/`group_by_with_percentage` では各値の件数、`percentile` では各統計値に係数を掛けます。 例: ``` diff --git a/cmd/mackerel-plugin-jsonl/jsonkey.go b/cmd/mackerel-plugin-jsonl/jsonkey.go index 999bb67..b06f861 100644 --- a/cmd/mackerel-plugin-jsonl/jsonkey.go +++ b/cmd/mackerel-plugin-jsonl/jsonkey.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "regexp" + "strconv" "strings" "github.com/go-andiamo/splitter" @@ -11,62 +12,72 @@ import ( var ErrEmptyJsonKey = errors.New("json key is empty") -// path.to.key | toupper | tolower | trimspace | replace(regex, repl) -func parseJsonKeyWithFunc(s string) ([]string, []JsonKeyModifier, []JsonKeyInitializer, error) { +// path.to.key | toupper | tolower | trimspace | replace(regex, repl) | have("foo","bar","baz") | multiply(factor) +func parseJsonKeyWithFunc(s string) ([]string, []JsonKeyModifier, []JsonKeyNumericModifier, []JsonKeyInitializer, error) { emptyJsonModifier := []JsonKeyModifier{} + emptyJsonNumericModifier := []JsonKeyNumericModifier{} emptyJsonInitializer := []JsonKeyInitializer{} // , splitter.Parenthesis, splitter.SquareBracket sp, _ := splitter.NewSplitter('|', splitter.DoubleQuotesBackSlashEscaped, splitter.SingleQuotesDoubleEscaped, splitter.Parenthesis, splitter.SquareBrackets) // do not unescapeQuotes. do split more times keys, err := sp.Split(s, splitter.TrimSpaces, splitter.IgnoreEmpties) if err != nil { - return []string{}, emptyJsonModifier, emptyJsonInitializer, err + return []string{}, emptyJsonModifier, emptyJsonNumericModifier, emptyJsonInitializer, err } if len(keys) == 0 { - return []string{}, emptyJsonModifier, emptyJsonInitializer, ErrEmptyJsonKey + return []string{}, emptyJsonModifier, emptyJsonNumericModifier, emptyJsonInitializer, ErrEmptyJsonKey } jsonKey, err := parseJsonKey(keys[0]) if err != nil { - return []string{}, emptyJsonModifier, emptyJsonInitializer, err + return []string{}, emptyJsonModifier, emptyJsonNumericModifier, emptyJsonInitializer, err } modifiers := []JsonKeyModifier{} + numericModifiers := []JsonKeyNumericModifier{} initializers := []JsonKeyInitializer{} for _, fn := range keys[1:] { - mod, init, err := parseModifier(fn) + mod, numMod, init, err := parseModifier(fn) if err != nil { - return []string{}, emptyJsonModifier, emptyJsonInitializer, err + return []string{}, emptyJsonModifier, emptyJsonNumericModifier, emptyJsonInitializer, err } if mod != nil { modifiers = append(modifiers, mod) } + if numMod != nil { + numericModifiers = append(numericModifiers, numMod) + } if init != nil { initializers = append(initializers, init) } } - return jsonKey, modifiers, initializers, nil + return jsonKey, modifiers, numericModifiers, initializers, nil } -func parseModifier(fn string) (JsonKeyModifier, JsonKeyInitializer, error) { +func parseModifier(fn string) (JsonKeyModifier, JsonKeyNumericModifier, JsonKeyInitializer, error) { switch fn { case "tolower": - return func(s string) string { return strings.ToLower(s) }, nil, nil + return func(s string) string { return strings.ToLower(s) }, nil, nil, nil case "toupper": - return func(s string) string { return strings.ToUpper(s) }, nil, nil + return func(s string) string { return strings.ToUpper(s) }, nil, nil, nil case "trimspace": - return func(s string) string { return strings.TrimSpace(s) }, nil, nil + return func(s string) string { return strings.TrimSpace(s) }, nil, nil, nil } if strings.HasPrefix(fn, "replace(") && strings.HasSuffix(fn, ")") { mod, err := parseReplaceModifier(fn) - return mod, nil, err + return mod, nil, nil, err } if strings.HasPrefix(fn, "have(") && strings.HasSuffix(fn, ")") { init, err := parseHaveInitializer(fn) - return nil, init, err + return nil, nil, init, err + } + + if strings.HasPrefix(fn, "multiply(") && strings.HasSuffix(fn, ")") { + mod, err := parseMultiplyModifier(fn) + return nil, mod, nil, err } - return nil, nil, fmt.Errorf("unknown modifier: %s", fn) + return nil, nil, nil, fmt.Errorf("unknown modifier: %s", fn) } func parseReplaceModifier(fn string) (JsonKeyModifier, error) { @@ -106,6 +117,24 @@ func parseHaveInitializer(fn string) (JsonKeyInitializer, error) { }, nil } +func parseMultiplyModifier(fn string) (JsonKeyNumericModifier, error) { + // multiply("1000") + inner := fn[9 : len(fn)-1] + s, _ := splitter.NewSplitter(',', splitter.DoubleQuotesBackSlashEscaped, splitter.SingleQuotesDoubleEscaped) + parts, err := s.Split(inner, splitter.TrimSpaces, splitter.UnescapeQuotes, splitter.IgnoreEmpties) + if err != nil { + return nil, err + } + if len(parts) != 1 { + return nil, fmt.Errorf("invalid multiply() format: %s", fn) + } + factor, err := strconv.ParseFloat(parts[0], 64) + if err != nil { + return nil, fmt.Errorf("invalid multiply factor: %w in %s", err, fn) + } + return func(n float64) float64 { return n * factor }, nil +} + // path.to."foo.baz".[0].key func parseJsonKey(s string) ([]string, error) { sp, _ := splitter.NewSplitter('.', splitter.DoubleQuotesBackSlashEscaped, splitter.SingleQuotesDoubleEscaped, splitter.SquareBrackets) diff --git a/cmd/mackerel-plugin-jsonl/jsonkey_test.go b/cmd/mackerel-plugin-jsonl/jsonkey_test.go index 4ba611d..e598a39 100644 --- a/cmd/mackerel-plugin-jsonl/jsonkey_test.go +++ b/cmd/mackerel-plugin-jsonl/jsonkey_test.go @@ -6,7 +6,7 @@ import ( ) func TestParseJsonKeyWithFunc_basic(t *testing.T) { - key, mods, inits, err := parseJsonKeyWithFunc("foo.bar") + key, mods, nummods, inits, err := parseJsonKeyWithFunc("foo.bar") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -19,10 +19,13 @@ func TestParseJsonKeyWithFunc_basic(t *testing.T) { if len(mods) != 0 { t.Errorf("expected no modifiers, got %d", len(mods)) } + if len(nummods) != 0 { + t.Errorf("expected no numeric modifiers, got %d", len(nummods)) + } } func TestParseJsonKeyWithFunc_modifiers(t *testing.T) { - key, mods, inits, err := parseJsonKeyWithFunc("foo|tolower|toupper|trimspace|have('a','b','c')") + key, mods, nummods, inits, err := parseJsonKeyWithFunc("foo|tolower|toupper|trimspace|have('a','b','c')|multiply(2)") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -35,6 +38,9 @@ func TestParseJsonKeyWithFunc_modifiers(t *testing.T) { if len(mods) != 3 { t.Errorf("expected 3 modifiers, got %d", len(mods)) } + if len(nummods) != 1 { + t.Errorf("expected 1 numeric modifier, got %d", len(nummods)) + } // test modifier chain v := " FOO " for _, m := range mods { @@ -51,11 +57,19 @@ func TestParseJsonKeyWithFunc_modifiers(t *testing.T) { t.Errorf("initializer failed, key %s not found", k) } } + // test numeric modifier + n := 3.0 + for _, m := range nummods { + n = m(n) + } + if n != 6.0 { + t.Errorf("numeric modifier failed, got %v", n) + } } func TestParseJsonKeyWithFunc_replace(t *testing.T) { { - key, mods, inits, err := parseJsonKeyWithFunc("foo|replace('o','a')") + key, mods, nummods, inits, err := parseJsonKeyWithFunc("foo|replace('o','a')") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -68,13 +82,16 @@ func TestParseJsonKeyWithFunc_replace(t *testing.T) { if len(mods) != 1 { t.Errorf("expected 1 modifier, got %d", len(mods)) } + if len(nummods) != 0 { + t.Errorf("expected no numeric modifiers, got %d", len(nummods)) + } v := mods[0]("foo") if v != "faa" { t.Errorf("replace modifier failed, got %v", v) } } { - key, mods, inits, err := parseJsonKeyWithFunc(`foo|replace('[o|f]','a"')`) + key, mods, nummods, inits, err := parseJsonKeyWithFunc(`foo|replace('[o|f]','a"')`) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -87,6 +104,9 @@ func TestParseJsonKeyWithFunc_replace(t *testing.T) { if len(mods) != 1 { t.Errorf("expected 1 modifier, got %d", len(mods)) } + if len(nummods) != 0 { + t.Errorf("expected no numeric modifiers, got %d", len(nummods)) + } v := mods[0]("foo") if v != `a"a"a"` { t.Errorf("replace modifier failed, got %v", v) @@ -95,22 +115,23 @@ func TestParseJsonKeyWithFunc_replace(t *testing.T) { } func TestParseJsonKeyWithFunc_errors(t *testing.T) { - _, _, _, err := parseJsonKeyWithFunc("") - t.Logf("err for empty key: %#v", err) - if err == nil { - t.Error("expected error for empty key") - } - _, _, _, err = parseJsonKeyWithFunc("foo|replace('o')") - if err == nil { - t.Error("expected error for invalid replace format") - } - _, _, _, err = parseJsonKeyWithFunc("foo|replace('(','a')") - if err == nil { - t.Error("expected error for invalid regexp") + testCases := []struct { + input string + }{ + {""}, + {"foo|replace('o')"}, + {"foo|replace('(','a')"}, + {"foo|multiply()"}, + {"foo|multiply(a)"}, + {"foo|multiply(1,2)"}, + {"foo|unknownmod"}, } - _, _, _, err = parseJsonKeyWithFunc("foo|unknownmod") - if err == nil { - t.Error("expected error for unknown modifier") + for _, tc := range testCases { + _, _, _, _, err := parseJsonKeyWithFunc(tc.input) + t.Logf("err for input %q: %#v", tc.input, err) + if err == nil { + t.Errorf("expected error for input %q", tc.input) + } } } @@ -160,7 +181,7 @@ func TestParseJsonKeyWithFunc_cases(t *testing.T) { for _, tc := range testCases { t.Run(tc.input, func(t *testing.T) { - key, funcs, _, err := parseJsonKeyWithFunc(tc.input) + key, funcs, nummods, _, err := parseJsonKeyWithFunc(tc.input) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -170,6 +191,9 @@ func TestParseJsonKeyWithFunc_cases(t *testing.T) { if len(funcs) != tc.funcs { t.Errorf("expected %d modifier, got %d", tc.funcs, len(funcs)) } + if len(nummods) != 0 { + t.Errorf("expected no numeric modifiers, got %d", len(nummods)) + } }) } } diff --git a/cmd/mackerel-plugin-jsonl/reader.go b/cmd/mackerel-plugin-jsonl/reader.go index ddd7471..eb7e08e 100644 --- a/cmd/mackerel-plugin-jsonl/reader.go +++ b/cmd/mackerel-plugin-jsonl/reader.go @@ -10,17 +10,19 @@ import ( ) type JsonKeyModifier func(string) string +type JsonKeyNumericModifier func(float64) float64 type JsonKeyInitializer func(map[string]int) map[string]int type AggregatorFunction struct { - name string - jsonKey []string - JsonKeyModifiers []JsonKeyModifier - JsonKeyInitializers []JsonKeyInitializer - aggregator string - count int - groupBy map[string]int - percentiles *sampdo.Sampdo - percentileTargets []percentileTarget + name string + jsonKey []string + JsonKeyModifiers []JsonKeyModifier + JsonKeyNumericModifiers []JsonKeyNumericModifier + JsonKeyInitializers []JsonKeyInitializer + aggregator string + count int + groupBy map[string]int + percentiles *sampdo.Sampdo + percentileTargets []percentileTarget } func (af *AggregatorFunction) applyModifiers(s string) string { @@ -30,6 +32,13 @@ func (af *AggregatorFunction) applyModifiers(s string) string { return s } +func (af *AggregatorFunction) applyNumericModifiers(n float64) float64 { + for _, mod := range af.JsonKeyNumericModifiers { + n = mod(n) + } + return n +} + func (af *AggregatorFunction) applyInitializers(m map[string]int) map[string]int { for _, init := range af.JsonKeyInitializers { m = init(m) @@ -86,7 +95,7 @@ func (p *Opt) validateOptions() error { } func (p *Opt) buildAggregatorFunction(i int) (*AggregatorFunction, error) { - keys, modifiers, initializers, err := parseJsonKeyWithFunc(p.JsonKeys[i]) + keys, modifiers, numericModifiers, initializers, err := parseJsonKeyWithFunc(p.JsonKeys[i]) if err != nil { return nil, fmt.Errorf("invalid json key: %w", err) } @@ -111,15 +120,16 @@ func (p *Opt) buildAggregatorFunction(i int) (*AggregatorFunction, error) { } return &AggregatorFunction{ - name: p.KeyNames[i], - jsonKey: keys, - JsonKeyModifiers: modifiers, - JsonKeyInitializers: initializers, - aggregator: aggregator, - percentileTargets: targets, - count: 0, - groupBy: map[string]int{}, - percentiles: percentileStore, + name: p.KeyNames[i], + jsonKey: keys, + JsonKeyModifiers: modifiers, + JsonKeyNumericModifiers: numericModifiers, + JsonKeyInitializers: initializers, + aggregator: aggregator, + percentileTargets: targets, + count: 0, + groupBy: map[string]int{}, + percentiles: percentileStore, }, nil } @@ -147,11 +157,11 @@ func (p *Opt) setupPaths() { p.paths = paths } -func (p *Opt) calculatePerDuration(i int) float64 { +func (p *Opt) calculatePerDuration(i float64) float64 { if p.PerSec { - return float64(i) / p.duration + return i / p.duration } - return (float64(i) / p.duration) * 60 + return (i / p.duration) * 60 } func (p *Opt) output() string { @@ -179,7 +189,8 @@ func (p *Opt) writeCountOutput(output *strings.Builder, af *AggregatorFunction, // avoid division by zero return } - fmt.Fprintf(output, "%s.%s\t%f\t%d\n", p.Prefix, af.name, p.calculatePerDuration(af.count), now) + count := af.applyNumericModifiers(float64(af.count)) + fmt.Fprintf(output, "%s.%s\t%f\t%d\n", p.Prefix, af.name, p.calculatePerDuration(count), now) } func (p *Opt) writeGroupByOutput(output *strings.Builder, af *AggregatorFunction, now uint64) { @@ -191,7 +202,8 @@ func (p *Opt) writeGroupByOutput(output *strings.Builder, af *AggregatorFunction total := 0 for k, v := range af.groupBy { safeKey := sanitizeMetricKey(k) - fmt.Fprintf(output, "%s.%s.%s\t%f\t%d\n", p.Prefix, af.name, safeKey, p.calculatePerDuration(v), now) + f := af.applyNumericModifiers(float64(v)) + fmt.Fprintf(output, "%s.%s.%s\t%f\t%d\n", p.Prefix, af.name, safeKey, p.calculatePerDuration(f), now) total += v } if af.aggregator == "group_by_with_percentage" && total > 0 { @@ -231,6 +243,7 @@ func (p *Opt) writePercentileOutput(output *strings.Builder, af *AggregatorFunct if err != nil { continue } + value = af.applyNumericModifiers(value) fmt.Fprintf(output, "%s.%s.%s\t%f\t%d\n", p.Prefix, af.name, target.name, value, now) } } From 6cb309a4e03bcf746dcecf00dd4dfc75055f0983 Mon Sep 17 00:00:00 2001 From: Masahiro Nagano Date: Sun, 11 Oct 2026 12:25:48 +0900 Subject: [PATCH 2/5] Update cmd/mackerel-plugin-jsonl/jsonkey.go Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- cmd/mackerel-plugin-jsonl/jsonkey.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cmd/mackerel-plugin-jsonl/jsonkey.go b/cmd/mackerel-plugin-jsonl/jsonkey.go index b06f861..a533628 100644 --- a/cmd/mackerel-plugin-jsonl/jsonkey.go +++ b/cmd/mackerel-plugin-jsonl/jsonkey.go @@ -132,6 +132,9 @@ func parseMultiplyModifier(fn string) (JsonKeyNumericModifier, error) { if err != nil { return nil, fmt.Errorf("invalid multiply factor: %w in %s", err, fn) } + if factor != factor || factor-factor != 0 { + return nil, fmt.Errorf("multiply factor must be finite: %s", fn) + } return func(n float64) float64 { return n * factor }, nil } From 5847c9769259c805f2c8539f99784fef0ef3e1bd Mon Sep 17 00:00:00 2001 From: Masahiro Nagano Date: Sun, 11 Oct 2026 13:24:17 +0900 Subject: [PATCH 3/5] fix: validate multiply factor to ensure it is finite and not NaN --- cmd/mackerel-plugin-jsonl/jsonkey.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cmd/mackerel-plugin-jsonl/jsonkey.go b/cmd/mackerel-plugin-jsonl/jsonkey.go index a533628..ddd56d4 100644 --- a/cmd/mackerel-plugin-jsonl/jsonkey.go +++ b/cmd/mackerel-plugin-jsonl/jsonkey.go @@ -3,6 +3,7 @@ package main import ( "errors" "fmt" + "math" "regexp" "strconv" "strings" @@ -132,7 +133,8 @@ func parseMultiplyModifier(fn string) (JsonKeyNumericModifier, error) { if err != nil { return nil, fmt.Errorf("invalid multiply factor: %w in %s", err, fn) } - if factor != factor || factor-factor != 0 { + // check if factor is finite or not NaN + if math.IsNaN(factor) || math.IsInf(factor, 0) { return nil, fmt.Errorf("multiply factor must be finite: %s", fn) } return func(n float64) float64 { return n * factor }, nil From 389e3073ef2dc491a231db42713f842a7f594b19 Mon Sep 17 00:00:00 2001 From: Masahiro Nagano Date: Sun, 11 Oct 2026 13:31:57 +0900 Subject: [PATCH 4/5] test: refactor test cases to use helper function for parsing JSON keys --- cmd/mackerel-plugin-jsonl/jsonkey_test.go | 79 ++++++----------------- 1 file changed, 20 insertions(+), 59 deletions(-) diff --git a/cmd/mackerel-plugin-jsonl/jsonkey_test.go b/cmd/mackerel-plugin-jsonl/jsonkey_test.go index e598a39..ecd7120 100644 --- a/cmd/mackerel-plugin-jsonl/jsonkey_test.go +++ b/cmd/mackerel-plugin-jsonl/jsonkey_test.go @@ -5,42 +5,36 @@ import ( "testing" ) -func TestParseJsonKeyWithFunc_basic(t *testing.T) { - key, mods, nummods, inits, err := parseJsonKeyWithFunc("foo.bar") +func parseJsonKeyWithFuncOrFail(t *testing.T, s string, initLen, modsLen, nummodsLen int) ([]string, []JsonKeyModifier, []JsonKeyNumericModifier, []JsonKeyInitializer) { + key, mods, nummods, inits, err := parseJsonKeyWithFunc(s) if err != nil { t.Fatalf("unexpected error: %v", err) } - if !reflect.DeepEqual(key, []string{"foo", "bar"}) { - t.Errorf("expected [foo bar], got %v", key) + if len(inits) != initLen { + t.Fatalf("expected %d initializers, got %d", initLen, len(inits)) } - if len(inits) != 0 { - t.Errorf("expected no initializers, got %d", len(inits)) + if len(mods) != modsLen { + t.Fatalf("expected %d modifiers, got %d", modsLen, len(mods)) } - if len(mods) != 0 { - t.Errorf("expected no modifiers, got %d", len(mods)) + if len(nummods) != nummodsLen { + t.Fatalf("expected %d numeric modifiers, got %d", nummodsLen, len(nummods)) } - if len(nummods) != 0 { - t.Errorf("expected no numeric modifiers, got %d", len(nummods)) + return key, mods, nummods, inits +} + +func TestParseJsonKeyWithFunc_basic(t *testing.T) { + key, _, _, _ := parseJsonKeyWithFuncOrFail(t, "foo.bar", 0, 0, 0) + if !reflect.DeepEqual(key, []string{"foo", "bar"}) { + t.Errorf("expected [foo bar], got %v", key) } } func TestParseJsonKeyWithFunc_modifiers(t *testing.T) { - key, mods, nummods, inits, err := parseJsonKeyWithFunc("foo|tolower|toupper|trimspace|have('a','b','c')|multiply(2)") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } + key, mods, nummods, inits := parseJsonKeyWithFuncOrFail(t, "foo|tolower|toupper|trimspace|have('a','b','c')|multiply(2)", 1, 3, 1) if key[0] != "foo" { t.Errorf("expected key 'foo', got %v", key) } - if len(inits) != 1 { - t.Errorf("expected 1 initializer, got %d", len(inits)) - } - if len(mods) != 3 { - t.Errorf("expected 3 modifiers, got %d", len(mods)) - } - if len(nummods) != 1 { - t.Errorf("expected 1 numeric modifier, got %d", len(nummods)) - } + // test modifier chain v := " FOO " for _, m := range mods { @@ -69,44 +63,20 @@ func TestParseJsonKeyWithFunc_modifiers(t *testing.T) { func TestParseJsonKeyWithFunc_replace(t *testing.T) { { - key, mods, nummods, inits, err := parseJsonKeyWithFunc("foo|replace('o','a')") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } + key, mods, _, _ := parseJsonKeyWithFuncOrFail(t, "foo|replace('o','a')", 0, 1, 0) if key[0] != "foo" { t.Errorf("expected key 'foo', got %v", key) } - if len(inits) != 0 { - t.Errorf("expected no initializers, got %d", len(inits)) - } - if len(mods) != 1 { - t.Errorf("expected 1 modifier, got %d", len(mods)) - } - if len(nummods) != 0 { - t.Errorf("expected no numeric modifiers, got %d", len(nummods)) - } v := mods[0]("foo") if v != "faa" { t.Errorf("replace modifier failed, got %v", v) } } { - key, mods, nummods, inits, err := parseJsonKeyWithFunc(`foo|replace('[o|f]','a"')`) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } + key, mods, _, _ := parseJsonKeyWithFuncOrFail(t, `foo|replace('[o|f]','a"')`, 0, 1, 0) if key[0] != "foo" { t.Errorf("expected key 'foo', got %v", key) } - if len(inits) != 0 { - t.Errorf("expected no initializers, got %d", len(inits)) - } - if len(mods) != 1 { - t.Errorf("expected 1 modifier, got %d", len(mods)) - } - if len(nummods) != 0 { - t.Errorf("expected no numeric modifiers, got %d", len(nummods)) - } v := mods[0]("foo") if v != `a"a"a"` { t.Errorf("replace modifier failed, got %v", v) @@ -181,19 +151,10 @@ func TestParseJsonKeyWithFunc_cases(t *testing.T) { for _, tc := range testCases { t.Run(tc.input, func(t *testing.T) { - key, funcs, nummods, _, err := parseJsonKeyWithFunc(tc.input) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } + key, _, _, _ := parseJsonKeyWithFuncOrFail(t, tc.input, 0, tc.funcs, 0) if !reflect.DeepEqual(key, tc.expected) { t.Errorf("expected %v, got %v", tc.expected, key) } - if len(funcs) != tc.funcs { - t.Errorf("expected %d modifier, got %d", tc.funcs, len(funcs)) - } - if len(nummods) != 0 { - t.Errorf("expected no numeric modifiers, got %d", len(nummods)) - } }) } } From 4c9fb5f5eb57394910cae3c9e917da2c6dfe85a0 Mon Sep 17 00:00:00 2001 From: Masahiro Nagano Date: Sun, 11 Oct 2026 13:33:17 +0900 Subject: [PATCH 5/5] Update README.md Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 85bed88..b1961a5 100644 --- a/README.md +++ b/README.md @@ -98,10 +98,10 @@ command = "/path/to/mackerel-plugin-jsonl --key-name count --json-key foo.bar -- | 関数 | 説明 | 利用できる aggregator | | --- | --- | --- | -| `tolower` | 値を小文字に変換します。 | `count`, `group_by`, `group_by_with_percentage`, `percentile` | -| `toupper` | 値を大文字に変換します。 | `count`, `group_by`, `group_by_with_percentage`, `percentile` | -| `trimspace` | 値の前後の空白を除去します。 | `count`, `group_by`, `group_by_with_percentage`, `percentile` | -| `replace('pattern','repl')` | 正規表現で値を置換します。 | `count`, `group_by`, `group_by_with_percentage`, `percentile` | +| `tolower` | 値を小文字に変換します。 | `group_by`, `group_by_with_percentage` | +| `toupper` | 値を大文字に変換します。 | `group_by`, `group_by_with_percentage` | +| `trimspace` | 値の前後の空白を除去します。 | `group_by`, `group_by_with_percentage` | +| `replace('pattern','repl')` | 正規表現で値を置換します。 | `group_by`, `group_by_with_percentage` | | `have('key1','key2','key3')` | 集計結果に指定したキーをあらかじめ `0` で追加します。集計した値がない場合でもメトリックを出力できます。 | `group_by`, `group_by_with_percentage` | | `multiply(factor)` | 数値に指定した係数を掛けます。 | `count`, `group_by`, `group_by_with_percentage`, `percentile` |