Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,14 @@ command = "/path/to/mackerel-plugin-jsonl --key-name count --json-key foo.bar --
| `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` |

### 注意事項

- 変換関数・初期化関数は `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` では各統計値に係数を掛けます。

例:
```
Expand Down
64 changes: 49 additions & 15 deletions cmd/mackerel-plugin-jsonl/jsonkey.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,70 +3,82 @@ package main
import (
"errors"
"fmt"
"math"
"regexp"
"strconv"
"strings"

"github.com/go-andiamo/splitter"
)

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) {
Expand Down Expand Up @@ -106,6 +118,28 @@ 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)
}
Comment thread
kazeburo marked this conversation as resolved.
// 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
}

// path.to."foo.baz".[0].key
func parseJsonKey(s string) ([]string, error) {
sp, _ := splitter.NewSplitter('.', splitter.DoubleQuotesBackSlashEscaped, splitter.SingleQuotesDoubleEscaped, splitter.SquareBrackets)
Expand Down
105 changes: 45 additions & 60 deletions cmd/mackerel-plugin-jsonl/jsonkey_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,36 +5,36 @@ import (
"testing"
)

func TestParseJsonKeyWithFunc_basic(t *testing.T) {
key, mods, 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))
}
return key, mods, nummods, inits
}

func TestParseJsonKeyWithFunc_modifiers(t *testing.T) {
key, mods, inits, err := parseJsonKeyWithFunc("foo|tolower|toupper|trimspace|have('a','b','c')")
if err != nil {
t.Fatalf("unexpected error: %v", err)
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 := 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))
}

// test modifier chain
v := " FOO "
for _, m := range mods {
Expand All @@ -51,42 +51,32 @@ 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')")
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))
}
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"')`)
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))
}
v := mods[0]("foo")
if v != `a"a"a"` {
t.Errorf("replace modifier failed, got %v", v)
Expand All @@ -95,22 +85,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)
}
}
}

Expand Down Expand Up @@ -160,16 +151,10 @@ func TestParseJsonKeyWithFunc_cases(t *testing.T) {

for _, tc := range testCases {
t.Run(tc.input, func(t *testing.T) {
key, funcs, _, 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))
}
})
}
}
Loading
Loading