From 810ec4c5f8ec5bf028d2de015fbb1779ff8fd94d Mon Sep 17 00:00:00 2001 From: Maksim Nabokikh <32434187+nabokihms@users.noreply.github.com> Date: Tue, 30 Jun 2020 17:33:26 +0400 Subject: [PATCH] Provisioning: Use folders structure from the file system to create desired folders in dashboard provisioning. (#23117) Fixes #12016 --- docs/sources/administration/provisioning.md | 31 ++++ .../provisioning/dashboards/file_reader.go | 97 ++++++++-- .../dashboards/file_reader_test.go | 67 ++++++- .../folderOne/dashboard1.json | 172 ++++++++++++++++++ .../folderTwo/dashboard2.json | 172 ++++++++++++++++++ .../folders-from-files-structure/root.json | 172 ++++++++++++++++++ 6 files changed, 682 insertions(+), 29 deletions(-) create mode 100644 pkg/services/provisioning/dashboards/testdata/test-dashboards/folders-from-files-structure/folderOne/dashboard1.json create mode 100644 pkg/services/provisioning/dashboards/testdata/test-dashboards/folders-from-files-structure/folderTwo/dashboard2.json create mode 100644 pkg/services/provisioning/dashboards/testdata/test-dashboards/folders-from-files-structure/root.json diff --git a/docs/sources/administration/provisioning.md b/docs/sources/administration/provisioning.md index 137d504ed0d..75a0eb868da 100644 --- a/docs/sources/administration/provisioning.md +++ b/docs/sources/administration/provisioning.md @@ -269,6 +269,8 @@ providers: options: # path to dashboard files on disk. Required when using the 'file' type path: /var/lib/grafana/dashboards + # use folder names from filesystem to create folders in Grafana + foldersFromFilesStructure: true ``` When Grafana starts, it will update/insert all dashboards available in the configured path. Then later on poll that path every **updateIntervalSeconds** and look for updated json files and update/insert those into the database. @@ -302,6 +304,35 @@ By default Grafana will delete dashboards in the database if the file is removed > Be careful not to re-use the same `title` multiple times within a folder > or `uid` within the same installation as this will cause weird behaviors. +### Provision folders structure from filesystem to Grafana +If you already store your dashboards using folders in a git repo or on a filesystem, and also you want to have the same folder names in the Grafana menu, you can use `foldersFromFilesStructure` option. + +For example, to replicate these dashboards structure from the filesystem to Grafana, +``` +/etc/dashboards +├── /server +│ ├── /common_dashboard.json +│ └── /network_dashboard.json +└── /application + ├── /requests_dashboard.json + └── /resources_dashboard.json +``` +you need to specify just this short provision configuration file. +```yaml +apiVersion: 1 + +providers: +- name: dashboards + type: file + updateIntervalSeconds: 30 + options: + path: /etc/dashboards + foldersFromFileStructure: true +``` +`server` and `application` will become new folders in Grafana menu. + +> **Note.** `folder` and `folderUid` options should be empty or missing to make `foldersFromFileStructure` works. + ## Alert Notification Channels Alert Notification Channels can be provisioned by adding one or more yaml config files in the [`provisioning/notifiers`](/administration/configuration/#provisioning) directory. diff --git a/pkg/services/provisioning/dashboards/file_reader.go b/pkg/services/provisioning/dashboards/file_reader.go index 7ff939ae8d4..4d73acb74ca 100644 --- a/pkg/services/provisioning/dashboards/file_reader.go +++ b/pkg/services/provisioning/dashboards/file_reader.go @@ -33,6 +33,7 @@ type FileReader struct { Path string log log.Logger dashboardProvisioningService dashboards.DashboardProvisioningService + FoldersFromFilesStructure bool } // NewDashboardFileReader returns a new filereader based on `config` @@ -48,11 +49,17 @@ func NewDashboardFileReader(cfg *config, log log.Logger) (*FileReader, error) { log.Warn("[Deprecated] The folder property is deprecated. Please use path instead.") } + foldersFromFilesStructure, _ := cfg.Options["foldersFromFilesStructure"].(bool) + if foldersFromFilesStructure && cfg.Folder != "" && cfg.FolderUID != "" { + return nil, fmt.Errorf("'folder' and 'folderUID' should be empty using 'foldersFromFilesStructure' option") + } + return &FileReader{ Cfg: cfg, Path: path, log: log, dashboardProvisioningService: dashboards.NewProvisioningService(), + FoldersFromFilesStructure: foldersFromFilesStructure, }, nil } @@ -81,11 +88,6 @@ func (fr *FileReader) startWalkingDisk() error { return err } - folderID, err := getOrCreateFolderID(fr.Cfg, fr.dashboardProvisioningService) - if err != nil && err != ErrFolderNameMissing { - return err - } - provisionedDashboardRefs, err := getProvisionedDashboardByPath(fr.dashboardProvisioningService, fr.Cfg.Name) if err != nil { return err @@ -101,16 +103,61 @@ func (fr *FileReader) startWalkingDisk() error { sanityChecker := newProvisioningSanityChecker(fr.Cfg.Name) + if fr.FoldersFromFilesStructure { + err = fr.storeDashboardsInFoldersFromFileStructure(filesFoundOnDisk, provisionedDashboardRefs, resolvedPath, &sanityChecker) + } else { + err = fr.storeDashboardsInFolder(filesFoundOnDisk, provisionedDashboardRefs, &sanityChecker) + } + if err != nil { + return err + } + + sanityChecker.logWarnings(fr.log) + + return nil +} + +// storeDashboardsInFolder saves dashboards from the filesystem on disk to the folder from config +func (fr *FileReader) storeDashboardsInFolder(filesFoundOnDisk map[string]os.FileInfo, dashboardRefs map[string]*models.DashboardProvisioning, sanityChecker *provisioningSanityChecker) error { + folderID, err := getOrCreateFolderID(fr.Cfg, fr.dashboardProvisioningService, fr.Cfg.Folder) + + if err != nil && err != ErrFolderNameMissing { + return err + } + // save dashboards based on json files for path, fileInfo := range filesFoundOnDisk { - provisioningMetadata, err := fr.saveDashboard(path, folderID, fileInfo, provisionedDashboardRefs) + provisioningMetadata, err := fr.saveDashboard(path, folderID, fileInfo, dashboardRefs) sanityChecker.track(provisioningMetadata) if err != nil { fr.log.Error("failed to save dashboard", "error", err) } } - sanityChecker.logWarnings(fr.log) + return nil +} +// storeDashboardsInFoldersFromFilesystemStructure saves dashboards from the filesystem on disk to the same folder +// in grafana as they are in on the filesystem +func (fr *FileReader) storeDashboardsInFoldersFromFileStructure(filesFoundOnDisk map[string]os.FileInfo, dashboardRefs map[string]*models.DashboardProvisioning, resolvedPath string, sanityChecker *provisioningSanityChecker) error { + for path, fileInfo := range filesFoundOnDisk { + folderName := "" + + dashboardsFolder := filepath.Dir(path) + if dashboardsFolder != resolvedPath { + folderName = filepath.Base(dashboardsFolder) + } + + folderID, err := getOrCreateFolderID(fr.Cfg, fr.dashboardProvisioningService, folderName) + if err != nil && err != ErrFolderNameMissing { + return err + } + + provisioningMetadata, err := fr.saveDashboard(path, folderID, fileInfo, dashboardRefs) + sanityChecker.track(provisioningMetadata) + if err != nil { + fr.log.Error("failed to save dashboard", "error", err) + } + } return nil } @@ -171,7 +218,7 @@ func (fr *FileReader) saveDashboard(path string, folderID int64, fileInfo os.Fil // keeps track of what uid's and title's we have already provisioned dash := jsonFile.dashboard provisioningMetadata.uid = dash.Dashboard.Uid - provisioningMetadata.title = dash.Dashboard.Title + provisioningMetadata.identity = dashboardIdentity{title: dash.Dashboard.Title, folderID: dash.Dashboard.FolderId} if upToDate { return provisioningMetadata, nil @@ -212,12 +259,12 @@ func getProvisionedDashboardByPath(service dashboards.DashboardProvisioningServi return byPath, nil } -func getOrCreateFolderID(cfg *config, service dashboards.DashboardProvisioningService) (int64, error) { - if cfg.Folder == "" { +func getOrCreateFolderID(cfg *config, service dashboards.DashboardProvisioningService, folderName string) (int64, error) { + if folderName == "" { return 0, ErrFolderNameMissing } - cmd := &models.GetDashboardQuery{Slug: models.SlugifyTitle(cfg.Folder), OrgId: cfg.OrgID} + cmd := &models.GetDashboardQuery{Slug: models.SlugifyTitle(folderName), OrgId: cfg.OrgID} err := bus.Dispatch(cmd) if err != nil && err != models.ErrDashboardNotFound { @@ -227,7 +274,7 @@ func getOrCreateFolderID(cfg *config, service dashboards.DashboardProvisioningSe // dashboard folder not found. create one. if err == models.ErrDashboardNotFound { dash := &dashboards.SaveDashboardDTO{} - dash.Dashboard = models.NewDashboardFolder(cfg.Folder) + dash.Dashboard = models.NewDashboardFolder(folderName) dash.Dashboard.IsFolder = true dash.Overwrite = true dash.OrgId = cfg.OrgID @@ -356,29 +403,39 @@ func (fr *FileReader) resolvedPath() string { } type provisioningMetadata struct { - uid string - title string + uid string + identity dashboardIdentity +} + +type dashboardIdentity struct { + folderID int64 + title string +} + +func (d *dashboardIdentity) Exists() bool { + return len(d.title) > 0 && d.folderID > 0 } func newProvisioningSanityChecker(provisioningProvider string) provisioningSanityChecker { return provisioningSanityChecker{ provisioningProvider: provisioningProvider, uidUsage: map[string]uint8{}, - titleUsage: map[string]uint8{}} + titleUsage: map[dashboardIdentity]uint8{}, + } } type provisioningSanityChecker struct { provisioningProvider string uidUsage map[string]uint8 - titleUsage map[string]uint8 + titleUsage map[dashboardIdentity]uint8 } func (checker provisioningSanityChecker) track(pm provisioningMetadata) { if len(pm.uid) > 0 { checker.uidUsage[pm.uid]++ } - if len(pm.title) > 0 { - checker.titleUsage[pm.title]++ + if pm.identity.Exists() { + checker.titleUsage[pm.identity]++ } } @@ -389,9 +446,9 @@ func (checker provisioningSanityChecker) logWarnings(log log.Logger) { } } - for title, times := range checker.titleUsage { + for identity, times := range checker.titleUsage { if times > 1 { - log.Error("the same 'title' is used more than once", "title", title, "provider", checker.provisioningProvider) + log.Error("the same 'title' is used more than once", "title", identity.title, "provider", checker.provisioningProvider) } } } diff --git a/pkg/services/provisioning/dashboards/file_reader_test.go b/pkg/services/provisioning/dashboards/file_reader_test.go index bf5d6f70b9d..a9734043439 100644 --- a/pkg/services/provisioning/dashboards/file_reader_test.go +++ b/pkg/services/provisioning/dashboards/file_reader_test.go @@ -1,6 +1,7 @@ package dashboards import ( + "fmt" "math/rand" "os" "path/filepath" @@ -8,22 +9,22 @@ import ( "testing" "time" - "github.com/grafana/grafana/pkg/util" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/infra/log" . "github.com/smartystreets/goconvey/convey" ) var ( - defaultDashboards = "testdata/test-dashboards/folder-one" - brokenDashboards = "testdata/test-dashboards/broken-dashboards" - oneDashboard = "testdata/test-dashboards/one-dashboard" - containingID = "testdata/test-dashboards/containing-id" - unprovision = "testdata/test-dashboards/unprovision" + defaultDashboards = "testdata/test-dashboards/folder-one" + brokenDashboards = "testdata/test-dashboards/broken-dashboards" + oneDashboard = "testdata/test-dashboards/one-dashboard" + containingID = "testdata/test-dashboards/containing-id" + unprovision = "testdata/test-dashboards/unprovision" + foldersFromFilesStructure = "testdata/test-dashboards/folders-from-files-structure" fakeService *fakeDashboardProvisioningService ) @@ -52,6 +53,14 @@ func TestCreatingNewDashboardFileReader(t *testing.T) { So(reader.Path, ShouldNotEqual, "") }) + Convey("using foldersFromFilesStructure as options", func() { + cfg.Options["path"] = foldersFromFilesStructure + cfg.Options["foldersFromFilesStructure"] = true + reader, err := NewDashboardFileReader(cfg, log.New("test-logger")) + So(err, ShouldBeNil) + So(reader.Path, ShouldNotEqual, "") + }) + Convey("using full path", func() { fullPath := "/var/lib/grafana/dashboards" if runtime.GOOS == "windows" { @@ -152,6 +161,46 @@ func TestDashboardFileReader(t *testing.T) { So(len(fakeService.inserted), ShouldEqual, 1) }) + Convey("Get folder from files structure", func() { + cfg.Options["path"] = foldersFromFilesStructure + cfg.Options["foldersFromFilesStructure"] = true + + reader, err := NewDashboardFileReader(cfg, logger) + So(err, ShouldBeNil) + + err = reader.startWalkingDisk() + So(err, ShouldBeNil) + + So(len(fakeService.inserted), ShouldEqual, 5) + + foldersCount := 0 + for _, d := range fakeService.inserted { + if d.Dashboard.IsFolder { + foldersCount++ + } + } + So(foldersCount, ShouldEqual, 2) + + foldersAndDashboards := make(map[string]struct{}, 5) + for _, d := range fakeService.inserted { + title := d.Dashboard.Title + if _, ok := foldersAndDashboards[title]; ok { + So(fmt.Errorf("dashboard title %q already exists", title), ShouldBeNil) + } + + switch title { + case "folderOne", "folderTwo": + So(d.Dashboard.IsFolder, ShouldBeTrue) + case "Grafana1", "Grafana2", "RootDashboard": + So(d.Dashboard.IsFolder, ShouldBeFalse) + default: + So(fmt.Errorf("unknown dashboard title %q", title), ShouldBeNil) + } + + foldersAndDashboards[title] = struct{}{} + } + }) + Convey("Invalid configuration should return error", func() { cfg := &config{ Name: "Default", @@ -213,7 +262,7 @@ func TestDashboardFileReader(t *testing.T) { }, } - _, err := getOrCreateFolderID(cfg, fakeService) + _, err := getOrCreateFolderID(cfg, fakeService, cfg.Folder) So(err, ShouldEqual, ErrFolderNameMissing) }) @@ -228,7 +277,7 @@ func TestDashboardFileReader(t *testing.T) { }, } - folderID, err := getOrCreateFolderID(cfg, fakeService) + folderID, err := getOrCreateFolderID(cfg, fakeService, cfg.Folder) So(err, ShouldBeNil) inserted := false for _, d := range fakeService.inserted { diff --git a/pkg/services/provisioning/dashboards/testdata/test-dashboards/folders-from-files-structure/folderOne/dashboard1.json b/pkg/services/provisioning/dashboards/testdata/test-dashboards/folders-from-files-structure/folderOne/dashboard1.json new file mode 100644 index 00000000000..febb98be0e8 --- /dev/null +++ b/pkg/services/provisioning/dashboards/testdata/test-dashboards/folders-from-files-structure/folderOne/dashboard1.json @@ -0,0 +1,172 @@ +{ + "title": "Grafana1", + "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
  • Ctrl+S saves the current dashboard
  • \n
  • Ctrl+F Opens the dashboard finder
  • \n
  • Ctrl+H Hide/show row controls
  • \n
  • Click and drag graph title to move panel
  • \n
  • Hit Escape to exit graph when in fullscreen or edit mode
  • \n
  • Click the colored icon in the legend to change series color
  • \n
  • Ctrl or Shift + Click legend name to hide other series
  • \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 + } diff --git a/pkg/services/provisioning/dashboards/testdata/test-dashboards/folders-from-files-structure/folderTwo/dashboard2.json b/pkg/services/provisioning/dashboards/testdata/test-dashboards/folders-from-files-structure/folderTwo/dashboard2.json new file mode 100644 index 00000000000..9291f16d9e7 --- /dev/null +++ b/pkg/services/provisioning/dashboards/testdata/test-dashboards/folders-from-files-structure/folderTwo/dashboard2.json @@ -0,0 +1,172 @@ +{ + "title": "Grafana2", + "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
  • Ctrl+S saves the current dashboard
  • \n
  • Ctrl+F Opens the dashboard finder
  • \n
  • Ctrl+H Hide/show row controls
  • \n
  • Click and drag graph title to move panel
  • \n
  • Hit Escape to exit graph when in fullscreen or edit mode
  • \n
  • Click the colored icon in the legend to change series color
  • \n
  • Ctrl or Shift + Click legend name to hide other series
  • \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 + } diff --git a/pkg/services/provisioning/dashboards/testdata/test-dashboards/folders-from-files-structure/root.json b/pkg/services/provisioning/dashboards/testdata/test-dashboards/folders-from-files-structure/root.json new file mode 100644 index 00000000000..57b15e8a007 --- /dev/null +++ b/pkg/services/provisioning/dashboards/testdata/test-dashboards/folders-from-files-structure/root.json @@ -0,0 +1,172 @@ +{ + "title": "RootDashboard", + "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
  • Ctrl+S saves the current dashboard
  • \n
  • Ctrl+F Opens the dashboard finder
  • \n
  • Ctrl+H Hide/show row controls
  • \n
  • Click and drag graph title to move panel
  • \n
  • Hit Escape to exit graph when in fullscreen or edit mode
  • \n
  • Click the colored icon in the legend to change series color
  • \n
  • Ctrl or Shift + Click legend name to hide other series
  • \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 + }