diff --git a/grafana b/grafana index 34ab1e529b4..06de80f2a76 160000 --- a/grafana +++ b/grafana @@ -1 +1 @@ -Subproject commit 34ab1e529b499af836631f8076c2c4df02be5860 +Subproject commit 06de80f2a764b76df6047fc4700c74aaf5734521 diff --git a/install_dependencies.sh b/install_dependencies.sh new file mode 100644 index 00000000000..eafcee62380 --- /dev/null +++ b/install_dependencies.sh @@ -0,0 +1,2 @@ +go get code.google.com/p/goprotobuf/{proto,protoc-gen-go} + diff --git a/pkg/api/api_dashboard.go b/pkg/api/api_dashboard.go index 60ecdc8189e..5e2ca7c96d1 100644 --- a/pkg/api/api_dashboard.go +++ b/pkg/api/api_dashboard.go @@ -1,6 +1,7 @@ package api import ( + log "github.com/alecthomas/log4go" "github.com/gin-gonic/gin" "github.com/torkelo/grafana-pro/pkg/models" ) @@ -16,7 +17,7 @@ func init() { func (self *HttpServer) getDashboard(c *gin.Context) { id := c.Params.ByName("id") - dash, err := self.store.GetById(id) + dash, err := self.store.GetDashboardByTitle(id, "test") if err != nil { c.JSON(404, newErrorResponse("Dashboard not found")) return @@ -30,6 +31,7 @@ func (self *HttpServer) search(c *gin.Context) { results, err := self.store.Query(query) if err != nil { + log.Error("Store query error: %v", err) c.JSON(500, newErrorResponse("Failed")) return } @@ -41,9 +43,17 @@ func (self *HttpServer) postDashboard(c *gin.Context) { var command saveDashboardCommand if c.EnsureBody(&command) { - err := self.store.Save(&models.Dashboard{Data: command.Dashboard}) + dashboard := models.NewDashboard("test") + dashboard.Data = command.Dashboard + dashboard.Title = dashboard.Data["title"].(string) + + if dashboard.Data["id"] != nil { + dashboard.Id = dashboard.Data["id"].(string) + } + + err := self.store.SaveDashboard(dashboard) if err == nil { - c.JSON(200, gin.H{"status": "saved"}) + c.JSON(200, gin.H{"status": "success", "id": dashboard.Id}) return } } diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index 9f333aff8dc..82ce602c136 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -3,20 +3,39 @@ package models import ( "encoding/json" "io" + "time" ) type Dashboard struct { - Data map[string]interface{} + Id string `gorethink:"id,omitempty"` + AccountId string + LastModifiedByUserId string + LastModifiedByDate time.Time + CreatedDate time.Time + + Title string + Tags []string + Data map[string]interface{} +} + +type UserContext struct { + UserId string + AccountId string } type SearchResult struct { - Type string `json:"title"` Id string `json:"id"` Title string `json:"title"` } func NewDashboard(title string) *Dashboard { dash := &Dashboard{} + dash.Id = "" + dash.AccountId = "test" + dash.LastModifiedByDate = time.Now() + dash.CreatedDate = time.Now() + dash.LastModifiedByUserId = "123" + dash.Title = title dash.Data = make(map[string]interface{}) dash.Data["title"] = title @@ -31,23 +50,11 @@ func NewFromJson(reader io.Reader) (*Dashboard, error) { return nil, err } + dash.Title = dash.Data["title"].(string) + return dash, nil } -/*type DashboardServices struct { -} - -type DashboardServicesFilter struct { -} - -type DashboardServicesFilterTime struct { - From string To string -}*/ - func (dash *Dashboard) GetString(prop string) string { return dash.Data[prop].(string) } - -func (dash *Dashboard) Title() string { - return dash.GetString("title") -} diff --git a/pkg/stores/file_store.go b/pkg/stores/file_store.go index 6b9765e6473..bf1c3f4f77e 100644 --- a/pkg/stores/file_store.go +++ b/pkg/stores/file_store.go @@ -1,155 +1,156 @@ package stores -import ( - "encoding/json" - "io" - "os" - "path/filepath" - "strings" - - log "github.com/alecthomas/log4go" - "github.com/torkelo/grafana-pro/pkg/models" -) - -type fileStore struct { - dataDir string - dashDir string - cache map[string]*models.Dashboard -} - -func NewFileStore(dataDir string) *fileStore { - - if dirDoesNotExist(dataDir) { - log.Crashf("FileStore failed to initialize, dataDir does not exist %v", dataDir) - } - - dashDir := filepath.Join(dataDir, "dashboards") - - if dirDoesNotExist(dashDir) { - log.Debug("Did not find dashboard dir, creating...") - err := os.Mkdir(dashDir, 0777) - if err != nil { - log.Crashf("FileStore failed to initialize, could not create directory %v, error: %v", dashDir, err) - } - } - - store := &fileStore{} - store.dataDir = dataDir - store.dashDir = dashDir - store.cache = make(map[string]*models.Dashboard) - go store.scanFiles() - - return store -} - -func (store *fileStore) scanFiles() { - visitor := func(path string, f os.FileInfo, err error) error { - if err != nil { - return err - } - if f.IsDir() { - return nil - } - if strings.HasSuffix(f.Name(), ".json") { - err = store.loadDashboardIntoCache(path) - if err != nil { - return err - } - } - return nil - } - - err := filepath.Walk(store.dashDir, visitor) - if err != nil { - log.Error("FileStore::updateCache failed %v", err) - } -} - -func (store fileStore) loadDashboardIntoCache(filename string) error { - log.Info("Loading dashboard file %v into cache", filename) - dash, err := loadDashboardFromFile(filename) - if err != nil { - return err - } - - store.cache[dash.Title()] = dash - - return nil -} - -func (store *fileStore) Close() { - -} - -func (store *fileStore) GetById(id string) (*models.Dashboard, error) { - log.Debug("FileStore::GetById id = %v", id) - filename := store.getFilePathForDashboard(id) - - return loadDashboardFromFile(filename) -} - -func (store *fileStore) Save(dash *models.Dashboard) error { - filename := store.getFilePathForDashboard(dash.Title()) - - log.Debug("Saving dashboard %v to %v", dash.Title(), filename) - - var err error - var data []byte - if data, err = json.Marshal(dash.Data); err != nil { - return err - } - - return writeFile(filename, data) -} - -func (store *fileStore) Query(query string) ([]*models.SearchResult, error) { - results := make([]*models.SearchResult, 0, 50) - - for _, dash := range store.cache { - item := &models.SearchResult{ - Id: dash.Title(), - Type: "dashboard", - } - results = append(results, item) - } - - return results, nil -} - -func loadDashboardFromFile(filename string) (*models.Dashboard, error) { - log.Debug("FileStore::loading dashboard from file %v", filename) - - configFile, err := os.Open(filename) - if err != nil { - return nil, err - } - - return models.NewFromJson(configFile) -} - -func (store *fileStore) getFilePathForDashboard(id string) string { - id = strings.ToLower(id) - id = strings.Replace(id, " ", "-", -1) - return filepath.Join(store.dashDir, id) + ".json" -} - -func dirDoesNotExist(dir string) bool { - _, err := os.Stat(dir) - return os.IsNotExist(err) -} - -func writeFile(filename string, data []byte) error { - f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) - if err != nil { - return err - } - n, err := f.Write(data) - if err == nil && n < len(data) { - err = io.ErrShortWrite - } - if err1 := f.Close(); err == nil { - err = err1 - } - - return err -} +// +// import ( +// "encoding/json" +// "io" +// "os" +// "path/filepath" +// "strings" +// +// log "github.com/alecthomas/log4go" +// "github.com/torkelo/grafana-pro/pkg/models" +// ) +// +// type fileStore struct { +// dataDir string +// dashDir string +// cache map[string]*models.Dashboard +// } +// +// func NewFileStore(dataDir string) *fileStore { +// +// if dirDoesNotExist(dataDir) { +// log.Crashf("FileStore failed to initialize, dataDir does not exist %v", dataDir) +// } +// +// dashDir := filepath.Join(dataDir, "dashboards") +// +// if dirDoesNotExist(dashDir) { +// log.Debug("Did not find dashboard dir, creating...") +// err := os.Mkdir(dashDir, 0777) +// if err != nil { +// log.Crashf("FileStore failed to initialize, could not create directory %v, error: %v", dashDir, err) +// } +// } +// +// store := &fileStore{} +// store.dataDir = dataDir +// store.dashDir = dashDir +// store.cache = make(map[string]*models.Dashboard) +// store.scanFiles() +// +// return store +// } +// +// func (store *fileStore) scanFiles() { +// visitor := func(path string, f os.FileInfo, err error) error { +// if err != nil { +// return err +// } +// if f.IsDir() { +// return nil +// } +// if strings.HasSuffix(f.Name(), ".json") { +// err = store.loadDashboardIntoCache(path) +// if err != nil { +// return err +// } +// } +// return nil +// } +// +// err := filepath.Walk(store.dashDir, visitor) +// if err != nil { +// log.Error("FileStore::updateCache failed %v", err) +// } +// } +// +// func (store fileStore) loadDashboardIntoCache(filename string) error { +// log.Info("Loading dashboard file %v into cache", filename) +// dash, err := loadDashboardFromFile(filename) +// if err != nil { +// return err +// } +// +// store.cache[dash.Title] = dash +// +// return nil +// } +// +// func (store *fileStore) Close() { +// +// } +// +// func (store *fileStore) GetById(id string) (*models.Dashboard, error) { +// log.Debug("FileStore::GetById id = %v", id) +// filename := store.getFilePathForDashboard(id) +// +// return loadDashboardFromFile(filename) +// } +// +// func (store *fileStore) Save(dash *models.Dashboard) error { +// filename := store.getFilePathForDashboard(dash.Title) +// +// log.Debug("Saving dashboard %v to %v", dash.Title, filename) +// +// var err error +// var data []byte +// if data, err = json.Marshal(dash.Data); err != nil { +// return err +// } +// +// return writeFile(filename, data) +// } +// +// func (store *fileStore) Query(query string) ([]*models.SearchResult, error) { +// results := make([]*models.SearchResult, 0, 50) +// +// for _, dash := range store.cache { +// item := &models.SearchResult{ +// Id: dash.Title, +// Type: "dashboard", +// } +// results = append(results, item) +// } +// +// return results, nil +// } +// +// func loadDashboardFromFile(filename string) (*models.Dashboard, error) { +// log.Debug("FileStore::loading dashboard from file %v", filename) +// +// configFile, err := os.Open(filename) +// if err != nil { +// return nil, err +// } +// +// return models.NewFromJson(configFile) +// } +// +// func (store *fileStore) getFilePathForDashboard(id string) string { +// id = strings.ToLower(id) +// id = strings.Replace(id, " ", "-", -1) +// return filepath.Join(store.dashDir, id) + ".json" +// } +// +// func dirDoesNotExist(dir string) bool { +// _, err := os.Stat(dir) +// return os.IsNotExist(err) +// } +// +// func writeFile(filename string, data []byte) error { +// f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) +// if err != nil { +// return err +// } +// n, err := f.Write(data) +// if err == nil && n < len(data) { +// err = io.ErrShortWrite +// } +// if err1 := f.Close(); err == nil { +// err = err1 +// } +// +// return err +// } diff --git a/pkg/stores/file_store_test.go b/pkg/stores/file_store_test.go index 77c0224c53e..aff4673c7f5 100644 --- a/pkg/stores/file_store_test.go +++ b/pkg/stores/file_store_test.go @@ -1,112 +1,113 @@ package stores -import ( - "fmt" - "io" - "io/ioutil" - "os" - "path/filepath" - "testing" - - . "github.com/smartystreets/goconvey/convey" - "github.com/torkelo/grafana-pro/pkg/models" -) - -func TestFileStore(t *testing.T) { - - GivenFileStore("When saving a dashboard", t, func(store *fileStore) { - dashboard := models.NewDashboard("hello") - - err := store.Save(dashboard) - - Convey("should be saved to disk", func() { - So(err, ShouldBeNil) - - _, err = os.Stat(store.getFilePathForDashboard("hello")) - So(err, ShouldBeNil) - }) - }) - - GivenFileStore("When getting a saved dashboard", t, func(store *fileStore) { - copyDashboardToTempData("default.json", "", store.dashDir) - dash, err := store.GetById("default") - - Convey("should be read from disk", func() { - So(err, ShouldBeNil) - So(dash, ShouldNotBeNil) - - So(dash.Title(), ShouldEqual, "Grafana Play Home") - }) - }) - - GivenFileStore("when getting dashboard with capital letters", t, func(store *fileStore) { - copyDashboardToTempData("annotations.json", "", store.dashDir) - dash, err := store.GetById("AnnoTations") - - Convey("should be read from disk", func() { - So(err, ShouldBeNil) - So(dash, ShouldNotBeNil) - - So(dash.Title(), ShouldEqual, "Annotations") - }) - }) - - GivenFileStore("When copying dashboards into data dir", t, func(store *fileStore) { - copyDashboardToTempData("annotations.json", "", store.dashDir) - copyDashboardToTempData("default.json", "", store.dashDir) - copyDashboardToTempData("graph-styles.json", "", store.dashDir) - store.scanFiles() - - Convey("scan should generate index of all dashboards", func() { - - result, err := store.Query("*") - So(err, ShouldBeNil) - So(len(result), ShouldEqual, 3) - }) - }) -} - -func copyDashboardToTempData(name string, destName string, dir string) { - if destName == "" { - destName = name - } - source, _ := filepath.Abs("../../data/dashboards/" + name) - dest := filepath.Join(dir, destName) - err := copyFile(dest, source) - if err != nil { - panic(fmt.Sprintf("failed to copy file %v", name)) - } -} - -func GivenFileStore(desc string, t *testing.T, f func(store *fileStore)) { - Convey(desc, t, func() { - tempDir, _ := ioutil.TempDir("", "store") - - store := NewFileStore(tempDir) - - f(store) - - Reset(func() { - os.RemoveAll(tempDir) - }) - }) -} - -func copyFile(dst, src string) error { - in, err := os.Open(src) - if err != nil { - return err - } - defer in.Close() - out, err := os.Create(dst) - if err != nil { - return err - } - defer out.Close() - _, err = io.Copy(out, in) - cerr := out.Close() - if err != nil { - return err - } - return cerr -} +// +// import ( +// "fmt" +// "io" +// "io/ioutil" +// "os" +// "path/filepath" +// "testing" +// +// . "github.com/smartystreets/goconvey/convey" +// "github.com/torkelo/grafana-pro/pkg/models" +// ) +// +// func TestFileStore(t *testing.T) { +// +// GivenFileStore("When saving a dashboard", t, func(store *fileStore) { +// dashboard := models.NewDashboard("hello") +// +// err := store.Save(dashboard) +// +// Convey("should be saved to disk", func() { +// So(err, ShouldBeNil) +// +// _, err = os.Stat(store.getFilePathForDashboard("hello")) +// So(err, ShouldBeNil) +// }) +// }) +// +// GivenFileStore("When getting a saved dashboard", t, func(store *fileStore) { +// copyDashboardToTempData("default.json", "", store.dashDir) +// dash, err := store.GetById("default") +// +// Convey("should be read from disk", func() { +// So(err, ShouldBeNil) +// So(dash, ShouldNotBeNil) +// +// So(dash.Title, ShouldEqual, "Grafana Play Home") +// }) +// }) +// +// GivenFileStore("when getting dashboard with capital letters", t, func(store *fileStore) { +// copyDashboardToTempData("annotations.json", "", store.dashDir) +// dash, err := store.GetById("AnnoTations") +// +// Convey("should be read from disk", func() { +// So(err, ShouldBeNil) +// So(dash, ShouldNotBeNil) +// +// So(dash.Title, ShouldEqual, "Annotations") +// }) +// }) +// +// GivenFileStore("When copying dashboards into data dir", t, func(store *fileStore) { +// copyDashboardToTempData("annotations.json", "", store.dashDir) +// copyDashboardToTempData("default.json", "", store.dashDir) +// copyDashboardToTempData("graph-styles.json", "", store.dashDir) +// store.scanFiles() +// +// Convey("scan should generate index of all dashboards", func() { +// +// result, err := store.Query("*") +// So(err, ShouldBeNil) +// So(len(result), ShouldEqual, 3) +// }) +// }) +// } +// +// func copyDashboardToTempData(name string, destName string, dir string) { +// if destName == "" { +// destName = name +// } +// source, _ := filepath.Abs("../../data/dashboards/" + name) +// dest := filepath.Join(dir, destName) +// err := copyFile(dest, source) +// if err != nil { +// panic(fmt.Sprintf("failed to copy file %v", name)) +// } +// } +// +// func GivenFileStore(desc string, t *testing.T, f func(store *fileStore)) { +// Convey(desc, t, func() { +// tempDir, _ := ioutil.TempDir("", "store") +// +// store := NewFileStore(tempDir) +// +// f(store) +// +// Reset(func() { +// os.RemoveAll(tempDir) +// }) +// }) +// } +// +// func copyFile(dst, src string) error { +// in, err := os.Open(src) +// if err != nil { +// return err +// } +// defer in.Close() +// out, err := os.Create(dst) +// if err != nil { +// return err +// } +// defer out.Close() +// _, err = io.Copy(out, in) +// cerr := out.Close() +// if err != nil { +// return err +// } +// return cerr +// } diff --git a/pkg/stores/rethinkdb.go b/pkg/stores/rethinkdb.go new file mode 100644 index 00000000000..ad0f277501f --- /dev/null +++ b/pkg/stores/rethinkdb.go @@ -0,0 +1,94 @@ +package stores + +import ( + "time" + + log "github.com/alecthomas/log4go" + r "github.com/dancannon/gorethink" + "github.com/torkelo/grafana-pro/pkg/models" +) + +type rethinkStore struct { + session *r.Session +} + +type RethinkCfg struct { + DatabaseName string +} + +func NewRethinkStore(config *RethinkCfg) *rethinkStore { + log.Info("Initializing rethink storage") + + session, err := r.Connect(r.ConnectOpts{ + Address: "localhost:28015", + Database: config.DatabaseName, + MaxIdle: 10, + IdleTimeout: time.Second * 10, + }) + + if err != nil { + log.Crash("Failed to connect to rethink database %v", err) + } + + r.DbCreate(config.DatabaseName).Exec(session) + r.Db(config.DatabaseName).TableCreate("dashboards").Exec(session) + r.Db(config.DatabaseName).Table("dashboards").IndexCreateFunc("AccountIdTitle", func(row r.Term) interface{} { + return []interface{}{row.Field("AccountId"), row.Field("Title")} + }).Exec(session) + + return &rethinkStore{ + session: session, + } +} + +func (self *rethinkStore) SaveDashboard(dash *models.Dashboard) error { + resp, err := r.Table("dashboards").Insert(dash).RunWrite(self.session) + if err != nil { + return err + } + + log.Info("Inserted: %v, Errors: %v, Updated: %v", resp.Inserted, resp.Errors, resp.Updated) + log.Info("First error:", resp.FirstError) + if len(resp.GeneratedKeys) > 0 { + dash.Id = resp.GeneratedKeys[0] + } + + return nil +} + +func (self *rethinkStore) GetDashboardByTitle(title string, accountId string) (*models.Dashboard, error) { + resp, err := r.Table("dashboards").GetAllByIndex("AccountIdTitle", []interface{}{accountId, title}).Run(self.session) + if err != nil { + return nil, err + } + + var dashboard models.Dashboard + err = resp.One(&dashboard) + if err != nil { + return nil, err + } + + return &dashboard, nil +} + +func (self *rethinkStore) Query(query string) ([]*models.SearchResult, error) { + + docs, err := r.Table("dashboards").Filter(r.Row.Field("Title").Match(".*")).Run(self.session) + if err != nil { + return nil, err + } + + results := make([]*models.SearchResult, 0, 50) + var dashboard models.Dashboard + for docs.Next(&dashboard) { + log.Info("title: ", dashboard.Title) + results = append(results, &models.SearchResult{ + Title: dashboard.Title, + Id: dashboard.Id, + }) + } + + return results, nil +} + +func (self *rethinkStore) Close() {} diff --git a/pkg/stores/rethinkdb_test.go b/pkg/stores/rethinkdb_test.go new file mode 100644 index 00000000000..aa6e7d99684 --- /dev/null +++ b/pkg/stores/rethinkdb_test.go @@ -0,0 +1,29 @@ +package stores + +import ( + "testing" + + . "github.com/smartystreets/goconvey/convey" + "github.com/torkelo/grafana-pro/pkg/models" +) + +func TestRethinkStore(t *testing.T) { + + Convey("Insert dashboard", t, func() { + store := NewRethinkStore(&RethinkCfg{DatabaseName: "tests"}) + //defer r.DbDrop("tests").Exec(store.session) + + dashboard := models.NewDashboard("test") + dashboard.AccountId = "123" + + err := store.SaveDashboard(dashboard) + So(err, ShouldBeNil) + So(dashboard.Id, ShouldNotBeEmpty) + + read, err := store.GetDashboardByTitle("test", "123") + So(err, ShouldBeNil) + So(read, ShouldNotBeNil) + + }) + +} diff --git a/pkg/stores/store.go b/pkg/stores/store.go index 8ce505c420e..63fb3ca4e29 100644 --- a/pkg/stores/store.go +++ b/pkg/stores/store.go @@ -5,12 +5,12 @@ import ( ) type Store interface { - GetById(id string) (*models.Dashboard, error) - Save(dash *models.Dashboard) error + GetDashboardByTitle(id string, accountId string) (*models.Dashboard, error) + SaveDashboard(dash *models.Dashboard) error Query(query string) ([]*models.SearchResult, error) Close() } func New() Store { - return NewFileStore("data") + return NewRethinkStore(&RethinkCfg{DatabaseName: "grafana"}) } diff --git a/start_dependencies.sh b/start_dependencies.sh new file mode 100755 index 00000000000..a8ecfd53c4c --- /dev/null +++ b/start_dependencies.sh @@ -0,0 +1,7 @@ +docker kill gfdev +docker rm gfdev + +docker run -d -p 8180:8080 -p 28015:28015 -p 29015:29015 \ + --name rethinkdb \ + -v /var/docker/grafana-pro-rethinkdb:/data \ + dockerfile/rethinkdb