Skip to content

Commit

Permalink
feat: added routing tools to amtool
Browse files Browse the repository at this point in the history
Signed-off-by: Martin Chodur <m.chodur@seznam.cz>
  • Loading branch information
FUSAKLA committed Aug 22, 2018
1 parent 049663b commit f1f4e32
Show file tree
Hide file tree
Showing 16 changed files with 1,133 additions and 10 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.1 / 2018-07-10

Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 4 additions & 10 deletions cli/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
}
Expand Down
120 changes: 120 additions & 0 deletions cli/routing.go
Original file line number Diff line number Diff line change
@@ -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 changes: 101 additions & 0 deletions cli/test_routing.go
Original file line number Diff line number Diff line change
@@ -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 changes: 64 additions & 0 deletions cli/test_routing_test.go
Original file line number Diff line number Diff line change
@@ -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 changes: 22 additions & 0 deletions cli/testdata/conf.routing-reverted.yml
Original file line number Diff line number Diff line change
@@ -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 changes: 21 additions & 0 deletions cli/testdata/conf.routing.yml
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit f1f4e32

Please sign in to comment.