diff --git a/conf/dashboards/dashboards.yaml b/conf/dashboards/dashboards.yaml new file mode 100644 index 00000000000..909a621ca18 --- /dev/null +++ b/conf/dashboards/dashboards.yaml @@ -0,0 +1,6 @@ +- name: 'default' + org_id: 1 + folder: '' + type: file + options: + folder: /var/lib/grafana/dashboards \ No newline at end of file diff --git a/conf/defaults.ini b/conf/defaults.ini index a145d57482b..9dd1857f270 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -23,6 +23,9 @@ plugins = data/plugins # Config files containing datasources that will be configured at startup datasources = conf/datasources +# Config files containing folders to read dashboards from and insert into the database. +dashboards = conf/dashboards + #################################### Server ############################## [server] # Protocol (http, https, socket) diff --git a/conf/sample.ini b/conf/sample.ini index 233a97deef8..44e6f0d4ca3 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -23,6 +23,9 @@ # Config files containing datasources that will be configured at startup ;datasources = conf/datasources +# Config files containing folders to read dashboards from and insert into the database. +;dashboards = conf/dashboards + #################################### Server #################################### [server] # Protocol (http, https, socket) diff --git a/pkg/cmd/grafana-server/server.go b/pkg/cmd/grafana-server/server.go index f5c6b0d1cee..820295a9b88 100644 --- a/pkg/cmd/grafana-server/server.go +++ b/pkg/cmd/grafana-server/server.go @@ -68,7 +68,7 @@ func (g *GrafanaServerImpl) Start() { social.NewOAuthService() plugins.Init() - if err := provisioning.StartUp(setting.DatasourcesPath); err != nil { + if err := provisioning.Init(g.context, setting.HomePath, setting.Cfg); err != nil { logger.Error("Failed to provision Grafana from config", "error", err) g.Shutdown(1, "Startup failed") return diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index 0463e9c209b..4d1d1f1f3d5 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -11,11 +11,12 @@ import ( // Typed errors var ( - ErrDashboardNotFound = errors.New("Dashboard not found") - ErrDashboardSnapshotNotFound = errors.New("Dashboard snapshot not found") - ErrDashboardWithSameNameExists = errors.New("A dashboard with the same name already exists") - ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else") - ErrDashboardTitleEmpty = errors.New("Dashboard title cannot be empty") + ErrDashboardNotFound = errors.New("Dashboard not found") + ErrDashboardSnapshotNotFound = errors.New("Dashboard snapshot not found") + ErrDashboardWithSameNameExists = errors.New("A dashboard with the same name already exists") + ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else") + ErrDashboardTitleEmpty = errors.New("Dashboard title cannot be empty") + ErrDashboardContainsInvalidAlertData = errors.New("Invalid alert data. Cannot save dashboard") ) type UpdatePluginDashboardError struct { @@ -139,6 +140,8 @@ type SaveDashboardCommand struct { RestoredFrom int `json:"-"` PluginId string `json:"-"` + UpdatedAt time.Time + Result *Dashboard } diff --git a/pkg/services/provisioning/dashboard/config_reader.go b/pkg/services/provisioning/dashboard/config_reader.go new file mode 100644 index 00000000000..d7ee92a592c --- /dev/null +++ b/pkg/services/provisioning/dashboard/config_reader.go @@ -0,0 +1,49 @@ +package dashboard + +import ( + "io/ioutil" + "path/filepath" + "strings" + + yaml "gopkg.in/yaml.v2" +) + +type configReader struct { + path string +} + +func (cr *configReader) readConfig() ([]*DashboardsAsConfig, error) { + files, err := ioutil.ReadDir(cr.path) + if err != nil { + return nil, err + } + + var dashboards []*DashboardsAsConfig + for _, file := range files { + if !strings.HasSuffix(file.Name(), ".yaml") && !strings.HasSuffix(file.Name(), ".yml") { + continue + } + + filename, _ := filepath.Abs(filepath.Join(cr.path, file.Name())) + yamlFile, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + + var datasource []*DashboardsAsConfig + err = yaml.Unmarshal(yamlFile, &datasource) + if err != nil { + return nil, err + } + + dashboards = append(dashboards, datasource...) + } + + for i := range dashboards { + if dashboards[i].OrgId == 0 { + dashboards[i].OrgId = 1 + } + } + + return dashboards, nil +} diff --git a/pkg/services/provisioning/dashboard/dashboard.go b/pkg/services/provisioning/dashboard/dashboard.go new file mode 100644 index 00000000000..22ed5add831 --- /dev/null +++ b/pkg/services/provisioning/dashboard/dashboard.go @@ -0,0 +1,51 @@ +package dashboard + +import ( + "context" + "fmt" + + "github.com/grafana/grafana/pkg/log" +) + +type DashboardProvisioner struct { + cfgReader *configReader + log log.Logger + ctx context.Context +} + +func Provision(ctx context.Context, configDirectory string) (*DashboardProvisioner, error) { + d := &DashboardProvisioner{ + cfgReader: &configReader{path: configDirectory}, + log: log.New("provisioning.dashboard"), + ctx: ctx, + } + + return d, d.Init(ctx) +} + +func (provider *DashboardProvisioner) Init(ctx context.Context) error { + cfgs, err := provider.cfgReader.readConfig() + if err != nil { + return err + } + + for _, cfg := range cfgs { + if cfg.Type == "file" { + fileReader, err := NewDashboardFilereader(cfg, provider.log.New("type", cfg.Type, "name", cfg.Name)) + if err != nil { + return err + } + + // err = fileReader.Init() + // if err != nil { + // provider.log.Error("Failed to load dashboards", "error", err) + // } + + go fileReader.Listen(ctx) + } else { + return fmt.Errorf("type %s is not supported", cfg.Type) + } + } + + return nil +} diff --git a/pkg/services/provisioning/dashboard/dashboard_test.go b/pkg/services/provisioning/dashboard/dashboard_test.go new file mode 100644 index 00000000000..6b1bf3fc6d3 --- /dev/null +++ b/pkg/services/provisioning/dashboard/dashboard_test.go @@ -0,0 +1,47 @@ +package dashboard + +import ( + "testing" + + . "github.com/smartystreets/goconvey/convey" +) + +var ( + simpleDashboardConfig string = "./test-configs/dashboards-from-disk" +) + +func TestDashboardsAsConfig(t *testing.T) { + Convey("Dashboards as configuration", t, func() { + + Convey("Can read config file", func() { + + cfgProvifer := configReader{path: simpleDashboardConfig} + cfg, err := cfgProvifer.readConfig() + if err != nil { + t.Fatalf("readConfig return an error %v", err) + } + + So(len(cfg), ShouldEqual, 2) + + ds := cfg[0] + + So(ds.Name, ShouldEqual, "general dashboards") + So(ds.Type, ShouldEqual, "file") + So(ds.OrgId, ShouldEqual, 2) + So(ds.Folder, ShouldEqual, "developers") + + So(len(ds.Options), ShouldEqual, 1) + So(ds.Options["folder"], ShouldEqual, "/var/lib/grafana/dashboards") + + ds2 := cfg[1] + + So(ds2.Name, ShouldEqual, "default") + So(ds2.Type, ShouldEqual, "file") + So(ds2.OrgId, ShouldEqual, 1) + So(ds2.Folder, ShouldEqual, "") + + So(len(ds2.Options), ShouldEqual, 1) + So(ds2.Options["folder"], ShouldEqual, "/var/lib/grafana/dashboards") + }) + }) +} diff --git a/pkg/services/provisioning/dashboard/file_reader.go b/pkg/services/provisioning/dashboard/file_reader.go new file mode 100644 index 00000000000..c9eacb04aeb --- /dev/null +++ b/pkg/services/provisioning/dashboard/file_reader.go @@ -0,0 +1,212 @@ +package dashboard + +import ( + "context" + "fmt" + "github.com/grafana/grafana/pkg/services/alerting" + "os" + "path/filepath" + "strings" + "sync" + "time" + + "github.com/grafana/grafana/pkg/bus" + + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/models" +) + +type fileReader struct { + Cfg *DashboardsAsConfig + Path string + log log.Logger + dashboardCache *dashboardCache +} + +type dashboardCache struct { + mutex *sync.Mutex + dashboards map[string]*DashboardJson +} + +func newDashboardCache() *dashboardCache { + return &dashboardCache{ + dashboards: map[string]*DashboardJson{}, + mutex: &sync.Mutex{}, + } +} + +func (dc *dashboardCache) addCache(json *DashboardJson) { + dc.mutex.Lock() + defer dc.mutex.Unlock() + dc.dashboards[json.Path] = json +} + +func (dc *dashboardCache) getCache(path string) (*DashboardJson, bool) { + dc.mutex.Lock() + defer dc.mutex.Unlock() + v, exist := dc.dashboards[path] + return v, exist +} + +func NewDashboardFilereader(cfg *DashboardsAsConfig, log log.Logger) (*fileReader, error) { + path, ok := cfg.Options["folder"].(string) + if !ok { + return nil, fmt.Errorf("Failed to load dashboards. folder param is not a string") + } + + if _, err := os.Stat(path); os.IsNotExist(err) { + log.Error("Cannot read directory", "error", err) + } + + return &fileReader{ + Cfg: cfg, + Path: path, + log: log, + dashboardCache: newDashboardCache(), + }, nil +} + +func (fr *fileReader) Listen(ctx context.Context) error { + ticker := time.NewTicker(time.Second * 1) + + if err := fr.walkFolder(); err != nil { + fr.log.Error("failed to search for dashboards", "error", err) + } + + for { + select { + case <-ticker.C: + fr.walkFolder() + case <-ctx.Done(): + return nil + } + } +} + +func (fr *fileReader) walkFolder() error { + if _, err := os.Stat(fr.Path); err != nil { + if os.IsNotExist(err) { + return err + } + } + + return filepath.Walk(fr.Path, func(path string, f os.FileInfo, err error) error { + if err != nil { + return err + } + if f.IsDir() { + if strings.HasPrefix(f.Name(), ".") { + return filepath.SkipDir + } + return nil + } + + if !strings.HasSuffix(f.Name(), ".json") { + return nil + } + + cachedDashboard, exist := fr.dashboardCache.getCache(path) + if exist && cachedDashboard.ModTime == f.ModTime() { + return nil + } + + dash, err := fr.readDashboardFromFile(path) + if err != nil { + fr.log.Error("failed to load dashboard from ", "file", path, "error", err) + return nil + } + + cmd := &models.GetDashboardQuery{Slug: dash.Dashboard.Slug} + err = bus.Dispatch(cmd) + + if err == models.ErrDashboardNotFound { + fr.log.Debug("saving new dashboard", "file", path) + return fr.saveDashboard(dash) + } + + if err != nil { + fr.log.Error("failed to query for dashboard", "slug", dash.Dashboard.Slug, "error", err) + return nil + } + + if cmd.Result.Updated.Unix() >= f.ModTime().Unix() { + fr.log.Debug("already using latest version", "dashboard", dash.Dashboard.Slug) + return nil + } + + fr.log.Debug("no dashboard in cache. Loading dashboard from disk into database.", "file", path) + return fr.saveDashboard(dash) + }) +} + +func (fr *fileReader) readDashboardFromFile(path string) (*DashboardJson, error) { + reader, err := os.Open(path) + if err != nil { + return nil, err + } + defer reader.Close() + + data, err := simplejson.NewFromReader(reader) + if err != nil { + return nil, err + } + + stat, _ := os.Stat(path) + dash := &DashboardJson{} + dash.Dashboard = models.NewDashboardFromJson(data) + dash.TitleLower = strings.ToLower(dash.Dashboard.Title) + dash.Path = path + dash.ModTime = stat.ModTime() + dash.OrgId = fr.Cfg.OrgId + dash.Folder = fr.Cfg.Folder + + if dash.Dashboard.Title == "" { + return nil, models.ErrDashboardTitleEmpty + } + + fr.dashboardCache.addCache(dash) + + return dash, nil +} + +func (fr *fileReader) saveDashboard(dashboardJson *DashboardJson) error { + dash := dashboardJson.Dashboard + + if dash.Title == "" { + return models.ErrDashboardTitleEmpty + } + + validateAlertsCmd := alerting.ValidateDashboardAlertsCommand{ + OrgId: dashboardJson.OrgId, + Dashboard: dash, + } + + if err := bus.Dispatch(&validateAlertsCmd); err != nil { + return models.ErrDashboardContainsInvalidAlertData + } + + cmd := models.SaveDashboardCommand{ + Dashboard: dash.Data, + Message: "Dashboard created from file.", + OrgId: dashboardJson.OrgId, + Overwrite: true, + UpdatedAt: dashboardJson.ModTime, + } + + err := bus.Dispatch(&cmd) + if err != nil { + return err + } + + alertCmd := alerting.UpdateDashboardAlertsCommand{ + OrgId: dashboardJson.OrgId, + Dashboard: cmd.Result, + } + + if err := bus.Dispatch(&alertCmd); err != nil { + return err + } + + return nil +} diff --git a/pkg/services/provisioning/dashboard/file_reader_test.go b/pkg/services/provisioning/dashboard/file_reader_test.go new file mode 100644 index 00000000000..095d0feeb3f --- /dev/null +++ b/pkg/services/provisioning/dashboard/file_reader_test.go @@ -0,0 +1,88 @@ +package dashboard + +import ( + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/alerting" + "testing" + + "github.com/grafana/grafana/pkg/log" + . "github.com/smartystreets/goconvey/convey" +) + +var ( + defaultDashboards string = "./test-dashboards/folder-one" + brokenDashboards string = "./test-dashboards/broken-dashboards" +) + +func TestDashboardFileReader(t *testing.T) { + Convey("Reading dashboards from disk", t, func() { + bus.ClearBusHandlers() + + bus.AddHandler("test", mockGetDashboardQuery) + bus.AddHandler("test", mockValidateDashboardAlertsCommand) + bus.AddHandler("test", mockSaveDashboardCommand) + bus.AddHandler("test", mockUpdateDashboardAlertsCommand) + logger := log.New("test.logger") + + Convey("Can read default dashboard", func() { + cfg := &DashboardsAsConfig{ + Name: "Default", + Type: "file", + OrgId: 1, + Folder: "", + Options: map[string]interface{}{ + "folder": defaultDashboards, + }, + } + reader, err := NewDashboardFilereader(cfg, logger) + So(err, ShouldBeNil) + + err = reader.walkFolder() + So(err, ShouldBeNil) + }) + + Convey("Invalid configuration should return error", func() { + cfg := &DashboardsAsConfig{ + Name: "Default", + Type: "file", + OrgId: 1, + Folder: "", + } + + _, err := NewDashboardFilereader(cfg, logger) + So(err, ShouldNotBeNil) + }) + + Convey("Broken dashboards should not cause error", func() { + cfg := &DashboardsAsConfig{ + Name: "Default", + Type: "file", + OrgId: 1, + Folder: "", + Options: map[string]interface{}{ + "folder": brokenDashboards, + }, + } + + _, err := NewDashboardFilereader(cfg, logger) + So(err, ShouldBeNil) + }) + }) +} + +func mockGetDashboardQuery(cmd *models.GetDashboardQuery) error { + return models.ErrDashboardNotFound +} + +func mockValidateDashboardAlertsCommand(cmd *alerting.ValidateDashboardAlertsCommand) error { + return nil +} + +func mockSaveDashboardCommand(cmd *models.SaveDashboardCommand) error { + return nil +} + +func mockUpdateDashboardAlertsCommand(cmd *alerting.UpdateDashboardAlertsCommand) error { + return nil +} diff --git a/pkg/services/provisioning/dashboard/test-configs/dashboards-from-disk/dev-dashboards.yaml b/pkg/services/provisioning/dashboard/test-configs/dashboards-from-disk/dev-dashboards.yaml new file mode 100644 index 00000000000..67cb383e813 --- /dev/null +++ b/pkg/services/provisioning/dashboard/test-configs/dashboards-from-disk/dev-dashboards.yaml @@ -0,0 +1,11 @@ +- name: 'general dashboards' + org_id: 2 + folder: 'developers' + type: file + options: + folder: /var/lib/grafana/dashboards + +- name: 'default' + type: file + options: + folder: /var/lib/grafana/dashboards diff --git a/pkg/services/provisioning/dashboard/test-dashboards/broken-dashboards/empty-json.json b/pkg/services/provisioning/dashboard/test-dashboards/broken-dashboards/empty-json.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pkg/services/provisioning/dashboard/test-dashboards/broken-dashboards/invalid.json b/pkg/services/provisioning/dashboard/test-dashboards/broken-dashboards/invalid.json new file mode 100644 index 00000000000..1aa388a6e78 --- /dev/null +++ b/pkg/services/provisioning/dashboard/test-dashboards/broken-dashboards/invalid.json @@ -0,0 +1,174 @@ +[] +{ + "title": "Grafana", + "tags": [], + "style": "dark", + "timezone": "browser", + "editable": true, + "rows": [ + { + "title": "New row", + "height": "150px", + "collapse": false, + "editable": true, + "panels": [ + { + "id": 1, + "span": 12, + "editable": true, + "type": "text", + "mode": "html", + "content": "
\n \n
", + "style": {}, + "title": "Welcome to" + } + ] + }, + { + "title": "Welcome to Grafana", + "height": "210px", + "collapse": false, + "editable": true, + "panels": [ + { + "id": 2, + "span": 6, + "type": "text", + "mode": "html", + "content": "
\n\n
\n
\n \n
\n
\n \n
\n
", + "style": {}, + "title": "Documentation Links" + }, + { + "id": 3, + "span": 6, + "type": "text", + "mode": "html", + "content": "
\n\n
\n
\n \n
\n
\n", + "style": {}, + "title": "Tips & Shortcuts" + } + ] + }, + { + "title": "test", + "height": "250px", + "editable": true, + "collapse": false, + "panels": [ + { + "id": 4, + "span": 12, + "type": "graph", + "x-axis": true, + "y-axis": true, + "scale": 1, + "y_formats": [ + "short", + "short" + ], + "grid": { + "max": null, + "min": null, + "leftMax": null, + "rightMax": null, + "leftMin": null, + "rightMin": null, + "threshold1": null, + "threshold2": null, + "threshold1Color": "rgba(216, 200, 27, 0.27)", + "threshold2Color": "rgba(234, 112, 112, 0.22)" + }, + "resolution": 100, + "lines": true, + "fill": 1, + "linewidth": 2, + "dashes": false, + "dashLength": 10, + "spaceLength": 10, + "points": false, + "pointradius": 5, + "bars": false, + "stack": true, + "spyable": true, + "options": false, + "legend": { + "show": true, + "values": false, + "min": false, + "max": false, + "current": false, + "total": false, + "avg": false + }, + "interactive": true, + "legend_counts": true, + "timezone": "browser", + "percentage": false, + "nullPointMode": "connected", + "steppedLine": false, + "tooltip": { + "value_type": "cumulative", + "query_as_alias": true + }, + "targets": [ + { + "target": "randomWalk('random walk')", + "function": "mean", + "column": "value" + } + ], + "aliasColors": {}, + "aliasYAxis": {}, + "title": "First Graph (click title to edit)", + "datasource": "graphite", + "renderer": "flot", + "annotate": { + "enable": false + } + } + ] + } + ], + "nav": [ + { + "type": "timepicker", + "collapse": false, + "enable": true, + "status": "Stable", + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ], + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "now": true + } + ], + "time": { + "from": "now-6h", + "to": "now" + }, + "templating": { + "list": [] + }, + "version": 5 + } + \ No newline at end of file diff --git a/pkg/services/provisioning/dashboard/test-dashboards/folder-one/dashboard1.json b/pkg/services/provisioning/dashboard/test-dashboards/folder-one/dashboard1.json new file mode 100644 index 00000000000..5b6765a4ed6 --- /dev/null +++ b/pkg/services/provisioning/dashboard/test-dashboards/folder-one/dashboard1.json @@ -0,0 +1,173 @@ +{ + "title": "Grafana", + "tags": [], + "style": "dark", + "timezone": "browser", + "editable": true, + "rows": [ + { + "title": "New row", + "height": "150px", + "collapse": false, + "editable": true, + "panels": [ + { + "id": 1, + "span": 12, + "editable": true, + "type": "text", + "mode": "html", + "content": "
\n \n
", + "style": {}, + "title": "Welcome to" + } + ] + }, + { + "title": "Welcome to Grafana", + "height": "210px", + "collapse": false, + "editable": true, + "panels": [ + { + "id": 2, + "span": 6, + "type": "text", + "mode": "html", + "content": "
\n\n
\n
\n \n
\n
\n \n
\n
", + "style": {}, + "title": "Documentation Links" + }, + { + "id": 3, + "span": 6, + "type": "text", + "mode": "html", + "content": "
\n\n
\n
\n \n
\n
\n", + "style": {}, + "title": "Tips & Shortcuts" + } + ] + }, + { + "title": "test", + "height": "250px", + "editable": true, + "collapse": false, + "panels": [ + { + "id": 4, + "span": 12, + "type": "graph", + "x-axis": true, + "y-axis": true, + "scale": 1, + "y_formats": [ + "short", + "short" + ], + "grid": { + "max": null, + "min": null, + "leftMax": null, + "rightMax": null, + "leftMin": null, + "rightMin": null, + "threshold1": null, + "threshold2": null, + "threshold1Color": "rgba(216, 200, 27, 0.27)", + "threshold2Color": "rgba(234, 112, 112, 0.22)" + }, + "resolution": 100, + "lines": true, + "fill": 1, + "linewidth": 2, + "dashes": false, + "dashLength": 10, + "spaceLength": 10, + "points": false, + "pointradius": 5, + "bars": false, + "stack": true, + "spyable": true, + "options": false, + "legend": { + "show": true, + "values": false, + "min": false, + "max": false, + "current": false, + "total": false, + "avg": false + }, + "interactive": true, + "legend_counts": true, + "timezone": "browser", + "percentage": false, + "nullPointMode": "connected", + "steppedLine": false, + "tooltip": { + "value_type": "cumulative", + "query_as_alias": true + }, + "targets": [ + { + "target": "randomWalk('random walk')", + "function": "mean", + "column": "value" + } + ], + "aliasColors": {}, + "aliasYAxis": {}, + "title": "First Graph (click title to edit)", + "datasource": "graphite", + "renderer": "flot", + "annotate": { + "enable": false + } + } + ] + } + ], + "nav": [ + { + "type": "timepicker", + "collapse": false, + "enable": true, + "status": "Stable", + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ], + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "now": true + } + ], + "time": { + "from": "now-6h", + "to": "now" + }, + "templating": { + "list": [] + }, + "version": 5 + } + \ No newline at end of file diff --git a/pkg/services/provisioning/dashboard/test-dashboards/folder-one/dashboard2.json b/pkg/services/provisioning/dashboard/test-dashboards/folder-one/dashboard2.json new file mode 100644 index 00000000000..5b6765a4ed6 --- /dev/null +++ b/pkg/services/provisioning/dashboard/test-dashboards/folder-one/dashboard2.json @@ -0,0 +1,173 @@ +{ + "title": "Grafana", + "tags": [], + "style": "dark", + "timezone": "browser", + "editable": true, + "rows": [ + { + "title": "New row", + "height": "150px", + "collapse": false, + "editable": true, + "panels": [ + { + "id": 1, + "span": 12, + "editable": true, + "type": "text", + "mode": "html", + "content": "
\n \n
", + "style": {}, + "title": "Welcome to" + } + ] + }, + { + "title": "Welcome to Grafana", + "height": "210px", + "collapse": false, + "editable": true, + "panels": [ + { + "id": 2, + "span": 6, + "type": "text", + "mode": "html", + "content": "
\n\n
\n
\n \n
\n
\n \n
\n
", + "style": {}, + "title": "Documentation Links" + }, + { + "id": 3, + "span": 6, + "type": "text", + "mode": "html", + "content": "
\n\n
\n
\n \n
\n
\n", + "style": {}, + "title": "Tips & Shortcuts" + } + ] + }, + { + "title": "test", + "height": "250px", + "editable": true, + "collapse": false, + "panels": [ + { + "id": 4, + "span": 12, + "type": "graph", + "x-axis": true, + "y-axis": true, + "scale": 1, + "y_formats": [ + "short", + "short" + ], + "grid": { + "max": null, + "min": null, + "leftMax": null, + "rightMax": null, + "leftMin": null, + "rightMin": null, + "threshold1": null, + "threshold2": null, + "threshold1Color": "rgba(216, 200, 27, 0.27)", + "threshold2Color": "rgba(234, 112, 112, 0.22)" + }, + "resolution": 100, + "lines": true, + "fill": 1, + "linewidth": 2, + "dashes": false, + "dashLength": 10, + "spaceLength": 10, + "points": false, + "pointradius": 5, + "bars": false, + "stack": true, + "spyable": true, + "options": false, + "legend": { + "show": true, + "values": false, + "min": false, + "max": false, + "current": false, + "total": false, + "avg": false + }, + "interactive": true, + "legend_counts": true, + "timezone": "browser", + "percentage": false, + "nullPointMode": "connected", + "steppedLine": false, + "tooltip": { + "value_type": "cumulative", + "query_as_alias": true + }, + "targets": [ + { + "target": "randomWalk('random walk')", + "function": "mean", + "column": "value" + } + ], + "aliasColors": {}, + "aliasYAxis": {}, + "title": "First Graph (click title to edit)", + "datasource": "graphite", + "renderer": "flot", + "annotate": { + "enable": false + } + } + ] + } + ], + "nav": [ + { + "type": "timepicker", + "collapse": false, + "enable": true, + "status": "Stable", + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ], + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "now": true + } + ], + "time": { + "from": "now-6h", + "to": "now" + }, + "templating": { + "list": [] + }, + "version": 5 + } + \ No newline at end of file diff --git a/pkg/services/provisioning/dashboard/types.go b/pkg/services/provisioning/dashboard/types.go new file mode 100644 index 00000000000..cef6b217fee --- /dev/null +++ b/pkg/services/provisioning/dashboard/types.go @@ -0,0 +1,34 @@ +package dashboard + +import ( + "sync" + "time" + + "github.com/grafana/grafana/pkg/models" +) + +type DashboardsAsConfig struct { + Name string `json:"name" yaml:"name"` + Type string `json:"type" yaml:"type"` + OrgId int64 `json:"org_id" yaml:"org_id"` + Folder string `json:"folder" yaml:"folder"` + Options map[string]interface{} `json:"options" yaml:"options"` +} + +type DashboardJson struct { + TitleLower string + Path string + OrgId int64 + Folder string + ModTime time.Time + Dashboard *models.Dashboard +} + +type DashboardIndex struct { + mutex *sync.Mutex + + PathToDashboard map[string]*DashboardJson +} + +type InsertDashboard func(cmd *models.Dashboard) error +type UpdateDashboard func(cmd *models.SaveDashboardCommand) error diff --git a/pkg/services/provisioning/provisioning.go b/pkg/services/provisioning/provisioning.go index 1bea60f03e4..51f406dc9d5 100644 --- a/pkg/services/provisioning/provisioning.go +++ b/pkg/services/provisioning/provisioning.go @@ -1,14 +1,47 @@ package provisioning import ( + "context" + "path/filepath" + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/services/provisioning/dashboard" "github.com/grafana/grafana/pkg/services/provisioning/datasources" + ini "gopkg.in/ini.v1" ) var ( logger log.Logger = log.New("services.provisioning") ) -func StartUp(datasourcePath string) error { - return datasources.Provision(datasourcePath) +type Provisioner struct { + datasourcePath string + dashboardPath string + bgContext context.Context +} + +func Init(backgroundContext context.Context, homePath string, cfg *ini.File) error { + datasourcePath := makeAbsolute(cfg.Section("paths").Key("datasources").String(), homePath) + if err := datasources.Provision(datasourcePath); err != nil { + return err + } + + dashboardPath := makeAbsolute(cfg.Section("paths").Key("dashboards").String(), homePath) + _, err := dashboard.Provision(backgroundContext, dashboardPath) + if err != nil { + return err + } + + return nil +} + +func (p *Provisioner) Listen() error { + return nil +} + +func makeAbsolute(path string, root string) string { + if filepath.IsAbs(path) { + return path + } + return filepath.Join(root, path) } diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index d91b4a08aa6..cdf9d4eb3c7 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -81,6 +81,11 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error { } else { dash.Version += 1 dash.Data.Set("version", dash.Version) + + if !cmd.UpdatedAt.IsZero() { + dash.Updated = cmd.UpdatedAt + } + affectedRows, err = sess.Id(dash.Id).Update(dash) } diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 2caf7366727..f604cdd680b 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -55,6 +55,7 @@ var ( DataPath string PluginsPath string DatasourcesPath string + DashboardsPath string CustomInitPath = "conf/custom.ini" // Log settings. @@ -475,6 +476,7 @@ func NewConfigContext(args *CommandLineArgs) error { InstanceName = Cfg.Section("").Key("instance_name").MustString("unknown_instance_name") PluginsPath = makeAbsolute(Cfg.Section("paths").Key("plugins").String(), HomePath) DatasourcesPath = makeAbsolute(Cfg.Section("paths").Key("datasources").String(), HomePath) + DashboardsPath = makeAbsolute(Cfg.Section("paths").Key("dashboards").String(), HomePath) server := Cfg.Section("server") AppUrl, AppSubUrl = parseAppUrlAndSubUrl(server)