Skip to content

Commit

Permalink
Merge pull request #2 from rebuy-de/reduce-copy-pasta-overhead
Browse files Browse the repository at this point in the history
reduce copy pasta overhead on all applications
  • Loading branch information
svenwltr authored Mar 12, 2018
2 parents 83f49db + 8526093 commit 83c9407
Show file tree
Hide file tree
Showing 4 changed files with 209 additions and 5 deletions.
77 changes: 77 additions & 0 deletions cmdutil/app.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package cmdutil

import (
"github.com/spf13/cobra"
graylog "gopkg.in/gemnasium/logrus-graylog-hook.v2"

log "github.com/sirupsen/logrus"
)

// Application provides the basic behaviour for NewRootCommand.
type Application interface {
// Run contains the actual application code. It is equivalent to
// the Run command of Cobra.
Run(cmd *cobra.Command, args []string)

// Bind is used to bind command line flags to fields of the
// application struct.
Bind(cmd *cobra.Command)
}

// NewRootCommand creates a Cobra command, which reflects our current best
// practices. It adds a verbose flag, sets up logrus and registers a Graylog
// hook. Also it registers the NewVersionCommand and prints the version on
// startup.
func NewRootCommand(app Application) *cobra.Command {
var (
gelfAddress string
verbose bool
)

cmd := &cobra.Command{
Use: BuildName,
Run: app.Run,

PersistentPreRun: func(cmd *cobra.Command, args []string) {
log.SetLevel(log.InfoLevel)

if verbose {
log.SetLevel(log.DebugLevel)
}

if gelfAddress != "" {
labels := map[string]interface{}{
"facility": BuildName,
"version": BuildVersion,
"commit-sha": BuildHash,
}
hook := graylog.NewGraylogHook(gelfAddress, labels)
hook.Level = log.DebugLevel
log.AddHook(hook)
}

log.WithFields(log.Fields{
"Version": BuildVersion,
"Date": BuildDate,
"Commit": BuildHash,
}).Infof("%s started", BuildName)
},

PersistentPostRun: func(cmd *cobra.Command, args []string) {
log.Infof("%s stopped", BuildName)
},
}

cmd.PersistentFlags().BoolVarP(
&verbose, "verbose", "v", false,
`Show debug logs.`)
cmd.PersistentFlags().StringVar(
&gelfAddress, "gelf-address", "",
`Address to Graylog for logging (format: "ip:port").`)

app.Bind(cmd)

cmd.AddCommand(NewVersionCommand())

return cmd
}
93 changes: 93 additions & 0 deletions cmdutil/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Package cmdutil contains helper utilities for setting up a CLI with Go,
// providing basic application behavior and for reducing boilerplate code.
//
// An example application can be found at
// https://github.com/rebuy-de/golang-template.
//
// Graceful Application Exits
//
// In many command line applications it is desired to exit the process
// immediately, if it is clear that the application cannot recover. Important
// note: This is designed for actual applications (ie not libraries), because
// only the application itself should decide when to exit. Libraries should
// alway return errors.
//
// There are three ways to handle fatal errors in Go. With os.Exit() the
// process will terminate immediately, but it will not call any deferrers which
// means that possible cleanup task do not get called. The next way is to call
// panic, which respects the defer statements, but unfortunately it is not
// possible to define an exit code and the user gets confused with a stack
// trace. Finally, the function could just return an error indicating that
// things failed, but this introduces a lot of code, conditionals and appears
// unnecessary, when it is already clear that the application cannot recover.
//
// The package cmdutil provides an alternative, which panics with a known
// struct and catches it right before the application exit. This is an example
// to illustrate the usage:
//
// func main() {
// defer cmdutil.HandleExit()
// run()
// }
//
// func run() {
// defer fmt.Println("important cleanup")
// err := doSomething()
// if err != nil {
// log.Error(err)
// cmdutil.Exit(2)
// }
// }
//
// The defer of HandleExit is the first statement in the main function. It
// ensures a pretty output and that the application exits with the specified
// exit code. The run function does something and makes the application exit
// with an exit code. The specified defer statement is still called. Also the
// application logging facility should be used to communicate the error, so the
// error actually appears on external logging applications like Syslog or
// Graylog.
//
// Minimal Application Boilerplate
//
// Golang is very helpful for creating glue code in the ops area and creating
// micro services. But when you want features like proper logging, a version
// subcommand and a clean structure, there is still a lot of boilerplate code
// needed. NewRootCommand creates a ready-to-use Cobra command to reduce the
// necessary code. This is an example to illustrate the usage:
//
// type App struct {
// Name string
// }
//
// func (app *App) Run(cmd *cobra.Command, args []string) {
// log.Infof("hello %s", app.Name)
// }
//
// func (app *App) Bind(cmd *cobra.Command) {
// cmd.PersistentFlags().StringVarP(
// &app.Name, "name", "n", "world",
// `Your name.`)
// }
//
// func NewRootCommand() *cobra.Command {
// cmd := cmdutil.NewRootCommand(new(App))
// cmd.Short = "an example app for golang which can be used as template"
// return cmd
// }
//
// The App struct contains fields for parameters which are defined in Bind or
// for internal states which might get defined while running the application.
//
// NewRootCommand also attaches NewVersionCommand to the application. It prints
// the compiled version of the application and other build parameters. These
// values need to be set by the build system via ldflags.
//
// BUILD_XDST=$(pwd)/vendor/github.com/rebuy-de/rebuy-go-sdk/cmdutil
// go build -ldflags "\
// -X '${BUILD_XDST}.BuildName=${NAME}' \
// -X '${BUILD_XDST}.BuildPackage=${PACKAGE}' \
// -X '${BUILD_XDST}.BuildVersion=${BUILD_VERSION}' \
// -X '${BUILD_XDST}.BuildDate=${BUILD_DATE}' \
// -X '${BUILD_XDST}.BuildHash=${BUILD_HASH}' \
// -X '${BUILD_XDST}.BuildEnvironment=${BUILD_ENVIRONMENT}' \
package cmdutil
5 changes: 0 additions & 5 deletions cmdutil/exit.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,6 @@ func Exit(code int) {
// HandleExit recovers from Exit calls and terminates the current program with
// a proper exit code. It should get deferred at the beginning of the main
// function.
//
// func main() {
// defer cmdutil.HandleExit()
// run() // this function might call Exit anytime
// }
func HandleExit() {
if e := recover(); e != nil {
if exit, ok := e.(exitCode); ok == true {
Expand Down
39 changes: 39 additions & 0 deletions cmdutil/version.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package cmdutil

import (
"fmt"

"github.com/spf13/cobra"
)

// The Build* variables are used by NewVersionCommand and NewRootCommand. They
// should be overwritten on build time by using ldflags.
var (
BuildVersion = "unknown"
BuildPackage = "unknown"
BuildDate = "unknown"
BuildHash = "unknown"
BuildEnvironment = "unknown"
BuildName = "unknown"
)

// NewVersionCommand creates a Cobra command, which prints the version
// and other build parameters (see Build* variables) and exits.
func NewVersionCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "version",
Short: "shows version of this application",
PersistentPreRun: func(cmd *cobra.Command, args []string) {},
PersistentPostRun: func(cmd *cobra.Command, args []string) {},
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("name: %s\n", BuildName)
fmt.Printf("package: %s\n", BuildPackage)
fmt.Printf("version: %s\n", BuildVersion)
fmt.Printf("build date: %s\n", BuildDate)
fmt.Printf("scm hash: %s\n", BuildHash)
fmt.Printf("environment: %s\n", BuildEnvironment)
},
}

return cmd
}

0 comments on commit 83c9407

Please sign in to comment.