Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Amtool implementation #636

Merged
merged 48 commits into from
Apr 20, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
3ea3e8a
Implement alertmanager cli tool 'amtool'
Dec 31, 2016
cd0d337
Update gitignore
Jan 4, 2017
7578ffc
Implement config subcommand
Jan 6, 2017
f33bac9
Move output flag to root command. Also print config notification to s…
Jan 6, 2017
b2ab06b
Implement alert command
Jan 6, 2017
be80a7e
Revamp silence command
Jan 6, 2017
02c4ac2
Flesh out format stuff
Jan 6, 2017
ba742c8
Fix config format for extended output
Jan 6, 2017
e61d360
Allow alername default when no = specified
Jan 6, 2017
605c403
Vendor in cobra, viper, and pflag
Jan 7, 2017
fbd99d9
Fix argument ordering
Jan 9, 2017
0d534c2
added more comments:
Jan 9, 2017
89869e7
Remove unneeded copyright block
Jan 23, 2017
a0bab45
Fix capital A in Alertmanager
Jan 23, 2017
1d8dbf0
Exit 1 instead of -1
Jan 23, 2017
513312f
Remove un-used function
Jan 23, 2017
8915d2f
Fixup tests to simplified types
Jan 23, 2017
a22ac3d
Remove unneeded comment from code generation
Jan 23, 2017
d9413d0
Make alertmanager url flag consistent with alertmanager cli syntax
Jan 23, 2017
d1dec7e
Made the url validation better
Jan 23, 2017
360a631
Fixups from 1:1 session
Jan 30, 2017
2199e68
Vastly improve help commands
Feb 24, 2017
8f614b0
Add docs about config file
Feb 25, 2017
8e8f364
Add docs about how to generate man pages and bash completions
Feb 25, 2017
597a362
Merge pull request #1 from Kellel/amtool
Kellel Feb 25, 2017
c7e603a
Fix my vendors
Feb 25, 2017
f5478c6
Re-order things in tests so that they pass (things are sorted now)
Feb 25, 2017
e523382
Fix ordering of regex string formatter
Mar 3, 2017
f536f83
Fix fmt.Sprintf formatting to multiple lines so that it's easier to read
Mar 3, 2017
eaf6249
Refactor config file loading
Mar 3, 2017
fbdc789
Update docs for config file
Mar 3, 2017
0aa1b09
Small fixes to config system
Mar 6, 2017
7326c32
fixup name of VersionInfo
Mar 7, 2017
576e643
Merge changes from master
Mar 16, 2017
70316d8
Allow export of regex from label parser
Mar 21, 2017
fba0793
Merge in changes related to parser code
Mar 21, 2017
32a8973
Major refactor to use filters in #633
Apr 6, 2017
aa47c41
Add missing dependancy
Apr 6, 2017
e3caaa8
Add alert query alias
Apr 7, 2017
7d6a860
Re-implement `alertname=` prefix when the first argument isn't a matcher
Apr 11, 2017
5d0bba4
Fix bug with extra regex information leaking from cli created silences
Apr 11, 2017
ac50b07
Implement `-q` flag for only silence id output
Apr 11, 2017
507b32b
Revert "Fix bug with extra regex information leaking from cli created…
Apr 12, 2017
dabbd06
Revert change to vendored repo and apply parsing change patch
Apr 12, 2017
7f3ce6d
Make `-q` `--quiet` flags a silence global command
Apr 19, 2017
0da7098
Fixup config handling since things changed in alertmanager since initail
Apr 19, 2017
ffb6747
Remove double error notifications
Apr 19, 2017
e56573b
Merge branch 'master' into master
Kellel Apr 19, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/data/
/alertmanager
/amtool
*.yml
*.yaml
/.build
Expand Down
2 changes: 2 additions & 0 deletions .promu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ build:
binaries:
- name: alertmanager
path: ./cmd/alertmanager
- name: amtool
path: ./cmd/amtool
flags: -a -tags netgo
ldflags: |
-X {{repoPath}}/vendor/github.com/prometheus/common/version.Version={{.Version}}
Expand Down
18 changes: 18 additions & 0 deletions artifacts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generating amtool artifacts

Amtool comes with the option to create a number of eaze-of-use artifacts that can be created.

go run generate_amtool_artifacts.go

## Bash completion

The bash completion file can be added to `/etc/bash_completion.d/`.

## Man pages

Man pages can be added to the man directory of your choice

