diff --git a/Makefile b/Makefile index 7c33f0a94..94273ada5 100644 --- a/Makefile +++ b/Makefile @@ -28,6 +28,7 @@ binary: build build: tools $(GOPATH) $(GO) build -o prometheus $(BUILDFLAGS) github.com/prometheus/prometheus/cmd/prometheus + $(GO) build -o promtool $(BUILDFLAGS) github.com/prometheus/prometheus/cmd/promtool docker: build docker build -t prometheus:$(REV) . @@ -35,7 +36,7 @@ docker: build tarball: $(ARCHIVE) $(ARCHIVE): build - tar -czf $(ARCHIVE) prometheus tools/rule_checker/rule_checker consoles console_libraries + tar -czf $(ARCHIVE) prometheus promtool tools/rule_checker/rule_checker consoles console_libraries release: REMOTE ?= $(error "can't upload, REMOTE not set") release: REMOTE_DIR ?= $(error "can't upload, REMOTE_DIR not set") diff --git a/cmd/promtool/main.go b/cmd/promtool/main.go new file mode 100644 index 000000000..3091a372c --- /dev/null +++ b/cmd/promtool/main.go @@ -0,0 +1,184 @@ +// Copyright 2015 The Prometheus Authors +// 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 main + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + "text/template" + + "gopkg.in/yaml.v2" + + "github.com/prometheus/prometheus/config" + "github.com/prometheus/prometheus/promql" + "github.com/prometheus/prometheus/util/cli" + "github.com/prometheus/prometheus/version" +) + +// CheckConfigCmd validates configuration files. +func CheckConfigCmd(t cli.Term, args ...string) int { + if len(args) == 0 { + t.Infof("usage: promtool check-config ") + return 2 + } + failed := false + + for _, arg := range args { + ruleFiles, err := checkConfig(t, arg) + if err != nil { + t.Errorf(" FAILED: %s", err) + failed = true + } else { + t.Infof(" SUCCESS: %d rule files found", len(ruleFiles)) + } + t.Infof("") + + for _, rf := range ruleFiles { + if n, err := checkRules(t, rf); err != nil { + t.Errorf(" FAILED: %s", err) + failed = true + } else { + t.Infof(" SUCCESS: %d rules found", n) + } + t.Infof("") + } + } + if failed { + return 1 + } + return 0 +} + +func checkConfig(t cli.Term, filename string) ([]string, error) { + t.Infof("Checking %s", filename) + + if stat, err := os.Stat(filename); err != nil { + return nil, fmt.Errorf("cannot get file info") + } else if stat.IsDir() { + return nil, fmt.Errorf("is a directory") + } + + content, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + + var cfg config.Config + if err := yaml.Unmarshal(content, &cfg); err != nil { + return nil, err + } + var ruleFiles []string + for _, rf := range cfg.RuleFiles { + rfs, err := filepath.Glob(rf) + if err != nil { + return nil, err + } + // If an explicit file was given, error if it doesn't exist. + if !strings.Contains(rf, "*") && len(rfs) == 0 { + return nil, fmt.Errorf("%q does not point to an existing file", rf) + } + ruleFiles = append(ruleFiles, rfs...) + } + + return ruleFiles, nil +} + +// CheckRulesCmd validates rule files. +func CheckRulesCmd(t cli.Term, args ...string) int { + if len(args) == 0 { + t.Infof("usage: promtool check-rules ") + return 2 + } + failed := false + + for _, arg := range args { + if n, err := checkRules(t, arg); err != nil { + t.Errorf(" FAILED: %s", err) + failed = true + } else { + t.Infof(" SUCCESS: %d rules found", n) + } + t.Infof("") + } + if failed { + return 1 + } + return 0 +} + +func checkRules(t cli.Term, filename string) (int, error) { + t.Infof("Checking %s", filename) + + if stat, err := os.Stat(filename); err != nil { + return 0, fmt.Errorf("cannot get file info") + } else if stat.IsDir() { + return 0, fmt.Errorf("is a directory") + } + + content, err := ioutil.ReadFile(filename) + if err != nil { + return 0, err + } + + rules, err := promql.ParseStmts(string(content)) + if err != nil { + return 0, err + } + return len(rules), nil +} + +var versionInfoTmpl = ` +prometheus, version {{.version}} (branch: {{.branch}}, revision: {{.revision}}) + build user: {{.buildUser}} + build date: {{.builDate}} + go version: {{.goVersion}} +` + +// VersionCmd prints the binaries version information. +func VersionCmd(t cli.Term, _ ...string) int { + tmpl := template.Must(template.New("version").Parse(versionInfoTmpl)) + + var buf bytes.Buffer + if err := tmpl.ExecuteTemplate(&buf, "version", version.Map); err != nil { + panic(err) + } + t.Out(strings.TrimSpace(buf.String())) + return 0 +} + +func main() { + app := cli.NewApp("promtool") + + app.Register("check-config", &cli.Command{ + Desc: "validate configuration files for correctness", + Run: CheckConfigCmd, + }) + + app.Register("check-rules", &cli.Command{ + Desc: "validate rule files for correctness", + Run: CheckRulesCmd, + }) + + app.Register("version", &cli.Command{ + Desc: "print the version of this binary", + Run: VersionCmd, + }) + + t := cli.BasicTerm(os.Stdout, os.Stderr) + os.Exit(app.Run(t, os.Args[1:]...)) +} diff --git a/util/cli/cli.go b/util/cli/cli.go new file mode 100644 index 000000000..063db561a --- /dev/null +++ b/util/cli/cli.go @@ -0,0 +1,168 @@ +// Copyright 2015 The Prometheus Authors +// 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" + "fmt" + "io" + "sort" + "strings" + "text/template" +) + +// Command represents a single command within an application. +type Command struct { + Desc string + Run func(t Term, args ...string) int +} + +// Term handles an application's output. +type Term interface { + Infof(format string, v ...interface{}) + Errorf(format string, v ...interface{}) + Out(format string) +} + +type basicTerm struct { + out, err io.Writer +} + +// Infof implements Term. +func (t *basicTerm) Infof(format string, v ...interface{}) { + fmt.Fprintf(t.err, format, v...) + fmt.Fprint(t.err, "\n") +} + +// Errorf implements Term. +func (t *basicTerm) Errorf(format string, v ...interface{}) { + fmt.Fprintf(t.err, format, v...) + fmt.Fprint(t.err, "\n") +} + +// Out implements Term. +func (t *basicTerm) Out(msg string) { + fmt.Fprint(t.out, msg) + fmt.Fprint(t.out, "\n") +} + +// BasicTerm returns a Term writing Infof and Errorf to err and Out to out. +func BasicTerm(out, err io.Writer) Term { + return &basicTerm{out: out, err: err} +} + +// App represents an application that may consist of multiple commands. +type App struct { + Name string + Help func() string + + commands map[string]*Command +} + +// NewApp creates a new application with a pre-registered help command. +func NewApp(name string) *App { + app := &App{ + Name: name, + commands: map[string]*Command{}, + } + app.Register("help", &Command{ + Desc: "prints this help text", + Run: func(t Term, _ ...string) int { + help := app.Help + if help == nil { + help = BasicHelp(app, tmpl) + } + t.Infof(help() + "\n") + return 0 + }, + }) + return app +} + +// Register adds a new command to the application. +func (app *App) Register(name string, cmd *Command) { + name = strings.TrimSpace(name) + if name == "" { + panic("command name must not be empty") + } + if _, ok := app.commands[name]; ok { + panic("command cannot be registered twice") + } + app.commands[name] = cmd +} + +// Run the application with the given arguments. Output is sent to t. +func (app *App) Run(t Term, args ...string) int { + help := app.commands["help"] + + if len(args) == 0 || strings.HasPrefix(args[0], "-") { + help.Run(t) + return 2 + } + cmd, ok := app.commands[args[0]] + if !ok { + help.Run(t) + return 2 + } + + return cmd.Run(t, args[1:]...) +} + +var tmpl = ` +usage: {{ .Name }} [] + +Available commands: + {{ range .Commands }}{{ .Name }} {{ .Desc }} + {{ end }} +` + +// BasicHelp returns a function that creates a basic help text for the application +// with its commands. +func BasicHelp(app *App, ts string) func() string { + t := template.Must(template.New("help").Parse(ts)) + + return func() string { + type command struct { + Name, Desc string + } + cmds := []command{} + + var maxLen int + names := []string{} + for name := range app.commands { + names = append(names, name) + if len(name) > maxLen { + maxLen = len(name) + } + } + sort.Strings(names) + + for _, name := range names { + cmds = append(cmds, command{ + Name: name + strings.Repeat(" ", maxLen-len(name)), + Desc: app.commands[name].Desc, + }) + } + + var buf bytes.Buffer + t.Execute(&buf, struct { + Name string + Commands []command + }{ + Name: app.Name, + Commands: cmds, + }) + return strings.TrimSpace(buf.String()) + } +}