diff --git a/.bra.toml b/.bra.toml new file mode 100644 index 00000000000..1fb047534e5 --- /dev/null +++ b/.bra.toml @@ -0,0 +1,14 @@ +[run] +init_cmds = [["./grafana-pro", "web"]] +watch_all = true +watch_dirs = [ + "$WORKDIR/pkg", + "$WORKDIR/views", +] +watch_exts = [".go", ".ini"] +build_delay = 1500 +cmds = [ + ["go", "install"], + ["go", "build"], + ["./grafana-pro", "web"] +] diff --git a/conf/grafana.ini b/conf/grafana.ini new file mode 100644 index 00000000000..322fc720698 --- /dev/null +++ b/conf/grafana.ini @@ -0,0 +1,41 @@ +app_name = Grafana Pro Server +app_mode = dev + +[server] +protocol = http +domain = localhost +root_url = %(protocol)s://%(domain)s:%(http_port)s/ +http_addr = +http_port = 3000 +ssh_port = 22 +route_log = true + +[log] +root_path = +; Either "console", "file", "conn", "smtp" or "database", default is "console" +; Use comma to separate multiple modes, e.g. "console, file" +mode = console +; Buffer length of channel, keep it as it is if you don't know what it is. +buffer_len = 10000 +; Either "Trace", "Debug", "Info", "Warn", "Error", "Critical", default is "Trace" +level = Trace + +; For "console" mode only +[log.console] +level = + +; For "file" mode only +[log.file] +level = +; This enables automated log rotate(switch of following options), default is true +log_rotate = true +; Max line number of single file, default is 1000000 +max_lines = 1000000 +; Max size shift of single file, default is 28 means 1 << 28, 256MB +max_lines_shift = 28 +; Segment log daily, default is true +daily_rotate = true +; Expired days of log file(delete after max days), default is 7 +max_days = 7 + + diff --git a/grafana-pro b/grafana-pro index 0a717ff5f02..85ace1a1cf6 100755 Binary files a/grafana-pro and b/grafana-pro differ diff --git a/grafana.go b/grafana.go index 31c46469a2f..1d2a7585b3e 100644 --- a/grafana.go +++ b/grafana.go @@ -2,32 +2,26 @@ package main import ( "os" - "time" + "runtime" - log "github.com/alecthomas/log4go" - "github.com/torkelo/grafana-pro/pkg/configuration" - "github.com/torkelo/grafana-pro/pkg/server" + "github.com/codegangsta/cli" + "github.com/torkelo/grafana-pro/pkg/cmd" ) -func main() { - port := os.Getenv("PORT") - if port == "" { - port = "3838" - } +const APP_VER = "0.1.0 Alpha" - log.Info("Starting Grafana-Pro v.1-alpha") - - cfg := configuration.NewCfg(port) - server, err := server.NewServer(cfg) - if err != nil { - time.Sleep(time.Second) - panic(err) - } - - err = server.ListenAndServe() - if err != nil { - log.Error("ListenAndServe failed: ", err) - } - - time.Sleep(time.Millisecond * 2000) +func init() { + runtime.GOMAXPROCS(runtime.NumCPU()) +} + +func main() { + app := cli.NewApp() + app.Name = "Grafana Pro" + app.Usage = "Grafana Pro Service" + app.Version = APP_VER + app.Commands = []cli.Command{ + cmd.CmdWeb, + } + app.Flags = append(app.Flags, []cli.Flag{}...) + app.Run(os.Args) } diff --git a/pkg/api/api.go b/pkg/api/api.go index cbe06fdb071..bdc44e0c94b 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -1,14 +1,17 @@ package api import ( + "fmt" "html/template" - log "github.com/alecthomas/log4go" "github.com/gin-gonic/gin" "github.com/gorilla/sessions" + "github.com/torkelo/grafana-pro/pkg/components" "github.com/torkelo/grafana-pro/pkg/configuration" + "github.com/torkelo/grafana-pro/pkg/log" "github.com/torkelo/grafana-pro/pkg/models" + "github.com/torkelo/grafana-pro/pkg/setting" "github.com/torkelo/grafana-pro/pkg/stores" ) @@ -34,9 +37,9 @@ func NewHttpServer(cfg *configuration.Cfg, store stores.Store) *HttpServer { } func (self *HttpServer) ListenAndServe() { - log.Info("Starting Http Listener on port %v", self.port) defer func() { self.shutdown <- true }() + gin.SetMode(gin.ReleaseMode) self.router = gin.New() self.router.Use(gin.Recovery(), apiLogger(), CacheHeadersMiddleware()) @@ -60,7 +63,9 @@ func (self *HttpServer) ListenAndServe() { self.router.GET("/admin/*_", self.auth(), self.index) self.router.GET("/account/*_", self.auth(), self.index) - self.router.Run(":" + self.port) + listenAddr := fmt.Sprintf("%s:%s", setting.HttpAddr, setting.HttpPort) + log.Info("Listen: %v://%s%s", setting.Protocol, listenAddr, setting.AppSubUrl) + self.router.Run(listenAddr) } func (self *HttpServer) index(c *gin.Context) { diff --git a/pkg/api/api_logger.go b/pkg/api/api_logger.go index 8c9a7251774..c889bbba999 100644 --- a/pkg/api/api_logger.go +++ b/pkg/api/api_logger.go @@ -1,12 +1,12 @@ package api import ( - "log" - "os" "strings" "time" "github.com/gin-gonic/gin" + + "github.com/torkelo/grafana-pro/pkg/log" ) var ( @@ -25,8 +25,6 @@ func ignoreLoggingRequest(code int, contentType string) bool { } func apiLogger() gin.HandlerFunc { - stdlogger := log.New(os.Stdout, "", 0) - return func(c *gin.Context) { // Start timer start := time.Now() @@ -53,26 +51,13 @@ func apiLogger() gin.HandlerFunc { requester = c.Request.RemoteAddr } - var color string - switch { - case code >= 200 && code <= 299: - color = green - case code >= 300 && code <= 399: - color = white - case code >= 400 && code <= 499: - color = yellow - default: - color = red - } - end := time.Now() latency := end.Sub(start) - stdlogger.Printf("[GIN] %v |%s %3d %s| %12v | %s %4s %s\n%s", - end.Format("2006/01/02 - 15:04:05"), - color, code, reset, + log.Info("[http] %s %s %3d %12v %s %s", + c.Request.Method, c.Request.URL.Path, + code, latency, requester, - c.Request.Method, c.Request.URL.Path, c.Errors.String(), ) } diff --git a/pkg/cmd/web.go b/pkg/cmd/web.go new file mode 100644 index 00000000000..d5f1c3f6912 --- /dev/null +++ b/pkg/cmd/web.go @@ -0,0 +1,45 @@ +package cmd + +import ( + "os" + "time" + + "github.com/codegangsta/cli" + "github.com/siddontang/go-log/log" + "github.com/torkelo/grafana-pro/pkg/configuration" + "github.com/torkelo/grafana-pro/pkg/routes" + "github.com/torkelo/grafana-pro/pkg/server" +) + +var CmdWeb = cli.Command{ + Name: "web", + Usage: "Start Grafana Pro web server", + Description: `Start Grafana Pro server`, + Action: runWeb, + Flags: []cli.Flag{}, +} + +func runWeb(*cli.Context) { + routes.GlobalInit() + port := os.Getenv("PORT") + if port == "" { + port = "3838" + } + + log.Info("Starting Grafana-Pro v.1-alpha") + + cfg := configuration.NewCfg(port) + server, err := server.NewServer(cfg) + if err != nil { + time.Sleep(time.Second) + panic(err) + } + + err = server.ListenAndServe() + if err != nil { + log.Error("ListenAndServe failed: ", err) + } + + time.Sleep(time.Millisecond * 2000) + +} diff --git a/pkg/log/console.go b/pkg/log/console.go new file mode 100644 index 00000000000..f5a8b96fd76 --- /dev/null +++ b/pkg/log/console.go @@ -0,0 +1,73 @@ +// Copyright 2014 The Gogs Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package log + +import ( + "encoding/json" + "log" + "os" + "runtime" +) + +type Brush func(string) string + +func NewBrush(color string) Brush { + pre := "\033[" + reset := "\033[0m" + return func(text string) string { + return pre + color + "m" + text + reset + } +} + +var colors = []Brush{ + NewBrush("1;36"), // Trace cyan + NewBrush("1;34"), // Debug blue + NewBrush("1;32"), // Info green + NewBrush("1;33"), // Warn yellow + NewBrush("1;31"), // Error red + NewBrush("1;35"), // Critical purple + NewBrush("1;31"), // Fatal red +} + +// ConsoleWriter implements LoggerInterface and writes messages to terminal. +type ConsoleWriter struct { + lg *log.Logger + Level int `json:"level"` +} + +// create ConsoleWriter returning as LoggerInterface. +func NewConsole() LoggerInterface { + return &ConsoleWriter{ + lg: log.New(os.Stdout, "", log.Ldate|log.Ltime), + Level: TRACE, + } +} + +func (cw *ConsoleWriter) Init(config string) error { + return json.Unmarshal([]byte(config), cw) +} + +func (cw *ConsoleWriter) WriteMsg(msg string, skip, level int) error { + if cw.Level > level { + return nil + } + if runtime.GOOS == "windows" { + cw.lg.Println(msg) + } else { + cw.lg.Println(colors[level](msg)) + } + return nil +} + +func (_ *ConsoleWriter) Flush() { + +} + +func (_ *ConsoleWriter) Destroy() { +} + +func init() { + Register("console", NewConsole) +} diff --git a/pkg/log/log.go b/pkg/log/log.go new file mode 100644 index 00000000000..d2e31cffb64 --- /dev/null +++ b/pkg/log/log.go @@ -0,0 +1,299 @@ +// Copyright 2014 The Gogs Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package log + +import ( + "fmt" + "os" + "path/filepath" + "runtime" + "strings" + "sync" +) + +var ( + loggers []*Logger +) + +func NewLogger(bufLen int64, mode, config string) { + logger := newLogger(bufLen) + + isExist := false + for _, l := range loggers { + if l.adapter == mode { + isExist = true + l = logger + } + } + if !isExist { + loggers = append(loggers, logger) + } + if err := logger.SetLogger(mode, config); err != nil { + Fatal(1, "Fail to set logger(%s): %v", mode, err) + } +} + +func Trace(format string, v ...interface{}) { + for _, logger := range loggers { + logger.Trace(format, v...) + } +} + +func Debug(format string, v ...interface{}) { + for _, logger := range loggers { + logger.Debug(format, v...) + } +} + +func Info(format string, v ...interface{}) { + for _, logger := range loggers { + logger.Info(format, v...) + } +} + +func Warn(format string, v ...interface{}) { + for _, logger := range loggers { + logger.Warn(format, v...) + } +} + +func Error(skip int, format string, v ...interface{}) { + for _, logger := range loggers { + logger.Error(skip, format, v...) + } +} + +func Critical(skip int, format string, v ...interface{}) { + for _, logger := range loggers { + logger.Critical(skip, format, v...) + } +} + +func Fatal(skip int, format string, v ...interface{}) { + Error(skip, format, v...) + for _, l := range loggers { + l.Close() + } + os.Exit(1) +} + +func Close() { + for _, l := range loggers { + l.Close() + } +} + +// .___ __ _____ +// | | _____/ |_ ____________/ ____\____ ____ ____ +// | |/ \ __\/ __ \_ __ \ __\\__ \ _/ ___\/ __ \ +// | | | \ | \ ___/| | \/| | / __ \\ \__\ ___/ +// |___|___| /__| \___ >__| |__| (____ /\___ >___ > +// \/ \/ \/ \/ \/ + +type LogLevel int + +const ( + TRACE = iota + DEBUG + INFO + WARN + ERROR + CRITICAL + FATAL +) + +// LoggerInterface represents behaviors of a logger provider. +type LoggerInterface interface { + Init(config string) error + WriteMsg(msg string, skip, level int) error + Destroy() + Flush() +} + +type loggerType func() LoggerInterface + +var adapters = make(map[string]loggerType) + +// Register registers given logger provider to adapters. +func Register(name string, log loggerType) { + if log == nil { + panic("log: register provider is nil") + } + if _, dup := adapters[name]; dup { + panic("log: register called twice for provider \"" + name + "\"") + } + adapters[name] = log +} + +type logMsg struct { + skip, level int + msg string +} + +// Logger is default logger in beego application. +// it can contain several providers and log message into all providers. +type Logger struct { + adapter string + lock sync.Mutex + level int + msg chan *logMsg + outputs map[string]LoggerInterface + quit chan bool +} + +// newLogger initializes and returns a new logger. +func newLogger(buffer int64) *Logger { + l := &Logger{ + msg: make(chan *logMsg, buffer), + outputs: make(map[string]LoggerInterface), + quit: make(chan bool), + } + go l.StartLogger() + return l +} + +// SetLogger sets new logger instanse with given logger adapter and config. +func (l *Logger) SetLogger(adapter string, config string) error { + l.lock.Lock() + defer l.lock.Unlock() + if log, ok := adapters[adapter]; ok { + lg := log() + if err := lg.Init(config); err != nil { + return err + } + l.outputs[adapter] = lg + l.adapter = adapter + } else { + panic("log: unknown adapter \"" + adapter + "\" (forgotten register?)") + } + return nil +} + +// DelLogger removes a logger adapter instance. +func (l *Logger) DelLogger(adapter string) error { + l.lock.Lock() + defer l.lock.Unlock() + if lg, ok := l.outputs[adapter]; ok { + lg.Destroy() + delete(l.outputs, adapter) + } else { + panic("log: unknown adapter \"" + adapter + "\" (forgotten register?)") + } + return nil +} + +func (l *Logger) writerMsg(skip, level int, msg string) error { + if l.level > level { + return nil + } + lm := &logMsg{ + skip: skip, + level: level, + } + + // Only error information needs locate position for debugging. + if lm.level >= ERROR { + pc, file, line, ok := runtime.Caller(skip) + if ok { + // Get caller function name. + fn := runtime.FuncForPC(pc) + var fnName string + if fn == nil { + fnName = "?()" + } else { + fnName = strings.TrimLeft(filepath.Ext(fn.Name()), ".") + "()" + } + + lm.msg = fmt.Sprintf("[%s:%d %s] %s", filepath.Base(file), line, fnName, msg) + } else { + lm.msg = msg + } + } else { + lm.msg = msg + } + l.msg <- lm + return nil +} + +// StartLogger starts logger chan reading. +func (l *Logger) StartLogger() { + for { + select { + case bm := <-l.msg: + for _, l := range l.outputs { + if err := l.WriteMsg(bm.msg, bm.skip, bm.level); err != nil { + fmt.Println("ERROR, unable to WriteMsg:", err) + } + } + case <-l.quit: + return + } + } +} + +// Flush flushs all chan data. +func (l *Logger) Flush() { + for _, l := range l.outputs { + l.Flush() + } +} + +// Close closes logger, flush all chan data and destroy all adapter instances. +func (l *Logger) Close() { + l.quit <- true + for { + if len(l.msg) > 0 { + bm := <-l.msg + for _, l := range l.outputs { + if err := l.WriteMsg(bm.msg, bm.skip, bm.level); err != nil { + fmt.Println("ERROR, unable to WriteMsg:", err) + } + } + } else { + break + } + } + for _, l := range l.outputs { + l.Flush() + l.Destroy() + } +} + +func (l *Logger) Trace(format string, v ...interface{}) { + msg := fmt.Sprintf("[T] "+format, v...) + l.writerMsg(0, TRACE, msg) +} + +func (l *Logger) Debug(format string, v ...interface{}) { + msg := fmt.Sprintf("[D] "+format, v...) + l.writerMsg(0, DEBUG, msg) +} + +func (l *Logger) Info(format string, v ...interface{}) { + msg := fmt.Sprintf("[I] "+format, v...) + l.writerMsg(0, INFO, msg) +} + +func (l *Logger) Warn(format string, v ...interface{}) { + msg := fmt.Sprintf("[W] "+format, v...) + l.writerMsg(0, WARN, msg) +} + +func (l *Logger) Error(skip int, format string, v ...interface{}) { + msg := fmt.Sprintf("[E] "+format, v...) + l.writerMsg(skip, ERROR, msg) +} + +func (l *Logger) Critical(skip int, format string, v ...interface{}) { + msg := fmt.Sprintf("[C] "+format, v...) + l.writerMsg(skip, CRITICAL, msg) +} + +func (l *Logger) Fatal(skip int, format string, v ...interface{}) { + msg := fmt.Sprintf("[F] "+format, v...) + l.writerMsg(skip, FATAL, msg) + l.Close() + os.Exit(1) +} diff --git a/pkg/routes/routes.go b/pkg/routes/routes.go new file mode 100644 index 00000000000..db9eb4de0e4 --- /dev/null +++ b/pkg/routes/routes.go @@ -0,0 +1,7 @@ +package routes + +import "github.com/torkelo/grafana-pro/pkg/setting" + +func GlobalInit() { + setting.NewConfigContext() +} diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go new file mode 100644 index 00000000000..721b84d10dc --- /dev/null +++ b/pkg/setting/setting.go @@ -0,0 +1,125 @@ +package setting + +import ( + "net/url" + "os" + "os/exec" + "path" + "path/filepath" + "runtime" + "strings" + + "github.com/Unknwon/com" + "github.com/Unknwon/goconfig" + "github.com/torkelo/grafana-pro/pkg/log" +) + +type Scheme string + +const ( + HTTP Scheme = "http" + HTTPS Scheme = "https" +) + +var ( + // App settings. + AppVer string + AppName string + AppUrl string + AppSubUrl string + + // Log settings. + LogRootPath string + LogModes []string + LogConfigs []string + + // Http server options + Protocol Scheme + Domain string + HttpAddr, HttpPort string + SshPort int + CertFile, KeyFile string + DisableRouterLog bool + + // Global setting objects. + Cfg *goconfig.ConfigFile + ConfRootPath string + CustomPath string // Custom directory path. + ProdMode bool + RunUser string + IsWindows bool +) + +func init() { + IsWindows = runtime.GOOS == "windows" + log.NewLogger(0, "console", `{"level": 0}`) +} + +// WorkDir returns absolute path of work directory. +func WorkDir() (string, error) { + execPath, err := ExecPath() + return path.Dir(strings.Replace(execPath, "\\", "/", -1)), err +} + +func ExecPath() (string, error) { + file, err := exec.LookPath(os.Args[0]) + if err != nil { + return "", err + } + p, err := filepath.Abs(file) + if err != nil { + return "", err + } + return p, nil +} + +func NewConfigContext() { + workDir, err := WorkDir() + if err != nil { + log.Fatal(4, "Fail to get work directory: %v", err) + } + ConfRootPath = path.Join(workDir, "conf") + + Cfg, err = goconfig.LoadConfigFile(path.Join(workDir, "conf/grafana.ini")) + if err != nil { + log.Fatal(4, "Fail to parse 'conf/grafana.ini': %v", err) + } + + CustomPath = os.Getenv("GRAFANA_CONF") + + if len(CustomPath) == 0 { + CustomPath = path.Join(workDir, "custom") + } + + cfgPath := path.Join(CustomPath, "conf/grafana.ini") + if com.IsFile(cfgPath) { + if err = Cfg.AppendFiles(cfgPath); err != nil { + log.Fatal(4, "Fail to load custom 'conf/grafana.ini': %v", err) + } + } else { + log.Warn("No custom 'conf/grafana.ini'") + } + + AppName = Cfg.MustValue("", "app_name", "Grafana Pro") + AppUrl = Cfg.MustValue("server", "root_url", "http://localhost:3000/") + if AppUrl[len(AppUrl)-1] != '/' { + AppUrl += "/" + } + + // Check if has app suburl. + url, err := url.Parse(AppUrl) + if err != nil { + log.Fatal(4, "Invalid root_url(%s): %s", AppUrl, err) + } + AppSubUrl = strings.TrimSuffix(url.Path, "/") + + Protocol = HTTP + if Cfg.MustValue("server", "protocol") == "https" { + Protocol = HTTPS + CertFile = Cfg.MustValue("server", "cert_file") + KeyFile = Cfg.MustValue("server", "key_file") + } + Domain = Cfg.MustValue("server", "domain", "localhost") + HttpAddr = Cfg.MustValue("server", "http_addr", "0.0.0.0") + HttpPort = Cfg.MustValue("server", "http_port", "3000") +} diff --git a/pkg/stores/rethinkdb.go b/pkg/stores/rethinkdb.go index 834597a742d..eb9a016c2a9 100644 --- a/pkg/stores/rethinkdb.go +++ b/pkg/stores/rethinkdb.go @@ -3,8 +3,9 @@ package stores import ( "time" - log "github.com/alecthomas/log4go" r "github.com/dancannon/gorethink" + + "github.com/torkelo/grafana-pro/pkg/log" ) type rethinkStore struct { @@ -31,7 +32,7 @@ func NewRethinkStore(config *RethinkCfg) *rethinkStore { }) if err != nil { - log.Crash("Failed to connect to rethink database %v", err) + log.Error(3, "Failed to connect to rethink database %v", err) } createRethinkDBTablesAndIndices(config, session)