cp artifacts/*.1 /usr/local/share/man/man1/
sudo mandb

Then you should be able to view the man pages as expected.
17 changes: 17 additions & 0 deletions artifacts/generate_amtool_artifacts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package main

import (
"github.com/spf13/cobra/doc"

"github.com/prometheus/alertmanager/cli"
)

func main() {
cli.RootCmd.GenBashCompletionFile("amtool_completion.sh")
header := &doc.GenManHeader{
Title: "amtool",
Section: "1",
}

doc.GenManTree(cli.RootCmd, header, ".")
}
173 changes: 173 additions & 0 deletions cli/alert.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package cli

import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"path"
"strings"
"time"

"github.com/prometheus/alertmanager/cli/format"
"github.com/prometheus/alertmanager/dispatch"
"github.com/prometheus/alertmanager/pkg/parse"
"github.com/prometheus/common/model"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)

type alertmanagerAlertResponse struct {
Status string `json:"status"`
Data []*alertGroup `json:"data,omitempty"`
ErrorType string `json:"errorType,omitempty"`
Error string `json:"error,omitempty"`
}

type alertGroup struct {
Labels model.LabelSet `json:"labels"`
GroupKey uint64 `json:"groupKey"`
Blocks []*alertBlock `json:"blocks"`
}

type alertBlock struct {
RouteOpts interface{} `json:"routeOpts"`
Alerts []*dispatch.APIAlert `json:"alerts"`
}

// alertCmd represents the alert command
var alertCmd = &cobra.Command{
Use: "alert",
Short: "View and search through current alerts",
Long: `View and search through current alerts.

Amtool has a simplified prometheus query syntax, but contains robust support for
bash variable expansions. The non-option section of arguments constructs a list
of "Matcher Groups" that will be used to filter your query. The following
examples will attempt to show this behaviour in action:

amtool alert query alertname=foo node=bar

This query will match all alerts with the alertname=foo and node=bar label
value pairs set.

amtool alert query foo node=bar

If alertname is ommited and the first argument does not contain a '=' or a
'=~' then it will be assumed to be the value of the alertname pair.

amtool alert query 'alertname=~foo.*'

As well as direct equality, regex matching is also supported. The '=~' syntax
(similar to prometheus) is used to represent a regex match. Regex matching
can be used in combination with a direct match.
`,
RunE: queryAlerts,
}

var alertQueryCmd = &cobra.Command{
Use: "query",
Short: "View and search through current alerts",
Long: alertCmd.Long,
RunE: queryAlerts,
}

func init() {
RootCmd.AddCommand(alertCmd)
alertCmd.AddCommand(alertQueryCmd)
alertQueryCmd.Flags().Bool("expired", false, "Show expired alerts as well as active")
alertQueryCmd.Flags().BoolP("silenced", "s", false, "Show silenced alerts")
viper.BindPFlag("expired", alertQueryCmd.Flags().Lookup("expired"))
viper.BindPFlag("silenced", alertQueryCmd.Flags().Lookup("silenced"))
}

func fetchAlerts(filter string) ([]*dispatch.APIAlert, error) {
alertResponse := alertmanagerAlertResponse{}

u, err := GetAlertmanagerURL()
if err != nil {
return []*dispatch.APIAlert{}, err
}

u.Path = path.Join(u.Path, "/api/v1/alerts/groups")
u.RawQuery = "filter=" + url.QueryEscape(filter)

res, err := http.Get(u.String())
if err != nil {
return []*dispatch.APIAlert{}, err
}

defer res.Body.Close()

err = json.NewDecoder(res.Body).Decode(&alertResponse)
if err != nil {
return []*dispatch.APIAlert{}, errors.New("Unable to decode json response")
}

if alertResponse.Status != "success" {
return []*dispatch.APIAlert{}, fmt.Errorf("[%s] %s", alertResponse.ErrorType, alertResponse.Error)
}

return flattenAlertOverview(alertResponse.Data), nil
}

func flattenAlertOverview(overview []*alertGroup) []*dispatch.APIAlert {
alerts := []*dispatch.APIAlert{}
for _, group := range overview {
for _, block := range group.Blocks {
alerts = append(alerts, block.Alerts...)
}
}
return alerts
}

func queryAlerts(cmd *cobra.Command, args []string) error {
expired := viper.GetBool("expired")
showSilenced := viper.GetBool("silenced")

var filterString = ""
if len(args) == 1 {
// If we only have one argument then it's possible that the user wants me to assume alertname=<arg>
// Attempt to use the parser to pare the argument
// If the parser fails then we likely don't have a (=|=~|!=|!~) so lets prepend `alertname=` to the front
_, err := parse.Matcher(args[0])
if err != nil {
filterString = fmt.Sprintf("{alertname=%s}", args[0])
} else {
filterString = fmt.Sprintf("{%s}", strings.Join(args, ","))
}
} else if len(args) > 1 {
filterString = fmt.Sprintf("{%s}", strings.Join(args, ","))
}

fetchedAlerts, err := fetchAlerts(filterString)
if err != nil {
return err
}

displayAlerts := []*dispatch.APIAlert{}
for _, alert := range fetchedAlerts {
// If we are only returning current alerts and this one has already expired skip it
if !expired {
if !alert.EndsAt.IsZero() && alert.EndsAt.Before(time.Now()) {
continue
}
}

if !showSilenced {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This filtering is something I've also implemented in the new alertmanager-ui. Seems like we should add it as a query string param to the url and do the filtering on the initial query, instead of duplicating our work in different consumers of the api?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. I kinda felt like it was a pain doing all this by hand in the cli tool

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The functionality is already available here: https:/prometheus/alertmanager/blob/master/dispatch/dispatch.go#L132-L134

All that needs to be done is parsing a ?silenced=true to show silenced alerts, and by default don't return them in the response.

// If any silence mutes this alert don't show it
if alert.Silenced != "" {
continue
}
}

displayAlerts = append(displayAlerts, alert)
}

formatter, found := format.Formatters[viper.GetString("output")]
if !found {
return errors.New("Unknown output formatter")
}
return formatter.FormatAlerts(displayAlerts)
}
106 changes: 106 additions & 0 deletions cli/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package cli

import (
"encoding/json"
"errors"
"fmt"
"net/http"
"path"
"time"

"github.com/prometheus/alertmanager/cli/format"
"github.com/prometheus/alertmanager/config"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)

// Config is the response type of alertmanager config endpoint
// Duped in cli/format needs to be moved to common/model
type Config struct {
Config string `json:"config"`
ConfigJSON config.Config `json:configJSON`
MeshStatus map[string]interface{} `json:"meshStatus"`
VersionInfo map[string]string `json:"versionInfo"`
Uptime time.Time `json:"uptime"`
}

type MeshStatus struct {
Name string `json:"name"`
NickName string `json:"nickName"`
Peers []PeerStatus `json:"peerStatus"`
}

type PeerStatus struct {
Name string `json:"name"`
NickName string `json:"nickName"`
UID uint64 `uid`
}

type alertmanagerStatusResponse struct {
Status string `json:"status"`
Data Config `json:"data,omitempty"`
ErrorType string `json:"errorType,omitempty"`
Error string `json:"error,omitempty"`
}

// alertCmd represents the alert command
var configCmd = &cobra.Command{
Use: "config",
Short: "View the running config",
Long: `View current config

The amount of output is controlled by the output selection flag:
- Simple: Print just the running config
- Extended: Print the running config as well as uptime and all version info
- Json: Print entire config object as json`,
RunE: queryConfig,
}

func init() {
RootCmd.AddCommand(configCmd)
}

func fetchConfig() (Config, error) {
configResponse := alertmanagerStatusResponse{}
u, err := GetAlertmanagerURL()
if err != nil {
return Config{}, err
}

u.Path = path.Join(u.Path, "/api/v1/status")
res, err := http.Get(u.String())
if err != nil {
return Config{}, err
}

defer res.Body.Close()
decoder := json.NewDecoder(res.Body)

err = decoder.Decode(&configResponse)
if err != nil {
panic(err)
return Config{}, err
}

if configResponse.Status != "success" {
return Config{}, fmt.Errorf("[%s] %s", configResponse.ErrorType, configResponse.Error)
}

return configResponse.Data, nil
}

func queryConfig(cmd *cobra.Command, args []string) error {
config, err := fetchConfig()
if err != nil {
return err
}

formatter, found := format.Formatters[viper.GetString("output")]
if !found {
return errors.New("Unknown output formatter")
}

c := format.Config(config)

return formatter.FormatConfig(c)
}
51 changes: 51 additions & 0 deletions cli/format/format.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package format

import (
"io"
"time"

"github.com/prometheus/alertmanager/config"
"github.com/prometheus/alertmanager/dispatch"
"github.com/prometheus/alertmanager/types"
"github.com/spf13/viper"
)

const DefaultDateFormat = "2006-01-02 15:04:05 MST"

// Config representation
// Need to get this moved to the prometheus/common/model repo having is duplicated here is smelly
type Config struct {
Config string `json:"config"`
ConfigJSON config.Config `json:configJSON`
MeshStatus map[string]interface{} `json:"meshStatus"`
VersionInfo map[string]string `json:"versionInfo"`
Uptime time.Time `json:"uptime"`
}

type MeshStatus struct {
Name string `json:"name"`
NickName string `json:"nickName"`
Peers []PeerStatus `json:"peerStatus"`
}

type PeerStatus struct {
Name string `json:"name"`
NickName string `json:"nickName"`
UID uint64 `uid`
}

// Formatter needs to be implemented for each new output formatter
type Formatter interface {
SetOutput(io.Writer)
FormatSilences([]types.Silence) error
FormatAlerts([]*dispatch.APIAlert) error
FormatConfig(Config) error
}

// Formatters is a map of cli argument name to formatter inferface object
var Formatters map[string]Formatter = map[string]Formatter{}

func FormatDate(input time.Time) string {
dateformat := viper.GetString("date.format")
return input.Format(dateformat)
}
Loading