diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ac203f3..88a74ad7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## Next release * [CHANGE] Revert Alertmanager working directory changes in Docker image back to `/alertmanager` (#1435) +* [FEATURE] [amtool] Added `config routes` tools for vizualization and testing routes (#1511) ## 0.15.2 / 2018-08-14 diff --git a/README.md b/README.md index 00bed330..06de3c21 100644 --- a/README.md +++ b/README.md @@ -286,6 +286,22 @@ output: extended receiver: team-X-pager ``` +### Routes + +Amtool allows you to vizualize the routes of your configuration in form of text tree view. +Also you can use it to test the routing by passing it label set of an alert +and it prints out all receivers the alert would match ordered and separated by `,`. +(If you use `--verify.receivers` amtool returns error code 1 on mismatch) + +Example of usage: +``` +# View routing tree of remote Alertmanager +amtool config routes --alertmanager.url=http://localhost:9090 + +# Test if alert matches expected receiver +./amtool config routes test --config.file=doc/examples/simple.yml --tree --verify.receivers=team-X-pager service=database owner=team-X +``` + ## High Availability > Warning: High Availability is under active development diff --git a/cli/config.go b/cli/config.go index 1734d16b..81381ce2 100644 --- a/cli/config.go +++ b/cli/config.go @@ -17,11 +17,9 @@ import ( "context" "errors" - "github.com/prometheus/client_golang/api" "gopkg.in/alecthomas/kingpin.v2" "github.com/prometheus/alertmanager/cli/format" - "github.com/prometheus/alertmanager/client" ) const configHelp = `View current config. @@ -34,17 +32,13 @@ The amount of output is controlled by the output selection flag: // configCmd represents the config command func configureConfigCmd(app *kingpin.Application) { - app.Command("config", configHelp).Action(execWithTimeout(queryConfig)).PreAction(requireAlertManagerURL) - + configCmd := app.Command("config", configHelp) + configCmd.Command("show", configHelp).Default().Action(execWithTimeout(queryConfig)).PreAction(requireAlertManagerURL) + configureRoutingCmd(configCmd) } func queryConfig(ctx context.Context, _ *kingpin.ParseContext) error { - c, err := api.NewClient(api.Config{Address: alertmanagerURL.String()}) - if err != nil { - return err - } - statusAPI := client.NewStatusAPI(c) - status, err := statusAPI.Get(ctx) + status, err := getRemoteAlertmanagerConfigStatus(ctx, alertmanagerURL) if err != nil { return err } diff --git a/cli/routing.go b/cli/routing.go new file mode 100644 index 00000000..eaafbe6a --- /dev/null +++ b/cli/routing.go @@ -0,0 +1,120 @@ +// Copyright 2018 Prometheus Team +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cli + +import ( + "bytes" + "context" + "fmt" + + "github.com/xlab/treeprint" + + "github.com/prometheus/alertmanager/client" + "github.com/prometheus/alertmanager/dispatch" + "gopkg.in/alecthomas/kingpin.v2" +) + +type routingShow struct { + configFile string + labels []string + expectedReceivers string + tree treeprint.Tree + debugTree bool +} + +const ( + routingHelp = `Prints alert routing tree + +Will print whole routing tree in form of ASCII tree view. + +Routing is loaded from a local configuration file or a running Alertmanager configuration. +Specifying --config.file takes precedence over --alertmanager.url. + +Example: + +./amtool config routes [show] --config.file=doc/examples/simple.yml + +` + branchSlugSeparator = " " +) + +func configureRoutingCmd(app *kingpin.CmdClause) { + var ( + c = &routingShow{} + routingCmd = app.Command("routes", routingHelp) + routingShowCmd = routingCmd.Command("show", routingHelp).Default() + configFlag = routingCmd.Flag("config.file", "Config file to be tested.") + ) + configFlag.ExistingFileVar(&c.configFile) + routingShowCmd.Action(execWithTimeout(c.routingShowAction)) + configureRoutingTestCmd(routingCmd, c) +} + +func (c *routingShow) routingShowAction(ctx context.Context, _ *kingpin.ParseContext) error { + // Load configuration form file or URL. + cfg, err := loadAlertmanagerConfig(ctx, alertmanagerURL, c.configFile) + if err != nil { + kingpin.Fatalf("%s", err) + return err + } + route := dispatch.NewRoute(cfg.Route, nil) + tree := treeprint.New() + convertRouteToTree(route, tree) + fmt.Println("Routing tree:") + fmt.Println(tree.String()) + return nil +} + +func getRouteTreeSlug(route *dispatch.Route, showContinue bool, showReceiver bool) string { + var branchSlug bytes.Buffer + if route.Matchers.Len() == 0 { + branchSlug.WriteString("default-route") + } else { + branchSlug.WriteString(route.Matchers.String()) + } + if route.Continue && showContinue { + branchSlug.WriteString(branchSlugSeparator) + branchSlug.WriteString("continue: true") + } + if showReceiver { + branchSlug.WriteString(branchSlugSeparator) + branchSlug.WriteString("receiver: ") + branchSlug.WriteString(route.RouteOpts.Receiver) + } + return branchSlug.String() +} + +func convertRouteToTree(route *dispatch.Route, tree treeprint.Tree) { + branch := tree.AddBranch(getRouteTreeSlug(route, true, true)) + for _, r := range route.Routes { + convertRouteToTree(r, branch) + } +} + +func getMatchingTree(route *dispatch.Route, tree treeprint.Tree, lset client.LabelSet) { + final := true + branch := tree.AddBranch(getRouteTreeSlug(route, false, false)) + for _, r := range route.Routes { + if r.Matchers.Match(convertClientToCommonLabelSet(lset)) == true { + getMatchingTree(r, branch, lset) + final = false + if r.Continue != true { + break + } + } + } + if final == true { + branch.SetValue(getRouteTreeSlug(route, false, true)) + } +} diff --git a/cli/test_routing.go b/cli/test_routing.go new file mode 100644 index 00000000..2c68bad8 --- /dev/null +++ b/cli/test_routing.go @@ -0,0 +1,101 @@ +// Copyright 2018 Prometheus Team +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cli + +import ( + "context" + "fmt" + "os" + "strings" + + "github.com/prometheus/alertmanager/client" + "github.com/prometheus/alertmanager/dispatch" + "github.com/xlab/treeprint" + "gopkg.in/alecthomas/kingpin.v2" +) + +const routingTestHelp = `Test alert routing + +Will return receiver names which the alert with given labels resolves to. +If the labelset resolves to multiple receivers, they are printed out in order as defined in the routing tree. + +Routing is loaded from a local configuration file or a running Alertmanager configuration. +Specifying --config.file takes precedence over --alertmanager.url. + +Example: + +./amtool config routes test --config.file=doc/examples/simple.yml --verify.receivers=team-DB-pager service=database + +` + +func configureRoutingTestCmd(cc *kingpin.CmdClause, c *routingShow) { + var routingTestCmd = cc.Command("test", routingTestHelp) + + routingTestCmd.Flag("verify.receivers", "Checks if specified receivers matches resolved receivers. The command fails if the labelset does not route to the specified receivers.").StringVar(&c.expectedReceivers) + routingTestCmd.Flag("tree", "Prints out matching routes tree.").BoolVar(&c.debugTree) + routingTestCmd.Arg("labels", "List of labels to be tested against the configured routes.").StringsVar(&c.labels) + routingTestCmd.Action(execWithTimeout(c.routingTestAction)) +} + +// resolveAlertReceivers returns list of receiver names which given LabelSet resolves to. +func resolveAlertReceivers(mainRoute *dispatch.Route, labels *client.LabelSet) ([]string, error) { + var ( + finalRoutes []*dispatch.Route + receivers []string + ) + finalRoutes = mainRoute.Match(convertClientToCommonLabelSet(*labels)) + for _, r := range finalRoutes { + receivers = append(receivers, r.RouteOpts.Receiver) + } + return receivers, nil +} + +func printMatchingTree(mainRoute *dispatch.Route, ls client.LabelSet) { + tree := treeprint.New() + getMatchingTree(mainRoute, tree, ls) + fmt.Println("Matching routes:") + fmt.Println(tree.String()) + fmt.Print("\n") +} + +func (c *routingShow) routingTestAction(ctx context.Context, _ *kingpin.ParseContext) error { + cfg, err := loadAlertmanagerConfig(ctx, alertmanagerURL, c.configFile) + if err != nil { + kingpin.Fatalf("%v\n", err) + return err + } + + mainRoute := dispatch.NewRoute(cfg.Route, nil) + + // Parse lables to LabelSet. + ls, err := parseLabels(c.labels) + if err != nil { + kingpin.Fatalf("Failed to parse labels: %v\n", err) + } + + if c.debugTree == true { + printMatchingTree(mainRoute, ls) + } + + receivers, err := resolveAlertReceivers(mainRoute, &ls) + receiversSlug := strings.Join(receivers, ",") + fmt.Printf("%s\n", receiversSlug) + + if c.expectedReceivers != "" && c.expectedReceivers != receiversSlug { + fmt.Printf("WARNING: Expected receivers did not match resolved receivers.\n") + os.Exit(1) + } + + return err +} diff --git a/cli/test_routing_test.go b/cli/test_routing_test.go new file mode 100644 index 00000000..4046ef47 --- /dev/null +++ b/cli/test_routing_test.go @@ -0,0 +1,64 @@ +// Copyright 2018 Prometheus Team +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cli + +import ( + "fmt" + "reflect" + "strings" + "testing" + + "github.com/prometheus/alertmanager/client" + "github.com/prometheus/alertmanager/config" + "github.com/prometheus/alertmanager/dispatch" +) + +type routingTestDefinition struct { + alert client.LabelSet + expectedReceivers []string + configFile string +} + +func checkResolvedReceivers(mainRoute *dispatch.Route, ls client.LabelSet, expectedReceivers []string) error { + resolvedReceivers, err := resolveAlertReceivers(mainRoute, &ls) + if err != nil { + return err + } + if !reflect.DeepEqual(expectedReceivers, resolvedReceivers) { + return fmt.Errorf("Unexpected routing result want: `%s`, got: `%s`", strings.Join(expectedReceivers, ","), strings.Join(resolvedReceivers, ",")) + } + return nil +} + +func TestRoutingTest(t *testing.T) { + tests := []*routingTestDefinition{ + &routingTestDefinition{configFile: "testdata/conf.routing.yml", alert: client.LabelSet{"test": "1"}, expectedReceivers: []string{"test1"}}, + &routingTestDefinition{configFile: "testdata/conf.routing.yml", alert: client.LabelSet{"test": "2"}, expectedReceivers: []string{"test1", "test2"}}, + &routingTestDefinition{configFile: "testdata/conf.routing-reverted.yml", alert: client.LabelSet{"test": "2"}, expectedReceivers: []string{"test2", "test1"}}, + &routingTestDefinition{configFile: "testdata/conf.routing.yml", alert: client.LabelSet{"test": "volovina"}, expectedReceivers: []string{"default"}}, + } + + for _, test := range tests { + cfg, _, err := config.LoadFile(test.configFile) + if err != nil { + t.Fatalf("failed to load test configuration: %v", err) + } + mainRoute := dispatch.NewRoute(cfg.Route, nil) + err = checkResolvedReceivers(mainRoute, test.alert, test.expectedReceivers) + if err != nil { + t.Fatalf("%v", err) + } + fmt.Println(" OK") + } +} diff --git a/cli/testdata/conf.routing-reverted.yml b/cli/testdata/conf.routing-reverted.yml new file mode 100644 index 00000000..46132723 --- /dev/null +++ b/cli/testdata/conf.routing-reverted.yml @@ -0,0 +1,22 @@ +global: + smtp_smarthost: 'localhost:25' + +templates: + - '/etc/alertmanager/template/*.tmpl' + +route: + receiver: default + routes: + - match: + test: 2 + receiver: test2 + continue: true + - match_re: + test: ^[12]$ + receiver: test1 + continue: true + +receivers: + - name: default + - name: test1 + - name: test2 diff --git a/cli/testdata/conf.routing.yml b/cli/testdata/conf.routing.yml new file mode 100644 index 00000000..8e32b281 --- /dev/null +++ b/cli/testdata/conf.routing.yml @@ -0,0 +1,21 @@ +global: + smtp_smarthost: 'localhost:25' + +templates: + - '/etc/alertmanager/template/*.tmpl' + +route: + receiver: default + routes: + - match_re: + test: ^[12]$ + receiver: test1 + continue: true + - match: + test: 2 + receiver: test2 + +receivers: + - name: default + - name: test1 + - name: test2 diff --git a/cli/utils.go b/cli/utils.go index 0bde3b31..66b481ed 100644 --- a/cli/utils.go +++ b/cli/utils.go @@ -18,9 +18,12 @@ import ( "errors" "fmt" "net/url" + "os" "path" "github.com/prometheus/alertmanager/client" + amconfig "github.com/prometheus/alertmanager/config" + "github.com/prometheus/client_golang/api" kingpin "gopkg.in/alecthomas/kingpin.v2" "github.com/prometheus/alertmanager/pkg/parse" @@ -70,6 +73,57 @@ func parseMatchers(inputMatchers []string) ([]labels.Matcher, error) { return matchers, nil } +// getRemoteAlertmanagerConfigStatus returns status responsecontaining configuration from remote Alertmanager +func getRemoteAlertmanagerConfigStatus(ctx context.Context, alertmanagerURL *url.URL) (*client.ServerStatus, error) { + c, err := api.NewClient(api.Config{Address: alertmanagerURL.String()}) + if err != nil { + return nil, err + } + statusAPI := client.NewStatusAPI(c) + status, err := statusAPI.Get(ctx) + if err != nil { + return nil, err + } + return status, nil +} + +func checkRoutingConfigInputFlags(alertmanagerURL *url.URL, configFile string) { + if alertmanagerURL != nil && configFile != "" { + fmt.Fprintln(os.Stderr, "Warning: --config.file flag overrides the --alertmanager.url.") + } + if alertmanagerURL == nil && configFile == "" { + kingpin.Fatalf("You have to specify one of --config.file or --alertmanager.url flags.") + } +} + +func loadAlertmanagerConfig(ctx context.Context, alertmanagerURL *url.URL, configFile string) (*amconfig.Config, error) { + checkRoutingConfigInputFlags(alertmanagerURL, configFile) + if configFile != "" { + cfg, _, err := amconfig.LoadFile(configFile) + if err != nil { + return nil, err + } + return cfg, nil + } + if alertmanagerURL != nil { + status, err := getRemoteAlertmanagerConfigStatus(ctx, alertmanagerURL) + if err != nil { + return nil, err + } + return status.ConfigJSON, nil + } + return nil, errors.New("Failed to get Alertmanager configuration.") +} + +// convertClientToCommonLabelSet converts client.LabelSet to model.Labelset +func convertClientToCommonLabelSet(cls client.LabelSet) model.LabelSet { + mls := make(model.LabelSet, len(cls)) + for ln, lv := range cls { + mls[model.LabelName(ln)] = model.LabelValue(lv) + } + return mls +} + // Parse a list of labels (cli arguments) func parseLabels(inputLabels []string) (client.LabelSet, error) { labelSet := make(client.LabelSet, len(inputLabels)) diff --git a/doc/examples/simple.yml b/doc/examples/simple.yml index caf5d1b3..3a2165ca 100644 --- a/doc/examples/simple.yml +++ b/doc/examples/simple.yml @@ -75,6 +75,7 @@ route: - match: owner: team-X receiver: team-X-pager + continue: true - match: owner: team-Y receiver: team-Y-pager diff --git a/vendor/github.com/xlab/treeprint/LICENSE b/vendor/github.com/xlab/treeprint/LICENSE new file mode 100644 index 00000000..5ab533ad --- /dev/null +++ b/vendor/github.com/xlab/treeprint/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) +Copyright © 2016 Maxim Kupriianov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/github.com/xlab/treeprint/README.md b/vendor/github.com/xlab/treeprint/README.md new file mode 100644 index 00000000..6f162829 --- /dev/null +++ b/vendor/github.com/xlab/treeprint/README.md @@ -0,0 +1,128 @@ +treeprint [![GoDoc](https://godoc.org/github.com/xlab/treeprint?status.svg)](https://godoc.org/github.com/xlab/treeprint) ![test coverage](https://img.shields.io/badge/coverage-68.6%25-green.svg) +========= + +Package `treeprint` provides a simple ASCII tree composing tool. + +SYSTEME FIGURE + +If you are familiar with the [tree](http://mama.indstate.edu/users/ice/tree/) utility that is a recursive directory listing command that produces a depth indented listing of files, then you have the idea of what it would look like. + +On my system the command yields the following + +``` + $ tree +. +├── LICENSE +├── README.md +├── treeprint.go +└── treeprint_test.go + +0 directories, 4 files +``` + +and I'd like to have the same format for my Go data structures when I print them. + +## Installation + +``` +$ go get github.com/xlab/treeprint +``` + +## Concept of work + +The general idea is that you initialise a new tree with `treeprint.New()` and then add nodes and +branches into it. Use `AddNode()` when you want add a node on the same level as the target or +use `AddBranch()` when you want to go a level deeper. So `tree.AddBranch().AddNode().AddNode()` would +create a new level with two distinct nodes on it. So `tree.AddNode().AddNode()` is a flat thing and +`tree.AddBranch().AddBranch().AddBranch()` is a high thing. Use `String()` or `Bytes()` on a branch +to render a subtree, or use it on the root to print the whole tree. + +The utility will yield Unicode-friendly trees. The output is predictable and there is no platform-dependent exceptions, so if you have issues with displaying the tree in the console, all platform-related transformations can be done after the tree has been rendered: [an example](https://github.com/xlab/treeprint/issues/2#issuecomment-324944141) for Asian locales. + +## Use cases + +When you want to render a complex data structure: + +```go +func main() { + tree := treeprint.New() + + // create a new branch in the root + one := tree.AddBranch("one") + + // add some nodes + one.AddNode("subnode1").AddNode("subnode2") + + // create a new sub-branch + one.AddBranch("two"). + AddNode("subnode1").AddNode("subnode2"). // add some nodes + AddBranch("three"). // add a new sub-branch + AddNode("subnode1").AddNode("subnode2") // add some nodes too + + // add one more node that should surround the inner branch + one.AddNode("subnode3") + + // add a new node to the root + tree.AddNode("outernode") + + fmt.Println(tree.String()) +} +``` + +Will give you: + +``` +. +├── one +│   ├── subnode1 +│   ├── subnode2 +│   ├── two +│   │   ├── subnode1 +│   │   ├── subnode2 +│   │   └── three +│   │   ├── subnode1 +│   │   └── subnode2 +│   └── subnode3 +└── outernode +``` + +Another case, when you have to make a tree where any leaf may have some meta-data (as `tree` is capable of it): + +```go +func main { + tree := treeprint.New() + + tree.AddNode("Dockerfile") + tree.AddNode("Makefile") + tree.AddNode("aws.sh") + tree.AddMetaBranch(" 204", "bin"). + AddNode("dbmaker").AddNode("someserver").AddNode("testtool") + tree.AddMetaBranch(" 374", "deploy"). + AddNode("Makefile").AddNode("bootstrap.sh") + tree.AddMetaNode("122K", "testtool.a") + + fmt.Println(tree.String()) +} +``` + +Output: + +``` +. +├── Dockerfile +├── Makefile +├── aws.sh +├── [ 204] bin +│   ├── dbmaker +│   ├── someserver +│   └── testtool +├── [ 374] deploy +│   ├── Makefile +│   └── bootstrap.sh +└── [122K] testtool.a +``` + +Yay! So it works. + +## License +MIT diff --git a/vendor/github.com/xlab/treeprint/helpers.go b/vendor/github.com/xlab/treeprint/helpers.go new file mode 100644 index 00000000..a091a5a0 --- /dev/null +++ b/vendor/github.com/xlab/treeprint/helpers.go @@ -0,0 +1,47 @@ +package treeprint + +import ( + "reflect" + "strings" +) + +func isEmpty(v *reflect.Value) bool { + switch v.Kind() { + case reflect.Array, reflect.Map, reflect.Slice, reflect.String: + return v.Len() == 0 + case reflect.Bool: + return !v.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return v.Uint() == 0 + case reflect.Float32, reflect.Float64: + return v.Float() == 0 + case reflect.Interface, reflect.Ptr: + return v.IsNil() + } + return false +} + +func tagSpec(tag string) (name string, omit bool) { + parts := strings.Split(tag, ",") + if len(parts) < 2 { + return tag, false + } + if parts[1] == "omitempty" { + return parts[0], true + } + return parts[0], false +} + +func filterTags(tag reflect.StructTag) string { + tags := strings.Split(string(tag), " ") + filtered := make([]string, 0, len(tags)) + for i := range tags { + if strings.HasPrefix(tags[i], "tree:") { + continue + } + filtered = append(filtered, tags[i]) + } + return strings.Join(filtered, " ") +} diff --git a/vendor/github.com/xlab/treeprint/struct.go b/vendor/github.com/xlab/treeprint/struct.go new file mode 100644 index 00000000..4d5cc825 --- /dev/null +++ b/vendor/github.com/xlab/treeprint/struct.go @@ -0,0 +1,322 @@ +package treeprint + +import ( + "fmt" + "reflect" + "strings" +) + +type StructTreeOption int + +const ( + StructNameTree StructTreeOption = iota + StructValueTree + StructTagTree + StructTypeTree + StructTypeSizeTree +) + +func FromStruct(v interface{}, opt ...StructTreeOption) (Tree, error) { + var treeOpt StructTreeOption + if len(opt) > 0 { + treeOpt = opt[0] + } + switch treeOpt { + case StructNameTree: + tree := New() + err := nameTree(tree, v) + return tree, err + case StructValueTree: + tree := New() + err := valueTree(tree, v) + return tree, err + case StructTagTree: + tree := New() + err := tagTree(tree, v) + return tree, err + case StructTypeTree: + tree := New() + err := typeTree(tree, v) + return tree, err + case StructTypeSizeTree: + tree := New() + err := typeSizeTree(tree, v) + return tree, err + default: + err := fmt.Errorf("treeprint: invalid StructTreeOption %v", treeOpt) + return nil, err + } +} + +type FmtFunc func(name string, v interface{}) (string, bool) + +func FromStructWithMeta(v interface{}, fmtFunc FmtFunc) (Tree, error) { + if fmtFunc == nil { + tree := New() + err := nameTree(tree, v) + return tree, err + } + tree := New() + err := metaTree(tree, v, fmtFunc) + return tree, err +} + +func Repr(v interface{}) string { + tree := New() + vType := reflect.TypeOf(v) + vValue := reflect.ValueOf(v) + _, val, isStruct := getValue(vType, &vValue) + if !isStruct { + return fmt.Sprintf("%+v", val.Interface()) + } + err := valueTree(tree, val.Interface()) + if err != nil { + return err.Error() + } + return tree.String() +} + +func nameTree(tree Tree, v interface{}) error { + typ, val, err := checkType(v) + if err != nil { + return err + } + fields := typ.NumField() + for i := 0; i < fields; i++ { + field := typ.Field(i) + fieldValue := val.Field(i) + name, skip, omit := getMeta(field.Name, field.Tag) + if skip || omit && isEmpty(&fieldValue) { + continue + } + typ, val, isStruct := getValue(field.Type, &fieldValue) + if !isStruct { + tree.AddNode(name) + continue + } else if subNum := typ.NumField(); subNum == 0 { + tree.AddNode(name) + continue + } + branch := tree.AddBranch(name) + if err := nameTree(branch, val.Interface()); err != nil { + err := fmt.Errorf("%v on struct branch %s", err, name) + return err + } + } + return nil +} + +func getMeta(fieldName string, tag reflect.StructTag) (name string, skip, omit bool) { + if tagStr := tag.Get("tree"); len(tagStr) > 0 { + name, omit = tagSpec(tagStr) + } + if name == "-" { + return fieldName, true, omit + } + if len(name) == 0 { + name = fieldName + } else if trimmed := strings.TrimSpace(name); len(trimmed) == 0 { + name = fieldName + } + return +} + +func valueTree(tree Tree, v interface{}) error { + typ, val, err := checkType(v) + if err != nil { + return err + } + fields := typ.NumField() + for i := 0; i < fields; i++ { + field := typ.Field(i) + fieldValue := val.Field(i) + name, skip, omit := getMeta(field.Name, field.Tag) + if skip || omit && isEmpty(&fieldValue) { + continue + } + typ, val, isStruct := getValue(field.Type, &fieldValue) + if !isStruct { + tree.AddMetaNode(val.Interface(), name) + continue + } else if subNum := typ.NumField(); subNum == 0 { + tree.AddMetaNode(val.Interface(), name) + continue + } + branch := tree.AddBranch(name) + if err := valueTree(branch, val.Interface()); err != nil { + err := fmt.Errorf("%v on struct branch %s", err, name) + return err + } + } + return nil +} + +func tagTree(tree Tree, v interface{}) error { + typ, val, err := checkType(v) + if err != nil { + return err + } + fields := typ.NumField() + for i := 0; i < fields; i++ { + field := typ.Field(i) + fieldValue := val.Field(i) + name, skip, omit := getMeta(field.Name, field.Tag) + if skip || omit && isEmpty(&fieldValue) { + continue + } + filteredTag := filterTags(field.Tag) + typ, val, isStruct := getValue(field.Type, &fieldValue) + if !isStruct { + tree.AddMetaNode(filteredTag, name) + continue + } else if subNum := typ.NumField(); subNum == 0 { + tree.AddMetaNode(filteredTag, name) + continue + } + branch := tree.AddMetaBranch(filteredTag, name) + if err := tagTree(branch, val.Interface()); err != nil { + err := fmt.Errorf("%v on struct branch %s", err, name) + return err + } + } + return nil +} + +func typeTree(tree Tree, v interface{}) error { + typ, val, err := checkType(v) + if err != nil { + return err + } + fields := typ.NumField() + for i := 0; i < fields; i++ { + field := typ.Field(i) + fieldValue := val.Field(i) + name, skip, omit := getMeta(field.Name, field.Tag) + if skip || omit && isEmpty(&fieldValue) { + continue + } + typ, val, isStruct := getValue(field.Type, &fieldValue) + typename := fmt.Sprintf("%T", val.Interface()) + if !isStruct { + tree.AddMetaNode(typename, name) + continue + } else if subNum := typ.NumField(); subNum == 0 { + tree.AddMetaNode(typename, name) + continue + } + branch := tree.AddMetaBranch(typename, name) + if err := typeTree(branch, val.Interface()); err != nil { + err := fmt.Errorf("%v on struct branch %s", err, name) + return err + } + } + return nil +} + +func typeSizeTree(tree Tree, v interface{}) error { + typ, val, err := checkType(v) + if err != nil { + return err + } + fields := typ.NumField() + for i := 0; i < fields; i++ { + field := typ.Field(i) + fieldValue := val.Field(i) + name, skip, omit := getMeta(field.Name, field.Tag) + if skip || omit && isEmpty(&fieldValue) { + continue + } + typ, val, isStruct := getValue(field.Type, &fieldValue) + typesize := typ.Size() + if !isStruct { + tree.AddMetaNode(typesize, name) + continue + } else if subNum := typ.NumField(); subNum == 0 { + tree.AddMetaNode(typesize, name) + continue + } + branch := tree.AddMetaBranch(typesize, name) + if err := typeSizeTree(branch, val.Interface()); err != nil { + err := fmt.Errorf("%v on struct branch %s", err, name) + return err + } + } + return nil +} + +func metaTree(tree Tree, v interface{}, fmtFunc FmtFunc) error { + typ, val, err := checkType(v) + if err != nil { + return err + } + fields := typ.NumField() + for i := 0; i < fields; i++ { + field := typ.Field(i) + fieldValue := val.Field(i) + name, skip, omit := getMeta(field.Name, field.Tag) + if skip || omit && isEmpty(&fieldValue) { + continue + } + typ, val, isStruct := getValue(field.Type, &fieldValue) + formatted, show := fmtFunc(name, val.Interface()) + if !isStruct { + if show { + tree.AddMetaNode(formatted, name) + continue + } + tree.AddNode(name) + continue + } else if subNum := typ.NumField(); subNum == 0 { + if show { + tree.AddMetaNode(formatted, name) + continue + } + tree.AddNode(name) + continue + } + var branch Tree + if show { + branch = tree.AddMetaBranch(formatted, name) + } else { + branch = tree.AddBranch(name) + } + if err := metaTree(branch, val.Interface(), fmtFunc); err != nil { + err := fmt.Errorf("%v on struct branch %s", err, name) + return err + } + } + return nil +} + +func getValue(typ reflect.Type, val *reflect.Value) (reflect.Type, *reflect.Value, bool) { + switch typ.Kind() { + case reflect.Ptr: + typ = typ.Elem() + if typ.Kind() == reflect.Struct { + elem := val.Elem() + return typ, &elem, true + } + case reflect.Struct: + return typ, val, true + } + return typ, val, false +} + +func checkType(v interface{}) (reflect.Type, *reflect.Value, error) { + typ := reflect.TypeOf(v) + val := reflect.ValueOf(v) + switch typ.Kind() { + case reflect.Ptr: + typ = typ.Elem() + if typ.Kind() != reflect.Struct { + err := fmt.Errorf("treeprint: %T is not a struct we could work with", v) + return nil, nil, err + } + val = val.Elem() + case reflect.Struct: + default: + err := fmt.Errorf("treeprint: %T is not a struct we could work with", v) + return nil, nil, err + } + return typ, &val, nil +} diff --git a/vendor/github.com/xlab/treeprint/treeprint.go b/vendor/github.com/xlab/treeprint/treeprint.go new file mode 100644 index 00000000..931bac8d --- /dev/null +++ b/vendor/github.com/xlab/treeprint/treeprint.go @@ -0,0 +1,206 @@ +// Package treeprint provides a simple ASCII tree composing tool. +package treeprint + +import ( + "bytes" + "fmt" + "io" + "reflect" +) + +type Value interface{} +type MetaValue interface{} + +// Tree represents a tree structure with leaf-nodes and branch-nodes. +type Tree interface { + // AddNode adds a new node to a branch. + AddNode(v Value) Tree + // AddMetaNode adds a new node with meta value provided to a branch. + AddMetaNode(meta MetaValue, v Value) Tree + // AddBranch adds a new branch node (a level deeper). + AddBranch(v Value) Tree + // AddMetaBranch adds a new branch node (a level deeper) with meta value provided. + AddMetaBranch(meta MetaValue, v Value) Tree + // Branch converts a leaf-node to a branch-node, + // applying this on a branch-node does no effect. + Branch() Tree + // FindByMeta finds a node whose meta value matches the provided one by reflect.DeepEqual, + // returns nil if not found. + FindByMeta(meta MetaValue) Tree + // FindByValue finds a node whose value matches the provided one by reflect.DeepEqual, + // returns nil if not found. + FindByValue(value Value) Tree + // returns the last node of a tree + FindLastNode() Tree + // String renders the tree or subtree as a string. + String() string + // Bytes renders the tree or subtree as byteslice. + Bytes() []byte + + SetValue(value Value) +} + +type node struct { + Root *node + Meta MetaValue + Value Value + Nodes []*node +} + +func (n *node) FindLastNode() Tree { + ns := n.Nodes + n = ns[len(ns)-1] + return n +} + +func (n *node) AddNode(v Value) Tree { + n.Nodes = append(n.Nodes, &node{ + Root: n, + Value: v, + }) + if n.Root != nil { + return n.Root + } + return n +} + +func (n *node) AddMetaNode(meta MetaValue, v Value) Tree { + n.Nodes = append(n.Nodes, &node{ + Root: n, + Meta: meta, + Value: v, + }) + if n.Root != nil { + return n.Root + } + return n +} + +func (n *node) AddBranch(v Value) Tree { + branch := &node{ + Value: v, + } + n.Nodes = append(n.Nodes, branch) + return branch +} + +func (n *node) AddMetaBranch(meta MetaValue, v Value) Tree { + branch := &node{ + Meta: meta, + Value: v, + } + n.Nodes = append(n.Nodes, branch) + return branch +} + +func (n *node) Branch() Tree { + n.Root = nil + return n +} + +func (n *node) FindByMeta(meta MetaValue) Tree { + for _, node := range n.Nodes { + if reflect.DeepEqual(node.Meta, meta) { + return node + } + if v := node.FindByMeta(meta); v != nil { + return v + } + } + return nil +} + +func (n *node) FindByValue(value Value) Tree { + for _, node := range n.Nodes { + if reflect.DeepEqual(node.Value, value) { + return node + } + if v := node.FindByMeta(value); v != nil { + return v + } + } + return nil +} + +func (n *node) Bytes() []byte { + buf := new(bytes.Buffer) + level := 0 + var levelsEnded []int + if n.Root == nil { + buf.WriteString(fmt.Sprintf("%v",n.Value)) + buf.WriteByte('\n') + } else { + edge := EdgeTypeMid + if len(n.Nodes) == 0 { + edge = EdgeTypeEnd + levelsEnded = append(levelsEnded, level) + } + printValues(buf, 0, levelsEnded, edge, n.Meta, n.Value) + } + if len(n.Nodes) > 0 { + printNodes(buf, level, levelsEnded, n.Nodes) + } + return buf.Bytes() +} + +func (n *node) String() string { + return string(n.Bytes()) +} + +func (n *node) SetValue(value Value){ + n.Value = value +} + +func printNodes(wr io.Writer, + level int, levelsEnded []int, nodes []*node) { + + for i, node := range nodes { + edge := EdgeTypeMid + if i == len(nodes)-1 { + levelsEnded = append(levelsEnded, level) + edge = EdgeTypeEnd + } + printValues(wr, level, levelsEnded, edge, node.Meta, node.Value) + if len(node.Nodes) > 0 { + printNodes(wr, level+1, levelsEnded, node.Nodes) + } + } +} + +func printValues(wr io.Writer, + level int, levelsEnded []int, edge EdgeType, meta MetaValue, val Value) { + + for i := 0; i < level; i++ { + if isEnded(levelsEnded, i) { + fmt.Fprint(wr, " ") + continue + } + fmt.Fprintf(wr, "%s   ", EdgeTypeLink) + } + if meta != nil { + fmt.Fprintf(wr, "%s [%v] %v\n", edge, meta, val) + return + } + fmt.Fprintf(wr, "%s %v\n", edge, val) +} + +func isEnded(levelsEnded []int, level int) bool { + for _, l := range levelsEnded { + if l == level { + return true + } + } + return false +} + +type EdgeType string + +var ( + EdgeTypeLink EdgeType = "│" + EdgeTypeMid EdgeType = "├──" + EdgeTypeEnd EdgeType = "└──" +) + +func New() Tree { + return &node{Value: "."} +} diff --git a/vendor/vendor.json b/vendor/vendor.json index e361c2da..fb00c45c 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -323,6 +323,12 @@ "revision": "1e01b2bdbae8edb393fcf555732304f34d192fc9", "revisionTime": "2016-11-21T14:22:35Z" }, + { + "checksumSHA1": "Oen67XBkt5voJj1wya8kIakvdT4=", + "path": "github.com/xlab/treeprint", + "revision": "d6fb6747feb6e7cfdc44682a024bddf87ef07ec2", + "revisionTime": "2018-06-15T14:19:04Z" + }, { "checksumSHA1": "hKZGpbFuCrkE4s1vxubKK1aFlfI=", "path": "golang.org/x/net/context",