feat: added routing tools to amtool (#1511)

Signed-off-by: Martin Chodur <m.chodur@seznam.cz>
This commit is contained in:
Martin Chodur 2018-08-22 16:41:09 +02:00 committed by stuart nelson
parent e220c00a01
commit 5d222bce55
16 changed files with 1133 additions and 10 deletions

View File

@ -1,6 +1,7 @@
## Next release ## Next release
* [CHANGE] Revert Alertmanager working directory changes in Docker image back to `/alertmanager` (#1435) * [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 ## 0.15.2 / 2018-08-14

View File

@ -286,6 +286,22 @@ output: extended
receiver: team-X-pager 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 ## High Availability
> Warning: High Availability is under active development > Warning: High Availability is under active development

View File

@ -17,11 +17,9 @@ import (
"context" "context"
"errors" "errors"
"github.com/prometheus/client_golang/api"
"gopkg.in/alecthomas/kingpin.v2" "gopkg.in/alecthomas/kingpin.v2"
"github.com/prometheus/alertmanager/cli/format" "github.com/prometheus/alertmanager/cli/format"
"github.com/prometheus/alertmanager/client"
) )
const configHelp = `View current config. 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 // configCmd represents the config command
func configureConfigCmd(app *kingpin.Application) { 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 { func queryConfig(ctx context.Context, _ *kingpin.ParseContext) error {
c, err := api.NewClient(api.Config{Address: alertmanagerURL.String()}) status, err := getRemoteAlertmanagerConfigStatus(ctx, alertmanagerURL)
if err != nil {
return err
}
statusAPI := client.NewStatusAPI(c)
status, err := statusAPI.Get(ctx)
if err != nil { if err != nil {
return err return err
} }

120
cli/routing.go Normal file
View File

@ -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))
}
}

101
cli/test_routing.go Normal file
View File

@ -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
}

64
cli/test_routing_test.go Normal file
View File

@ -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")
}
}

22
cli/testdata/conf.routing-reverted.yml vendored Normal file
View File

@ -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

21
cli/testdata/conf.routing.yml vendored Normal file
View File

@ -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

View File

@ -18,9 +18,12 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/url" "net/url"
"os"
"path" "path"
"github.com/prometheus/alertmanager/client" "github.com/prometheus/alertmanager/client"
amconfig "github.com/prometheus/alertmanager/config"
"github.com/prometheus/client_golang/api"
kingpin "gopkg.in/alecthomas/kingpin.v2" kingpin "gopkg.in/alecthomas/kingpin.v2"
"github.com/prometheus/alertmanager/pkg/parse" "github.com/prometheus/alertmanager/pkg/parse"
@ -70,6 +73,57 @@ func parseMatchers(inputMatchers []string) ([]labels.Matcher, error) {
return matchers, nil 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) // Parse a list of labels (cli arguments)
func parseLabels(inputLabels []string) (client.LabelSet, error) { func parseLabels(inputLabels []string) (client.LabelSet, error) {
labelSet := make(client.LabelSet, len(inputLabels)) labelSet := make(client.LabelSet, len(inputLabels))

View File

@ -75,6 +75,7 @@ route:
- match: - match:
owner: team-X owner: team-X
receiver: team-X-pager receiver: team-X-pager
continue: true
- match: - match:
owner: team-Y owner: team-Y
receiver: team-Y-pager receiver: team-Y-pager

20
vendor/github.com/xlab/treeprint/LICENSE generated vendored Normal file
View File

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright © 2016 Maxim Kupriianov <max@kc.vc>
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.

128
vendor/github.com/xlab/treeprint/README.md generated vendored Normal file
View File

@ -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.
<a href="https://upload.wikimedia.org/wikipedia/commons/5/58/ENC_SYSTEME_FIGURE.jpeg"><img alt="SYSTEME FIGURE" src="https://upload.wikimedia.org/wikipedia/commons/thumb/5/58/ENC_SYSTEME_FIGURE.jpeg/896px-ENC_SYSTEME_FIGURE.jpeg" align="left" width="300"></a>
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

47
vendor/github.com/xlab/treeprint/helpers.go generated vendored Normal file
View File

@ -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, " ")
}

322
vendor/github.com/xlab/treeprint/struct.go generated vendored Normal file
View File

@ -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
}

206
vendor/github.com/xlab/treeprint/treeprint.go generated vendored Normal file
View File

@ -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: "."}
}

6
vendor/vendor.json vendored
View File

@ -323,6 +323,12 @@
"revision": "1e01b2bdbae8edb393fcf555732304f34d192fc9", "revision": "1e01b2bdbae8edb393fcf555732304f34d192fc9",
"revisionTime": "2016-11-21T14:22:35Z" "revisionTime": "2016-11-21T14:22:35Z"
}, },
{
"checksumSHA1": "Oen67XBkt5voJj1wya8kIakvdT4=",
"path": "github.com/xlab/treeprint",
"revision": "d6fb6747feb6e7cfdc44682a024bddf87ef07ec2",
"revisionTime": "2018-06-15T14:19:04Z"
},
{ {
"checksumSHA1": "hKZGpbFuCrkE4s1vxubKK1aFlfI=", "checksumSHA1": "hKZGpbFuCrkE4s1vxubKK1aFlfI=",
"path": "golang.org/x/net/context", "path": "golang.org/x/net/context",