Commit d69b63cb by bergquist

dashboards as cfg: read first cfg version

parent a62ebb3e
- name: 'default'
org_id: 1
folder: ''
type: file
options:
folder: /var/lib/grafana/dashboards
\ No newline at end of file
...@@ -23,6 +23,9 @@ plugins = data/plugins ...@@ -23,6 +23,9 @@ plugins = data/plugins
# Config files containing datasources that will be configured at startup # Config files containing datasources that will be configured at startup
datasources = conf/datasources datasources = conf/datasources
# Config files containing folders to read dashboards from and insert into the database.
dashboards = conf/dashboards
#################################### Server ############################## #################################### Server ##############################
[server] [server]
# Protocol (http, https, socket) # Protocol (http, https, socket)
......
...@@ -23,6 +23,9 @@ ...@@ -23,6 +23,9 @@
# Config files containing datasources that will be configured at startup # Config files containing datasources that will be configured at startup
;datasources = conf/datasources ;datasources = conf/datasources
# Config files containing folders to read dashboards from and insert into the database.
;dashboards = conf/dashboards
#################################### Server #################################### #################################### Server ####################################
[server] [server]
# Protocol (http, https, socket) # Protocol (http, https, socket)
......
...@@ -68,7 +68,7 @@ func (g *GrafanaServerImpl) Start() { ...@@ -68,7 +68,7 @@ func (g *GrafanaServerImpl) Start() {
social.NewOAuthService() social.NewOAuthService()
plugins.Init() 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) logger.Error("Failed to provision Grafana from config", "error", err)
g.Shutdown(1, "Startup failed") g.Shutdown(1, "Startup failed")
return return
......
...@@ -11,11 +11,12 @@ import ( ...@@ -11,11 +11,12 @@ import (
// Typed errors // Typed errors
var ( var (
ErrDashboardNotFound = errors.New("Dashboard not found") ErrDashboardNotFound = errors.New("Dashboard not found")
ErrDashboardSnapshotNotFound = errors.New("Dashboard snapshot not found") ErrDashboardSnapshotNotFound = errors.New("Dashboard snapshot not found")
ErrDashboardWithSameNameExists = errors.New("A dashboard with the same name already exists") ErrDashboardWithSameNameExists = errors.New("A dashboard with the same name already exists")
ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else") ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else")
ErrDashboardTitleEmpty = errors.New("Dashboard title cannot be empty") ErrDashboardTitleEmpty = errors.New("Dashboard title cannot be empty")
ErrDashboardContainsInvalidAlertData = errors.New("Invalid alert data. Cannot save dashboard")
) )
type UpdatePluginDashboardError struct { type UpdatePluginDashboardError struct {
...@@ -139,6 +140,8 @@ type SaveDashboardCommand struct { ...@@ -139,6 +140,8 @@ type SaveDashboardCommand struct {
RestoredFrom int `json:"-"` RestoredFrom int `json:"-"`
PluginId string `json:"-"` PluginId string `json:"-"`
UpdatedAt time.Time
Result *Dashboard Result *Dashboard
} }
......
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
}
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
}
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")
})
})
}
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
}
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
}
- 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
[]
{
"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": "<div class=\"text-center\" style=\"padding-top: 15px\">\n<img src=\"img/logo_transparent_200x.png\"> \n</div>",
"style": {},
"title": "Welcome to"
}
]
},
{
"title": "Welcome to Grafana",
"height": "210px",
"collapse": false,
"editable": true,
"panels": [
{
"id": 2,
"span": 6,
"type": "text",
"mode": "html",
"content": "<br/>\n\n<div class=\"row-fluid\">\n <div class=\"span6\">\n <ul>\n <li>\n <a href=\"http://grafana.org/docs#configuration\" target=\"_blank\">Configuration</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/troubleshooting\" target=\"_blank\">Troubleshooting</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/support\" target=\"_blank\">Support</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/intro\" target=\"_blank\">Getting started</a> (Must read!)\n </li>\n </ul>\n </div>\n <div class=\"span6\">\n <ul>\n <li>\n <a href=\"http://grafana.org/docs/features/graphing\" target=\"_blank\">Graphing</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/annotations\" target=\"_blank\">Annotations</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/graphite\" target=\"_blank\">Graphite</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/influxdb\" target=\"_blank\">InfluxDB</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/opentsdb\" target=\"_blank\">OpenTSDB</a>\n </li>\n </ul>\n </div>\n</div>",
"style": {},
"title": "Documentation Links"
},
{
"id": 3,
"span": 6,
"type": "text",
"mode": "html",
"content": "<br/>\n\n<div class=\"row-fluid\">\n <div class=\"span12\">\n <ul>\n <li>Ctrl+S saves the current dashboard</li>\n <li>Ctrl+F Opens the dashboard finder</li>\n <li>Ctrl+H Hide/show row controls</li>\n <li>Click and drag graph title to move panel</li>\n <li>Hit Escape to exit graph when in fullscreen or edit mode</li>\n <li>Click the colored icon in the legend to change series color</li>\n <li>Ctrl or Shift + Click legend name to hide other series</li>\n </ul>\n </div>\n</div>\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
{
"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": "<div class=\"text-center\" style=\"padding-top: 15px\">\n<img src=\"img/logo_transparent_200x.png\"> \n</div>",
"style": {},
"title": "Welcome to"
}
]
},
{
"title": "Welcome to Grafana",
"height": "210px",
"collapse": false,
"editable": true,
"panels": [
{
"id": 2,
"span": 6,
"type": "text",
"mode": "html",
"content": "<br/>\n\n<div class=\"row-fluid\">\n <div class=\"span6\">\n <ul>\n <li>\n <a href=\"http://grafana.org/docs#configuration\" target=\"_blank\">Configuration</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/troubleshooting\" target=\"_blank\">Troubleshooting</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/support\" target=\"_blank\">Support</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/intro\" target=\"_blank\">Getting started</a> (Must read!)\n </li>\n </ul>\n </div>\n <div class=\"span6\">\n <ul>\n <li>\n <a href=\"http://grafana.org/docs/features/graphing\" target=\"_blank\">Graphing</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/annotations\" target=\"_blank\">Annotations</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/graphite\" target=\"_blank\">Graphite</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/influxdb\" target=\"_blank\">InfluxDB</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/opentsdb\" target=\"_blank\">OpenTSDB</a>\n </li>\n </ul>\n </div>\n</div>",
"style": {},
"title": "Documentation Links"
},
{
"id": 3,
"span": 6,
"type": "text",
"mode": "html",
"content": "<br/>\n\n<div class=\"row-fluid\">\n <div class=\"span12\">\n <ul>\n <li>Ctrl+S saves the current dashboard</li>\n <li>Ctrl+F Opens the dashboard finder</li>\n <li>Ctrl+H Hide/show row controls</li>\n <li>Click and drag graph title to move panel</li>\n <li>Hit Escape to exit graph when in fullscreen or edit mode</li>\n <li>Click the colored icon in the legend to change series color</li>\n <li>Ctrl or Shift + Click legend name to hide other series</li>\n </ul>\n </div>\n</div>\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
{
"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": "<div class=\"text-center\" style=\"padding-top: 15px\">\n<img src=\"img/logo_transparent_200x.png\"> \n</div>",
"style": {},
"title": "Welcome to"
}
]
},
{
"title": "Welcome to Grafana",
"height": "210px",
"collapse": false,
"editable": true,
"panels": [
{
"id": 2,
"span": 6,
"type": "text",
"mode": "html",
"content": "<br/>\n\n<div class=\"row-fluid\">\n <div class=\"span6\">\n <ul>\n <li>\n <a href=\"http://grafana.org/docs#configuration\" target=\"_blank\">Configuration</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/troubleshooting\" target=\"_blank\">Troubleshooting</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/support\" target=\"_blank\">Support</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/intro\" target=\"_blank\">Getting started</a> (Must read!)\n </li>\n </ul>\n </div>\n <div class=\"span6\">\n <ul>\n <li>\n <a href=\"http://grafana.org/docs/features/graphing\" target=\"_blank\">Graphing</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/annotations\" target=\"_blank\">Annotations</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/graphite\" target=\"_blank\">Graphite</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/influxdb\" target=\"_blank\">InfluxDB</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/opentsdb\" target=\"_blank\">OpenTSDB</a>\n </li>\n </ul>\n </div>\n</div>",
"style": {},
"title": "Documentation Links"
},
{
"id": 3,
"span": 6,
"type": "text",
"mode": "html",
"content": "<br/>\n\n<div class=\"row-fluid\">\n <div class=\"span12\">\n <ul>\n <li>Ctrl+S saves the current dashboard</li>\n <li>Ctrl+F Opens the dashboard finder</li>\n <li>Ctrl+H Hide/show row controls</li>\n <li>Click and drag graph title to move panel</li>\n <li>Hit Escape to exit graph when in fullscreen or edit mode</li>\n <li>Click the colored icon in the legend to change series color</li>\n <li>Ctrl or Shift + Click legend name to hide other series</li>\n </ul>\n </div>\n</div>\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
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
package provisioning package provisioning
import ( import (
"context"
"path/filepath"
"github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/services/provisioning/dashboard"
"github.com/grafana/grafana/pkg/services/provisioning/datasources" "github.com/grafana/grafana/pkg/services/provisioning/datasources"
ini "gopkg.in/ini.v1"
) )
var ( var (
logger log.Logger = log.New("services.provisioning") logger log.Logger = log.New("services.provisioning")
) )
func StartUp(datasourcePath string) error { type Provisioner struct {
return datasources.Provision(datasourcePath) 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)
} }
...@@ -81,6 +81,11 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error { ...@@ -81,6 +81,11 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error {
} else { } else {
dash.Version += 1 dash.Version += 1
dash.Data.Set("version", dash.Version) dash.Data.Set("version", dash.Version)
if !cmd.UpdatedAt.IsZero() {
dash.Updated = cmd.UpdatedAt
}
affectedRows, err = sess.Id(dash.Id).Update(dash) affectedRows, err = sess.Id(dash.Id).Update(dash)
} }
......
...@@ -55,6 +55,7 @@ var ( ...@@ -55,6 +55,7 @@ var (
DataPath string DataPath string
PluginsPath string PluginsPath string
DatasourcesPath string DatasourcesPath string
DashboardsPath string
CustomInitPath = "conf/custom.ini" CustomInitPath = "conf/custom.ini"
// Log settings. // Log settings.
...@@ -475,6 +476,7 @@ func NewConfigContext(args *CommandLineArgs) error { ...@@ -475,6 +476,7 @@ func NewConfigContext(args *CommandLineArgs) error {
InstanceName = Cfg.Section("").Key("instance_name").MustString("unknown_instance_name") InstanceName = Cfg.Section("").Key("instance_name").MustString("unknown_instance_name")
PluginsPath = makeAbsolute(Cfg.Section("paths").Key("plugins").String(), HomePath) PluginsPath = makeAbsolute(Cfg.Section("paths").Key("plugins").String(), HomePath)
DatasourcesPath = makeAbsolute(Cfg.Section("paths").Key("datasources").String(), HomePath) DatasourcesPath = makeAbsolute(Cfg.Section("paths").Key("datasources").String(), HomePath)
DashboardsPath = makeAbsolute(Cfg.Section("paths").Key("dashboards").String(), HomePath)
server := Cfg.Section("server") server := Cfg.Section("server")
AppUrl, AppSubUrl = parseAppUrlAndSubUrl(server) AppUrl, AppSubUrl = parseAppUrlAndSubUrl(server)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment