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

Remove cobra and viper, add metrics #1

Merged
merged 16 commits into from
Sep 2, 2021
34 changes: 33 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,33 @@
# golang-api-template
# Getting started

After cloning repo, run:

```
go mod init <new repo eg. github.com/charmixer/golang-api-template>
go mod tidy
go run main.go serve
```

# What the template gives

## Configuration setup

Reading configuration in following order

1. Read from yaml file by setting `CFG_PATH=/path/to/conf.yaml`
2. Read from environment `PATH_TO_STRUCT_FIELD=override-value`
3. Read from flags `go run main.go serve -f override-value`
4. If none of above use `default:"value"` tag from configuration struct

## Metrics

Middleware for prometheus has been added. Access metrics from `/metrics`

Includes defaults and stuff like total requests. Customize in `middleware/metrics.go`

## Documentation

Using the structs to setup your endpoints will allow for automatic generation of openapi spec.

Documentation can be found at `/docs` and spec at `/docs/openapi.yaml`
If you get an error try setting `-d localhost`
40 changes: 39 additions & 1 deletion cmd/oas.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,43 @@
package cmd

import (
"os"
"fmt"

"github.com/charmixer/oas/exporter"
"github.com/charmixer/golang-api-template/router"
)

type OasCmd struct {
// version bool `short:"v" long:"version" description:"display version"`
}

func (v *OasCmd) Execute(args []string) error {
fmt.Println("oascmd")
fmt.Printf("%#v\n", v)
fmt.Printf("%#v\n", Application)

oas := router.NewOas()

oasModel := exporter.ToOasModel(oas)
oasYaml, err := exporter.ToYaml(oasModel)

if err != nil {
fmt.Println(err)
os.Exit(1)
}

fmt.Println(oasYaml)

os.Exit(0)

return nil
}


/*
package cmd

import (
"github.com/charmixer/golang-api-template/pkg/oas"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -28,4 +66,4 @@ func init() {
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// serveCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}
}*/
121 changes: 68 additions & 53 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,84 +2,99 @@ package cmd

import (
"os"
"fmt"
"gopkg.in/yaml.v2"
"io/ioutil"

"github.com/spf13/cobra"

homedir "github.com/mitchellh/go-homedir"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/spf13/viper"

"github.com/charmixer/envconfig"
"github.com/charmixer/go-flags"
)

var cfgFile string
var verbose bool
var console bool

// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "golang-api-template",
Short: "Template api",
Long: `Template api`,
// Uncomment the following line if your bare application
// has an action associated with it:
// Run: func(cmd *cobra.Command, args []string) { },
type App struct {
Log struct {
Verbose bool `long:"verbose" description:"Verbose logging"`
Format string `long:"log-format" description:"Logging format" choice:"json" choice:"plain"`
}

Serve ServeCmd `command:"serve" description:"serves endpoints"`
Oas OasCmd `command:"oas" description:"Retrieve oas document"`
}

// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
if err := rootCmd.Execute(); err != nil {
log.Error().Err(err)
os.Exit(1)
var Application App
var parser = flags.NewParser(&Application, flags.HelpFlag | flags.PassDoubleDash)

func Execute(){
_,err := parser.Execute()

if err != nil {
e := err.(*flags.Error)
if e.Type != flags.ErrCommandRequired && e.Type != flags.ErrHelp {
fmt.Printf("%s\n", e.Message)
}
parser.WriteHelp(os.Stdout)
}

os.Exit(0)
}

func init() {
cobra.OnInitialize(initLogging)
cobra.OnInitialize(initConfig)
// 3. Priority: Config file
parseYamlFile(os.Getenv("CFG_PATH"), &Application)

// 2. Priority: Environment
parseEnv("CFG", &Application)

// Here you will define your flags and configuration settings.
// Cobra supports persistent flags, which, if defined here,
// will be global for your application.
// 1. Priority: Flags
parseFlags(&Application)

rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.golang-api-template.yaml)")
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "enable verbose output")
rootCmd.PersistentFlags().BoolVarP(&console, "console", "c", false, "enable human readable console output")
// 0. Priority: Defaults, if none of above is found

initLogging()
}

