Merge pull request #816 from prometheus/fabxc/promctl

Create promtool command
This commit is contained in:
Fabian Reinartz 2015-06-22 16:40:09 +02:00
commit ccbc801d19
3 changed files with 354 additions and 1 deletions

View file

@ -28,6 +28,7 @@ binary: build
build: tools $(GOPATH) build: tools $(GOPATH)
$(GO) build -o prometheus $(BUILDFLAGS) github.com/prometheus/prometheus/cmd/prometheus $(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
docker build -t prometheus:$(REV) . docker build -t prometheus:$(REV) .
@ -35,7 +36,7 @@ docker: build
tarball: $(ARCHIVE) tarball: $(ARCHIVE)
$(ARCHIVE): build $(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 ?= $(error "can't upload, REMOTE not set")
release: REMOTE_DIR ?= $(error "can't upload, REMOTE_DIR not set") release: REMOTE_DIR ?= $(error "can't upload, REMOTE_DIR not set")

184
cmd/promtool/main.go Normal file
View file

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

168
util/cli/cli.go Normal file
View file

@ -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 }} <command> [<args>]
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())
}
}