mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge pull request #10052 from bergquist/dashboards_from_disk
Load dashboards from disk into the database at regular interval
This commit is contained in:
commit
fb386f3c8d
@ -20,8 +20,8 @@ logs = data/log
|
||||
# Directory where grafana will automatically scan and look for plugins
|
||||
plugins = data/plugins
|
||||
|
||||
# Config files containing datasources that will be configured at startup
|
||||
datasources = conf/datasources
|
||||
# folder that contains provisioning config files that grafana will apply on startup and while running.
|
||||
provisioning = conf/provisioning
|
||||
|
||||
#################################### Server ##############################
|
||||
[server]
|
||||
@ -391,11 +391,6 @@ facility =
|
||||
# Syslog tag. By default, the process' argv[0] is used.
|
||||
tag =
|
||||
|
||||
#################################### Dashboard JSON files ################
|
||||
[dashboards.json]
|
||||
enabled = false
|
||||
path = /var/lib/grafana/dashboards
|
||||
|
||||
#################################### Usage Quotas ########################
|
||||
[quota]
|
||||
enabled = false
|
||||
|
6
conf/provisioning/dashboards/custom.yaml
Normal file
6
conf/provisioning/dashboards/custom.yaml
Normal file
@ -0,0 +1,6 @@
|
||||
- name: 'default'
|
||||
org_id: 1
|
||||
folder: ''
|
||||
type: file
|
||||
options:
|
||||
folder: /var/lib/grafana/dashboards
|
6
conf/provisioning/dashboards/sample.yaml
Normal file
6
conf/provisioning/dashboards/sample.yaml
Normal file
@ -0,0 +1,6 @@
|
||||
# - name: 'default'
|
||||
# org_id: 1
|
||||
# folder: ''
|
||||
# type: file
|
||||
# options:
|
||||
# folder: /var/lib/grafana/dashboards
|
48
conf/provisioning/datasources/custom.yaml
Normal file
48
conf/provisioning/datasources/custom.yaml
Normal file
@ -0,0 +1,48 @@
|
||||
# list of datasources that should be deleted from the database
|
||||
delete_datasources:
|
||||
- name: Graphite
|
||||
org_id: 1
|
||||
|
||||
# list of datasources to insert/update depending
|
||||
# whats available in the datbase
|
||||
datasources:
|
||||
# <string, required> name of the datasource. Required
|
||||
- name: Graphite
|
||||
# <string, required> datasource type. Required
|
||||
type: graphite
|
||||
# <string, required> access mode. direct or proxy. Required
|
||||
access: proxy
|
||||
# <int> org id. will default to org_id 1 if not specified
|
||||
org_id: 1
|
||||
# <string> url
|
||||
url: http://localhost:8080
|
||||
# <string> database password, if used
|
||||
password:
|
||||
# <string> database user, if used
|
||||
user:
|
||||
# <string> database name, if used
|
||||
database:
|
||||
# <bool> enable/disable basic auth
|
||||
basic_auth:
|
||||
# <string> basic auth username
|
||||
basic_auth_user:
|
||||
# <string> basic auth password
|
||||
basic_auth_password:
|
||||
# <bool> enable/disable with credentials headers
|
||||
with_credentials:
|
||||
# <bool> mark as default datasource. Max one per org
|
||||
is_default:
|
||||
# <map> fields that will be converted to json and stored in json_data
|
||||
json_data:
|
||||
graphiteVersion: "1.1"
|
||||
tlsAuth: true
|
||||
tlsAuthWithCACert: true
|
||||
# <string> json object of data that will be encrypted.
|
||||
secure_json_data:
|
||||
tlsCACert: "..."
|
||||
tlsClientCert: "..."
|
||||
tlsClientKey: "..."
|
||||
version: 1
|
||||
# <bool> allow users to edit datasources from the UI.
|
||||
editable: false
|
||||
|
@ -20,8 +20,8 @@
|
||||
# Directory where grafana will automatically scan and look for plugins
|
||||
;plugins = /var/lib/grafana/plugins
|
||||
|
||||
# Config files containing datasources that will be configured at startup
|
||||
;datasources = conf/datasources
|
||||
# folder that contains provisioning config files that grafana will apply on startup and while running.
|
||||
; provisioning = conf/provisioning
|
||||
|
||||
#################################### Server ####################################
|
||||
[server]
|
||||
@ -367,11 +367,6 @@ log_queries =
|
||||
;tag =
|
||||
|
||||
|
||||
;#################################### Dashboard JSON files ##########################
|
||||
[dashboards.json]
|
||||
;enabled = false
|
||||
;path = /var/lib/grafana/dashboards
|
||||
|
||||
#################################### Alerting ############################
|
||||
[alerting]
|
||||
# Disable alerting engine & UI features
|
||||
|
@ -74,7 +74,7 @@ Saltstack | [https://github.com/salt-formulas/salt-formula-grafana](https://gith
|
||||
|
||||
> This feature is available from v5.0
|
||||
|
||||
It's possible to manage datasources in Grafana by adding one or more yaml config files in the [`conf/datasources`](/installation/configuration/#datasources) directory. Each config file can contain a list of `datasources` that will be added or updated during start up. If the datasource already exists, Grafana will update it to match the configuration file. The config file can also contain a list of datasources that should be deleted. That list is called `delete_datasources`. Grafana will delete datasources listed in `delete_datasources` before inserting/updating those in the `datasource` list.
|
||||
It's possible to manage datasources in Grafana by adding one or more yaml config files in the [`provisioning/datasources`](/installation/configuration/#provisioning) directory. Each config file can contain a list of `datasources` that will be added or updated during start up. If the datasource already exists, Grafana will update it to match the configuration file. The config file can also contain a list of datasources that should be deleted. That list is called `delete_datasources`. Grafana will delete datasources listed in `delete_datasources` before inserting/updating those in the `datasource` list.
|
||||
|
||||
### Running multiple grafana instances.
|
||||
If you are running multiple instances of Grafana you might run into problems if they have different versions of the datasource.yaml configuration file. The best way to solve this problem is to add a version number to each datasource in the configuration and increase it when you update the config. Grafana will only update datasources with the same or lower version number than specified in the config. That way old configs cannot overwrite newer configs if they restart at the same time.
|
||||
@ -165,3 +165,20 @@ Secure json data is a map of settings that will be encrypted with [secret key](/
|
||||
| tlsClientKey | string | *All* |TLS Client key for outgoing requests |
|
||||
| password | string | Postgre | password |
|
||||
| user | string | Postgre | user |
|
||||
|
||||
### Dashboards
|
||||
|
||||
It's possible to manage dashboards in Grafana by adding one or more yaml config files in the [`provisioning/dashboards`](/installation/configuration/#provisioning) directory. Each config file can contain a list of `dashboards providers` that will load dashboards into grafana. Currently we only support reading dashboards from file but we will add more providers in the future.
|
||||
|
||||
The dashboard provider config file looks like this
|
||||
|
||||
```yaml
|
||||
- name: 'default'
|
||||
org_id: 1
|
||||
folder: ''
|
||||
type: file
|
||||
options:
|
||||
folder: /var/lib/grafana/dashboards
|
||||
```
|
||||
|
||||
When grafana starts it will update/insert all dashboards available in the configured folders. If you modify the file the dashboard will also be updated.
|
@ -91,12 +91,11 @@ file.
|
||||
|
||||
Directory where grafana will automatically scan and look for plugins
|
||||
|
||||
### datasources
|
||||
### provisioning
|
||||
|
||||
> This feature is available in 5.0+
|
||||
|
||||
Config files containing datasources that will be configured at startup.
|
||||
You can read more about the config files at the [provisioning page](/administration/provisioning/#datasources).
|
||||
Folder that contains [provisioning](/administration/provisioning) config files that grafana will apply on startup. Dashboards will be reloaded when the json files changes
|
||||
|
||||
## [server]
|
||||
|
||||
@ -635,8 +634,7 @@ Number dashboard versions to keep (per dashboard). Default: 20, Minimum: 1.
|
||||
|
||||
## [dashboards.json]
|
||||
|
||||
If you have a system that automatically builds dashboards as json files you can enable this feature to have the
|
||||
Grafana backend index those json dashboards which will make them appear in regular dashboard search.
|
||||
> This have been replaced with dashboards [provisioning](/administration/provisioning) in 5.0+
|
||||
|
||||
### enabled
|
||||
`true` or `false`. Is disabled by default.
|
||||
|
@ -31,6 +31,12 @@ case "$1" in
|
||||
cp /usr/share/grafana/conf/ldap.toml /etc/grafana/ldap.toml
|
||||
fi
|
||||
|
||||
if [ ! -f $PROVISIONING_CFG_DIR ]; then
|
||||
mkdir -p $PROVISIONING_CFG_DIR/dashboards $PROVISIONING_CFG_DIR/datasources
|
||||
cp /usr/share/grafana/conf/provisioning/dashboards/sample.yaml $PROVISIONING_CFG_DIR/dashboards/sample.yaml
|
||||
cp /usr/share/grafana/conf/provisioning/datasources/sample.yaml $PROVISIONING_CFG_DIR/datasources/sample.yaml
|
||||
fi
|
||||
|
||||
# configuration files should not be modifiable by grafana user, as this can be a security issue
|
||||
chown -Rh root:$GRAFANA_GROUP /etc/grafana/*
|
||||
chmod 755 /etc/grafana
|
||||
|
@ -18,5 +18,7 @@ RESTART_ON_UPGRADE=true
|
||||
|
||||
PLUGINS_DIR=/var/lib/grafana/plugins
|
||||
|
||||
PROVISIONING_CFG_DIR=/etc/grafana/provisioning
|
||||
|
||||
# Only used on systemd systems
|
||||
PID_FILE_DIR=/var/run/grafana
|
||||
|
@ -33,6 +33,7 @@ DATA_DIR=/var/lib/grafana
|
||||
PLUGINS_DIR=/var/lib/grafana/plugins
|
||||
LOG_DIR=/var/log/grafana
|
||||
CONF_FILE=$CONF_DIR/grafana.ini
|
||||
PROVISIONING_CFG_DIR=$CONF_DIR/provisioning
|
||||
MAX_OPEN_FILES=10000
|
||||
PID_FILE=/var/run/$NAME.pid
|
||||
DAEMON=/usr/sbin/$NAME
|
||||
@ -55,7 +56,7 @@ if [ -f "$DEFAULT" ]; then
|
||||
. "$DEFAULT"
|
||||
fi
|
||||
|
||||
DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR} cfg:default.paths.plugins=${PLUGINS_DIR}"
|
||||
DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} cfg:default.paths.provisioning=$PROVISIONING_CFG_DIR cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR} cfg:default.paths.plugins=${PLUGINS_DIR}"
|
||||
|
||||
function checkUser() {
|
||||
if [ `id -u` -ne 0 ]; then
|
||||
|
@ -14,12 +14,15 @@ Restart=on-failure
|
||||
WorkingDirectory=/usr/share/grafana
|
||||
RuntimeDirectory=grafana
|
||||
RuntimeDirectoryMode=0750
|
||||
ExecStart=/usr/sbin/grafana-server \
|
||||
--config=${CONF_FILE} \
|
||||
--pidfile=${PID_FILE_DIR}/grafana-server.pid \
|
||||
cfg:default.paths.logs=${LOG_DIR} \
|
||||
cfg:default.paths.data=${DATA_DIR} \
|
||||
cfg:default.paths.plugins=${PLUGINS_DIR}
|
||||
ExecStart=/usr/sbin/grafana-server \
|
||||
--config=${CONF_FILE} \
|
||||
--pidfile=${PID_FILE_DIR}/grafana-server.pid \
|
||||
cfg:default.paths.logs=${LOG_DIR} \
|
||||
cfg:default.paths.data=${DATA_DIR} \
|
||||
cfg:default.paths.plugins=${PLUGINS_DIR} \
|
||||
cfg:default.paths.provisioning=${PROVISIONING_CFG_DIR}
|
||||
|
||||
|
||||
LimitNOFILE=10000
|
||||
TimeoutStopSec=20
|
||||
UMask=0027
|
||||
|
@ -6,10 +6,12 @@ HOMEPATH=/usr/local/share/grafana
|
||||
LOGPATH=/usr/local/var/log/grafana
|
||||
DATAPATH=/usr/local/var/lib/grafana
|
||||
PLUGINPATH=/usr/local/var/lib/grafana/plugins
|
||||
DATASOURCECFGPATH=/usr/local/etc/grafana/datasources
|
||||
DASHBOARDSCFGPATH=/usr/local/etc/grafana/dashboards
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
$EXECUTABLE --config=$CONFIG --homepath=$HOMEPATH cfg:default.paths.logs=$LOGPATH cfg:default.paths.data=$DATAPATH cfg:default.paths.plugins=$PLUGINPATH 2> /dev/null &
|
||||
$EXECUTABLE --config=$CONFIG --homepath=$HOMEPATH cfg:default.paths.datasources=$DATASOURCECFGPATH cfg:default.paths.dashboards=$DASHBOARDSCFGPATH cfg:default.paths.logs=$LOGPATH cfg:default.paths.data=$DATAPATH cfg:default.paths.plugins=$PLUGINPATH 2> /dev/null &
|
||||
[ $? -eq 0 ] && echo "$DAEMON started"
|
||||
;;
|
||||
stop)
|
||||
|
@ -45,6 +45,12 @@ if [ $1 -eq 1 ] ; then
|
||||
cp /usr/share/grafana/conf/ldap.toml /etc/grafana/ldap.toml
|
||||
fi
|
||||
|
||||
if [ ! -f $PROVISIONING_CFG_DIR ]; then
|
||||
mkdir -p $PROVISIONING_CFG_DIR/dashboards $PROVISIONING_CFG_DIR/datasources
|
||||
cp /usr/share/grafana/conf/provisioning/dashboards/sample.yaml $PROVISIONING_CFG_DIR/dashboards/sample.yaml
|
||||
cp /usr/share/grafana/conf/provisioning/datasources/sample.yaml $PROVISIONING_CFG_DIR/datasources/sample.yaml
|
||||
fi
|
||||
|
||||
# Set user permissions on /var/log/grafana, /var/lib/grafana
|
||||
mkdir -p /var/log/grafana /var/lib/grafana
|
||||
chown -R $GRAFANA_USER:$GRAFANA_GROUP /var/log/grafana /var/lib/grafana
|
||||
|
@ -32,6 +32,7 @@ DATA_DIR=/var/lib/grafana
|
||||
PLUGINS_DIR=/var/lib/grafana/plugins
|
||||
LOG_DIR=/var/log/grafana
|
||||
CONF_FILE=$CONF_DIR/grafana.ini
|
||||
PROVISIONING_CFG_DIR=$CONF_DIR/provisioning
|
||||
MAX_OPEN_FILES=10000
|
||||
PID_FILE=/var/run/$NAME.pid
|
||||
DAEMON=/usr/sbin/$NAME
|
||||
@ -59,7 +60,7 @@ fi
|
||||
# overwrite settings from default file
|
||||
[ -e /etc/sysconfig/$NAME ] && . /etc/sysconfig/$NAME
|
||||
|
||||
DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR} cfg:default.paths.plugins=${PLUGINS_DIR}"
|
||||
DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} cfg:default.paths.provisioning=$PROVISIONING_CFG_DIR cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR} cfg:default.paths.plugins=${PLUGINS_DIR}"
|
||||
|
||||
function isRunning() {
|
||||
status -p $PID_FILE $NAME > /dev/null 2>&1
|
||||
|
@ -18,5 +18,7 @@ RESTART_ON_UPGRADE=true
|
||||
|
||||
PLUGINS_DIR=/var/lib/grafana/plugins
|
||||
|
||||
PROVISIONING_CFG_DIR=/etc/grafana/provisioning
|
||||
|
||||
# Only used on systemd systems
|
||||
PID_FILE_DIR=/var/run/grafana
|
||||
|
@ -14,12 +14,14 @@ Restart=on-failure
|
||||
WorkingDirectory=/usr/share/grafana
|
||||
RuntimeDirectory=grafana
|
||||
RuntimeDirectoryMode=0750
|
||||
ExecStart=/usr/sbin/grafana-server \
|
||||
--config=${CONF_FILE} \
|
||||
--pidfile=${PID_FILE_DIR}/grafana-server.pid \
|
||||
cfg:default.paths.logs=${LOG_DIR} \
|
||||
cfg:default.paths.data=${DATA_DIR} \
|
||||
cfg:default.paths.plugins=${PLUGINS_DIR}
|
||||
ExecStart=/usr/sbin/grafana-server \
|
||||
--config=${CONF_FILE} \
|
||||
--pidfile=${PID_FILE_DIR}/grafana-server.pid \
|
||||
cfg:default.paths.logs=${LOG_DIR} \
|
||||
cfg:default.paths.data=${DATA_DIR} \
|
||||
cfg:default.paths.plugins=${PLUGINS_DIR} \
|
||||
cfg:default.paths.provisioning=${PROVISIONING_CFG_DIR}
|
||||
|
||||
LimitNOFILE=10000
|
||||
TimeoutStopSec=20
|
||||
|
||||
|
@ -7,6 +7,8 @@ import (
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/dashdiffs"
|
||||
@ -16,7 +18,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
"github.com/grafana/grafana/pkg/services/search"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
@ -124,11 +125,6 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
|
||||
|
||||
dash := cmd.GetDashboardModel()
|
||||
|
||||
// Check if Title is empty
|
||||
if dash.Title == "" {
|
||||
return ApiError(400, m.ErrDashboardTitleEmpty.Error(), nil)
|
||||
}
|
||||
|
||||
if dash.Id == 0 {
|
||||
limitReached, err := middleware.QuotaReached(c, "dashboard")
|
||||
if err != nil {
|
||||
@ -139,17 +135,23 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
|
||||
}
|
||||
}
|
||||
|
||||
validateAlertsCmd := alerting.ValidateDashboardAlertsCommand{
|
||||
dashItem := &dashboards.SaveDashboardItem{
|
||||
Dashboard: dash,
|
||||
Message: cmd.Message,
|
||||
OrgId: c.OrgId,
|
||||
UserId: c.UserId,
|
||||
Dashboard: dash,
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&validateAlertsCmd); err != nil {
|
||||
dashboard, err := dashboards.GetRepository().SaveDashboard(dashItem)
|
||||
|
||||
if err == m.ErrDashboardTitleEmpty {
|
||||
return ApiError(400, m.ErrDashboardTitleEmpty.Error(), nil)
|
||||
}
|
||||
|
||||
if err == m.ErrDashboardContainsInvalidAlertData {
|
||||
return ApiError(500, "Invalid alert data. Cannot save dashboard", err)
|
||||
}
|
||||
|
||||
err := bus.Dispatch(&cmd)
|
||||
if err != nil {
|
||||
if err == m.ErrDashboardWithSameNameExists {
|
||||
return Json(412, util.DynMap{"status": "name-exists", "message": err.Error()})
|
||||
@ -171,18 +173,12 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
|
||||
return ApiError(500, "Failed to save dashboard", err)
|
||||
}
|
||||
|
||||
alertCmd := alerting.UpdateDashboardAlertsCommand{
|
||||
OrgId: c.OrgId,
|
||||
UserId: c.UserId,
|
||||
Dashboard: cmd.Result,
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&alertCmd); err != nil {
|
||||
return ApiError(500, "Failed to save alerts", err)
|
||||
if err == m.ErrDashboardFailedToUpdateAlertData {
|
||||
return ApiError(500, "Invalid alert data. Cannot save dashboard", err)
|
||||
}
|
||||
|
||||
c.TimeRequest(metrics.M_Api_Dashboard_Save)
|
||||
return Json(200, util.DynMap{"status": "success", "slug": cmd.Result.Slug, "version": cmd.Result.Version})
|
||||
return Json(200, util.DynMap{"status": "success", "slug": dashboard.Slug, "version": dashboard.Version})
|
||||
}
|
||||
|
||||
func canEditDashboard(role m.RoleType) bool {
|
||||
|
@ -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
|
||||
|
@ -11,11 +11,13 @@ 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")
|
||||
ErrDashboardFailedToUpdateAlertData = errors.New("Failed to save alert data")
|
||||
)
|
||||
|
||||
type UpdatePluginDashboardError struct {
|
||||
@ -139,6 +141,8 @@ type SaveDashboardCommand struct {
|
||||
RestoredFrom int `json:"-"`
|
||||
PluginId string `json:"-"`
|
||||
|
||||
UpdatedAt time.Time
|
||||
|
||||
Result *Dashboard
|
||||
}
|
||||
|
||||
|
82
pkg/services/dashboards/dashboards.go
Normal file
82
pkg/services/dashboards/dashboards.go
Normal file
@ -0,0 +1,82 @@
|
||||
package dashboards
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
SaveDashboard(*SaveDashboardItem) (*models.Dashboard, error)
|
||||
}
|
||||
|
||||
var repositoryInstance Repository
|
||||
|
||||
func GetRepository() Repository {
|
||||
return repositoryInstance
|
||||
}
|
||||
|
||||
func SetRepository(rep Repository) {
|
||||
repositoryInstance = rep
|
||||
}
|
||||
|
||||
type SaveDashboardItem struct {
|
||||
TitleLower string
|
||||
OrgId int64
|
||||
Folder string
|
||||
UpdatedAt time.Time
|
||||
UserId int64
|
||||
Message string
|
||||
Overwrite bool
|
||||
Dashboard *models.Dashboard
|
||||
}
|
||||
|
||||
type DashboardRepository struct{}
|
||||
|
||||
func (dr *DashboardRepository) SaveDashboard(json *SaveDashboardItem) (*models.Dashboard, error) {
|
||||
dashboard := json.Dashboard
|
||||
|
||||
if dashboard.Title == "" {
|
||||
return nil, models.ErrDashboardTitleEmpty
|
||||
}
|
||||
|
||||
validateAlertsCmd := alerting.ValidateDashboardAlertsCommand{
|
||||
OrgId: json.OrgId,
|
||||
Dashboard: dashboard,
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&validateAlertsCmd); err != nil {
|
||||
return nil, models.ErrDashboardContainsInvalidAlertData
|
||||
}
|
||||
|
||||
cmd := models.SaveDashboardCommand{
|
||||
Dashboard: dashboard.Data,
|
||||
Message: json.Message,
|
||||
OrgId: json.OrgId,
|
||||
Overwrite: json.Overwrite,
|
||||
UserId: json.UserId,
|
||||
}
|
||||
|
||||
if !json.UpdatedAt.IsZero() {
|
||||
cmd.UpdatedAt = json.UpdatedAt
|
||||
}
|
||||
|
||||
err := bus.Dispatch(&cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
alertCmd := alerting.UpdateDashboardAlertsCommand{
|
||||
OrgId: json.OrgId,
|
||||
UserId: json.UserId,
|
||||
Dashboard: cmd.Result,
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&alertCmd); err != nil {
|
||||
return nil, models.ErrDashboardFailedToUpdateAlertData
|
||||
}
|
||||
|
||||
return cmd.Result, nil
|
||||
}
|
49
pkg/services/provisioning/dashboards/config_reader.go
Normal file
49
pkg/services/provisioning/dashboards/config_reader.go
Normal file
@ -0,0 +1,49 @@
|
||||
package dashboards
|
||||
|
||||
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
|
||||
}
|
48
pkg/services/provisioning/dashboards/dashboard.go
Normal file
48
pkg/services/provisioning/dashboards/dashboard.go
Normal file
@ -0,0 +1,48 @@
|
||||
package dashboards
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
err := d.Provision(ctx)
|
||||
return d, err
|
||||
}
|
||||
|
||||
func (provider *DashboardProvisioner) Provision(ctx context.Context) error {
|
||||
cfgs, err := provider.cfgReader.readConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, cfg := range cfgs {
|
||||
switch cfg.Type {
|
||||
case "file":
|
||||
fileReader, err := NewDashboardFileReader(cfg, provider.log.New("type", cfg.Type, "name", cfg.Name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go fileReader.ReadAndListen(ctx)
|
||||
default:
|
||||
return fmt.Errorf("type %s is not supported", cfg.Type)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
49
pkg/services/provisioning/dashboards/dashboard_test.go
Normal file
49
pkg/services/provisioning/dashboards/dashboard_test.go
Normal file
@ -0,0 +1,49 @@
|
||||
package dashboards
|
||||
|
||||
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(ds.Editable, ShouldBeTrue)
|
||||
|
||||
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(ds2.Editable, ShouldBeFalse)
|
||||
|
||||
So(len(ds2.Options), ShouldEqual, 1)
|
||||
So(ds2.Options["folder"], ShouldEqual, "/var/lib/grafana/dashboards")
|
||||
})
|
||||
})
|
||||
}
|
175
pkg/services/provisioning/dashboards/file_reader.go
Normal file
175
pkg/services/provisioning/dashboards/file_reader.go
Normal file
@ -0,0 +1,175 @@
|
||||
package dashboards
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
|
||||
"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"
|
||||
gocache "github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type fileReader struct {
|
||||
Cfg *DashboardsAsConfig
|
||||
Path string
|
||||
log log.Logger
|
||||
dashboardRepo dashboards.Repository
|
||||
cache *gocache.Cache
|
||||
}
|
||||
|
||||
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,
|
||||
dashboardRepo: dashboards.GetRepository(),
|
||||
cache: gocache.New(5*time.Minute, 30*time.Minute),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (fr *fileReader) addCache(key string, json *dashboards.SaveDashboardItem) {
|
||||
fr.cache.Add(key, json, time.Minute*10)
|
||||
}
|
||||
|
||||
func (fr *fileReader) getCache(key string) (*dashboards.SaveDashboardItem, bool) {
|
||||
obj, exist := fr.cache.Get(key)
|
||||
if !exist {
|
||||
return nil, exist
|
||||
}
|
||||
|
||||
dash, ok := obj.(*dashboards.SaveDashboardItem)
|
||||
if !ok {
|
||||
return nil, ok
|
||||
}
|
||||
|
||||
return dash, ok
|
||||
}
|
||||
|
||||
func (fr *fileReader) ReadAndListen(ctx context.Context) error {
|
||||
ticker := time.NewTicker(time.Second * 3)
|
||||
|
||||
if err := fr.walkFolder(); err != nil {
|
||||
fr.log.Error("failed to search for dashboards", "error", err)
|
||||
}
|
||||
|
||||
running := false
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if !running { // avoid walking the filesystem in parallel. incase fs is very slow.
|
||||
running = true
|
||||
go func() {
|
||||
fr.walkFolder()
|
||||
running = false
|
||||
}()
|
||||
}
|
||||
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, fileInfo os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if fileInfo.IsDir() {
|
||||
if strings.HasPrefix(fileInfo.Name(), ".") {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(fileInfo.Name(), ".json") {
|
||||
return nil
|
||||
}
|
||||
|
||||
cachedDashboard, exist := fr.getCache(path)
|
||||
if exist && cachedDashboard.UpdatedAt == fileInfo.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 we dont have the dashboard in the db, save it!
|
||||
if err == models.ErrDashboardNotFound {
|
||||
fr.log.Debug("saving new dashboard", "file", path)
|
||||
_, err = fr.dashboardRepo.SaveDashboard(dash)
|
||||
return err
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
fr.log.Error("failed to query for dashboard", "slug", dash.Dashboard.Slug, "error", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// break if db version is newer then fil version
|
||||
if cmd.Result.Updated.Unix() >= fileInfo.ModTime().Unix() {
|
||||
return nil
|
||||
}
|
||||
|
||||
fr.log.Debug("loading dashboard from disk into database.", "file", path)
|
||||
_, err = fr.dashboardRepo.SaveDashboard(dash)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func (fr *fileReader) readDashboardFromFile(path string) (*dashboards.SaveDashboardItem, 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, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dash, err := createDashboardJson(data, stat.ModTime(), fr.Cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fr.addCache(path, dash)
|
||||
|
||||
return dash, nil
|
||||
}
|
137
pkg/services/provisioning/dashboards/file_reader_test.go
Normal file
137
pkg/services/provisioning/dashboards/file_reader_test.go
Normal file
@ -0,0 +1,137 @@
|
||||
package dashboards
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultDashboards string = "./test-dashboards/folder-one"
|
||||
brokenDashboards string = "./test-dashboards/broken-dashboards"
|
||||
oneDashboard string = "./test-dashboards/one-dashboard"
|
||||
|
||||
fakeRepo *fakeDashboardRepo
|
||||
)
|
||||
|
||||
func TestDashboardFileReader(t *testing.T) {
|
||||
Convey("Reading dashboards from disk", t, func() {
|
||||
bus.ClearBusHandlers()
|
||||
fakeRepo = &fakeDashboardRepo{}
|
||||
|
||||
bus.AddHandler("test", mockGetDashboardQuery)
|
||||
dashboards.SetRepository(fakeRepo)
|
||||
logger := log.New("test.logger")
|
||||
|
||||
cfg := &DashboardsAsConfig{
|
||||
Name: "Default",
|
||||
Type: "file",
|
||||
OrgId: 1,
|
||||
Folder: "",
|
||||
Options: map[string]interface{}{},
|
||||
}
|
||||
|
||||
Convey("Can read default dashboard", func() {
|
||||
cfg.Options["folder"] = defaultDashboards
|
||||
|
||||
reader, err := NewDashboardFileReader(cfg, logger)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
err = reader.walkFolder()
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(len(fakeRepo.inserted), ShouldEqual, 2)
|
||||
})
|
||||
|
||||
Convey("Should not update dashboards when db is newer", func() {
|
||||
cfg.Options["folder"] = oneDashboard
|
||||
|
||||
fakeRepo.getDashboard = append(fakeRepo.getDashboard, &models.Dashboard{
|
||||
Updated: time.Now().Add(time.Hour),
|
||||
Slug: "grafana",
|
||||
})
|
||||
|
||||
reader, err := NewDashboardFileReader(cfg, logger)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
err = reader.walkFolder()
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(len(fakeRepo.inserted), ShouldEqual, 0)
|
||||
})
|
||||
|
||||
Convey("Can read default dashboard and replace old version in database", func() {
|
||||
cfg.Options["folder"] = oneDashboard
|
||||
|
||||
stat, _ := os.Stat(oneDashboard + "/dashboard1.json")
|
||||
|
||||
fakeRepo.getDashboard = append(fakeRepo.getDashboard, &models.Dashboard{
|
||||
Updated: stat.ModTime().AddDate(0, 0, -1),
|
||||
Slug: "grafana",
|
||||
})
|
||||
|
||||
reader, err := NewDashboardFileReader(cfg, logger)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
err = reader.walkFolder()
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(len(fakeRepo.inserted), ShouldEqual, 1)
|
||||
})
|
||||
|
||||
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)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
type fakeDashboardRepo struct {
|
||||
inserted []*dashboards.SaveDashboardItem
|
||||
getDashboard []*models.Dashboard
|
||||
}
|
||||
|
||||
func (repo *fakeDashboardRepo) SaveDashboard(json *dashboards.SaveDashboardItem) (*models.Dashboard, error) {
|
||||
repo.inserted = append(repo.inserted, json)
|
||||
return json.Dashboard, nil
|
||||
}
|
||||
|
||||
func mockGetDashboardQuery(cmd *models.GetDashboardQuery) error {
|
||||
for _, d := range fakeRepo.getDashboard {
|
||||
if d.Slug == cmd.Slug {
|
||||
cmd.Result = d
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return models.ErrDashboardNotFound
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
- name: 'general dashboards'
|
||||
org_id: 2
|
||||
folder: 'developers'
|
||||
editable: true
|
||||
type: file
|
||||
options:
|
||||
folder: /var/lib/grafana/dashboards
|
||||
|
||||
- name: 'default'
|
||||
type: file
|
||||
options:
|
||||
folder: /var/lib/grafana/dashboards
|
@ -0,0 +1,6 @@
|
||||
[]
|
||||
{
|
||||
"title": "Grafana",
|
||||
|
||||
}
|
||||
|
@ -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": "<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
|
||||
}
|
||||
|
@ -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": "<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
|
||||
}
|
||||
|
@ -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": "<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
|
||||
}
|
||||
|
38
pkg/services/provisioning/dashboards/types.go
Normal file
38
pkg/services/provisioning/dashboards/types.go
Normal file
@ -0,0 +1,38 @@
|
||||
package dashboards
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
|
||||
"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"`
|
||||
Editable bool `json:"editable" yaml:"editable"`
|
||||
Options map[string]interface{} `json:"options" yaml:"options"`
|
||||
}
|
||||
|
||||
func createDashboardJson(data *simplejson.Json, lastModified time.Time, cfg *DashboardsAsConfig) (*dashboards.SaveDashboardItem, error) {
|
||||
|
||||
dash := &dashboards.SaveDashboardItem{}
|
||||
dash.Dashboard = models.NewDashboardFromJson(data)
|
||||
dash.TitleLower = strings.ToLower(dash.Dashboard.Title)
|
||||
dash.UpdatedAt = lastModified
|
||||
dash.Overwrite = true
|
||||
dash.OrgId = cfg.OrgId
|
||||
dash.Folder = cfg.Folder
|
||||
dash.Dashboard.Data.Set("editable", cfg.Editable)
|
||||
|
||||
if dash.Dashboard.Title == "" {
|
||||
return nil, models.ErrDashboardTitleEmpty
|
||||
}
|
||||
|
||||
return dash, nil
|
||||
}
|
@ -1,14 +1,35 @@
|
||||
package provisioning
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"context"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/provisioning/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/provisioning/datasources"
|
||||
ini "gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
var (
|
||||
logger log.Logger = log.New("services.provisioning")
|
||||
)
|
||||
func Init(ctx context.Context, homePath string, cfg *ini.File) error {
|
||||
provisioningPath := makeAbsolute(cfg.Section("paths").Key("provisioning").String(), homePath)
|
||||
|
||||
func StartUp(datasourcePath string) error {
|
||||
return datasources.Provision(datasourcePath)
|
||||
datasourcePath := path.Join(provisioningPath, "datasources")
|
||||
if err := datasources.Provision(datasourcePath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dashboardPath := path.Join(provisioningPath, "dashboards")
|
||||
_, err := dashboards.Provision(ctx, dashboardPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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 {
|
||||
} 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)
|
||||
}
|
||||
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/annotations"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrations"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@ -100,6 +101,7 @@ func SetEngine(engine *xorm.Engine) (err error) {
|
||||
|
||||
// Init repo instances
|
||||
annotations.SetRepository(&SqlAnnotationRepo{})
|
||||
dashboards.SetRepository(&dashboards.DashboardRepository{})
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -50,12 +50,12 @@ var (
|
||||
BuildStamp int64
|
||||
|
||||
// Paths
|
||||
LogsPath string
|
||||
HomePath string
|
||||
DataPath string
|
||||
PluginsPath string
|
||||
DatasourcesPath string
|
||||
CustomInitPath = "conf/custom.ini"
|
||||
LogsPath string
|
||||
HomePath string
|
||||
DataPath string
|
||||
PluginsPath string
|
||||
ProvisioningPath string
|
||||
CustomInitPath = "conf/custom.ini"
|
||||
|
||||
// Log settings.
|
||||
LogModes []string
|
||||
@ -474,8 +474,7 @@ func NewConfigContext(args *CommandLineArgs) error {
|
||||
Env = Cfg.Section("").Key("app_mode").MustString("development")
|
||||
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)
|
||||
|
||||
ProvisioningPath = makeAbsolute(Cfg.Section("paths").Key("provisioning").String(), HomePath)
|
||||
server := Cfg.Section("server")
|
||||
AppUrl, AppSubUrl = parseAppUrlAndSubUrl(server)
|
||||
|
||||
@ -670,6 +669,6 @@ func LogConfigurationInfo() {
|
||||
logger.Info("Path Data", "path", DataPath)
|
||||
logger.Info("Path Logs", "path", LogsPath)
|
||||
logger.Info("Path Plugins", "path", PluginsPath)
|
||||
logger.Info("Path Datasources", "path", DatasourcesPath)
|
||||
logger.Info("Path Provisioning", "path", ProvisioningPath)
|
||||
logger.Info("App mode " + Env)
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ module.exports = function(grunt) {
|
||||
});
|
||||
grunt.config('copy.backend_files', {
|
||||
expand: true,
|
||||
src: ['conf/*', 'vendor/phantomjs/*', 'scripts/*'],
|
||||
src: ['conf/**', 'vendor/phantomjs/*', 'scripts/*'],
|
||||
options: { mode: true},
|
||||
dest: '<%= tempDir %>'
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user