// initConfig reads in config file and ENV variables if set.
func initConfig() {
if cfgFile != "" {
// Use config file from the flag.
viper.SetConfigFile(cfgFile)
} else {
// Find home directory.
home, err := homedir.Dir()
if err != nil {
log.Error().Err(err)
os.Exit(1)
}
func parseYamlFile(file string, config interface{}) {
if file == "" {
return
}

// Search config in home directory with name ".golang-api-template" (without extension).
viper.AddConfigPath(home)
viper.SetConfigName(".golang-api-template")
yamlFile, err := ioutil.ReadFile(file)
if err != nil {
panic(err)
}
err = yaml.Unmarshal(yamlFile, &config)
if err != nil {
panic(err)
}
}

viper.AutomaticEnv() // read in environment variables that match
func parseEnv(prefix string, config interface{}) {
err := envconfig.Process(prefix, config)
if err != nil {
panic(err)
}
}

func parseFlags(config interface{}) {
err := parser.ParseFlags()

// If a config file is found, read it in.
if err := viper.ReadInConfig(); err == nil {
log.Info().Msgf("Using config file: %s", viper.ConfigFileUsed())
if err != nil {
e := err.(*flags.Error)
if e.Type != flags.ErrCommandRequired && e.Type != flags.ErrHelp {
fmt.Printf("%s\n", e.Message)
}
parser.WriteHelp(os.Stdout)
}
}

func initLogging() {

if verbose {
if Application.Log.Verbose {
log.Logger = log.With().Caller().Logger()
}

if console {
if Application.Log.Format == "plain" {
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
}

Expand All @@ -89,7 +104,7 @@ func initLogging() {
zerolog.MessageFieldName = "msg"

zerolog.SetGlobalLevel(zerolog.InfoLevel)
if verbose {
if Application.Log.Verbose {
zerolog.SetGlobalLevel(zerolog.DebugLevel)
}
}
110 changes: 85 additions & 25 deletions cmd/serve.go
Original file line number Diff line number Diff line change
@@ -1,39 +1,99 @@
package cmd

import (
"github.com/charmixer/golang-api-template/pkg/serve"
"github.com/spf13/cobra"
"context"
"fmt"
"net/http"
"os"
"os/signal"
"time"

"github.com/charmixer/golang-api-template/middleware"
"github.com/charmixer/golang-api-template/router"
"github.com/charmixer/golang-api-template/app"

"github.com/charmixer/oas/exporter"

"github.com/rs/zerolog/log"
)

// serveCmd represents the serve command
var serveCmd = &cobra.Command{
Use: "serve",
Short: "Serve endpoints",
Long: `Serve endpoints`,
Run: serve.RunServe(),
type ServeCmd struct {
Public struct {
Port int `short:"p" long:"port" description:"Port to serve app on" default:"8080"`
Ip string `short:"i" long:"ip" description:"IP to serve app on" default:"0.0.0.0"`
Domain string `short:"d" long:"domain" description:"Domain to access app through" default:"127.0.0.1"`
}
Timeout struct {
Write int `long:"write-timeout" description:"Timeout in seconds for write" default:"10"`
Read int `long:"read-timeout" description:"Timeout in seconds for read" default:"5"`
ReadHeader int `long:"read-header-timeout" description:"Timeout in seconds for read-header" default:"5"`
Idle int `long:"idle-timeout" description:"Timeout in seconds for idle" default:"10"`
Grace int `long:"grace-timeout" description:"Timeout in seconds before shutting down" default:"15"`
}
TLS struct {
Cert struct {
Path string
}
Key struct {
Path string
}
}
}

func init() {
func (cmd *ServeCmd) Execute(args []string) error {
app.Env.Ip = cmd.Public.Ip
app.Env.Port = cmd.Public.Port
app.Env.Domain = cmd.Public.Domain
app.Env.Addr = fmt.Sprintf("%s:%d", app.Env.Ip, app.Env.Port)

oas := router.NewOas()
oasModel := exporter.ToOasModel(oas)
spec, err := exporter.ToYaml(oasModel)
if err != nil {
log.Error().Err(err)
}
app.Env.OpenAPI = spec

chain := middleware.GetChain()
route := router.NewRouter(oas)

// Here you will define your flags and configuration settings.
srv := &http.Server{
Addr: app.Env.Addr,
// Good practice to set timeouts to avoid Slowloris attacks.
WriteTimeout: time.Second * time.Duration(cmd.Timeout.Write),
ReadTimeout: time.Second * time.Duration(cmd.Timeout.Read),
ReadHeaderTimeout: time.Second * time.Duration(cmd.Timeout.ReadHeader),
IdleTimeout: time.Second * time.Duration(cmd.Timeout.Idle),
Handler: chain.Then(route), // Pass our instance of gorilla/mux in.
}

// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// serveCmd.PersistentFlags().String("foo", "", "A help for foo")
// Run our server in a goroutine so that it doesn't block.
go func() {
log.Info().Msg("Listening on " + app.Env.Addr)
if err := srv.ListenAndServe(); err != nil {
log.Error().Err(err)
}
}()

serveCmd.Flags().IntP("port", "p", 8080, "The port used for serving the api.")
serveCmd.Flags().StringP("ip", "i", "0.0.0.0", "The ip used for serving the api.")
serveCmd.Flags().StringP("domain", "d", "localhost", "The domain used to access the api.")
c := make(chan os.Signal, 1)
// We'll accept graceful shutdowns when quit via SIGINT (Ctrl+C)
// SIGKILL, SIGQUIT or SIGTERM (Ctrl+/) will not be caught.
signal.Notify(c, os.Interrupt)

serveCmd.Flags().IntP("write-timeout", "", 10, "Timeout in seconds when writing response.")
serveCmd.Flags().IntP("read-timeout", "", 10, "Timeout in seconds when reading request headers and body.")
serveCmd.Flags().IntP("read-header-timeout", "", 5, "Timeout in seconds when reading request headers.")
serveCmd.Flags().IntP("idle-timeout", "", 15, "Timeout in seconds between requests when keep-alive is enabled. If 0 read-timeout is used.")
serveCmd.Flags().IntP("graceful-timeout", "", 15, "Timeout in seconds when shutting down.")
// Block until we receive our signal.
<-c

rootCmd.AddCommand(serveCmd)
// Create a deadline to wait for.
ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(cmd.Timeout.Idle))
defer cancel()
// Doesn't block if no connections, but will otherwise wait
// until the timeout deadline.
srv.Shutdown(ctx)
// Optionally, you could run srv.Shutdown in a goroutine and block on
// <-ctx.Done() if your application should wait for other services
// to finalize based on context cancellation.
log.Info().Msg("shutting down")
os.Exit(0)

// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// serveCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
return nil
}
14 changes: 4 additions & 10 deletions endpoints/metrics/metrics.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,12 @@
package metrics

import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"net/http"
)

type GetMetricsRequest struct {

}
type GetMetricsResponse struct {
cpu_usage int
mem_usage int
}


func GetMetrics(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("get /metrics\n"))
t := promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{}).(http.HandlerFunc)
t.ServeHTTP(w, r)
}
Loading