Skip to content

Commit

Permalink
Dashboard: Adds support for a global minimum dashboard refresh interv…
Browse files Browse the repository at this point in the history
…al (grafana#19416)

This feature would provide a way for administrators to limit the minimum 
dashboard refresh interval globally.
Filters out the refresh intervals available in the time picker that are lower 
than the set minimum refresh interval in the configuration .ini file
Adds the minimum refresh interval as available in the time picker.
If the user tries to enter a refresh interval that is lower than the minimum 
in the URL, defaults to the minimum interval.
When trying to update the JSON via the API, rejects the update if the 
dashboard's refresh interval is lower than the minimum.
When trying to update a dashboard via provisioning having a lower 
refresh interval than the minimum, defaults to the minimum interval 
and logs a warning. 

Fixes grafana#3356

Co-authored-by: Marcus Efraimsson <[email protected]>
  • Loading branch information
lfroment0 and marefr authored Feb 28, 2020
1 parent 1db8849 commit 72628c8
Show file tree
Hide file tree
Showing 17 changed files with 168 additions and 10 deletions.
4 changes: 4 additions & 0 deletions conf/defaults.ini
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,10 @@ snapshot_remove_expired = true
# Number dashboard versions to keep (per dashboard). Default: 20, Minimum: 1
versions_to_keep = 20

# Minimum dashboard refresh interval. When set, this will restrict users to set the refresh interval of a dashboard lower than given interval. Per default this is not set/unrestricted.
# The interval string is a possibly signed sequence of decimal numbers, followed by a unit suffix (ms, s, m, h, d), e.g. 30s or 1m.
min_refresh_interval =

#################################### Users ###############################
[users]
# disable user signup / registration
Expand Down
4 changes: 4 additions & 0 deletions conf/sample.ini
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,10 @@
# Number dashboard versions to keep (per dashboard). Default: 20, Minimum: 1
;versions_to_keep = 20

# Minimum dashboard refresh interval. When set, this will restrict users to set the refresh interval of a dashboard lower than given interval. Per default this is not set/unrestricted.
# The interval string is a possibly signed sequence of decimal numbers, followed by a unit suffix (ms, s, m, h, d), e.g. 30s or 1m.
;min_refresh_interval =

#################################### Users ###############################
[users]
# disable user signup / registration
Expand Down
7 changes: 7 additions & 0 deletions docs/sources/installation/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,13 @@ Set to false to disable all checks to https://grafana.com for new versions of in

Number dashboard versions to keep (per dashboard). Default: `20`, Minimum: `1`.

### min_refresh_interval

> Only available in Grafana v6.7+.
When set, this will restrict users to set the refresh interval of a dashboard lower than given interval. Per default this is not set/unrestricted.
The interval string is a possibly signed sequence of decimal numbers, followed by a unit suffix (ms, s, m, h, d), e.g. `30s` or `1m`.

## [dashboards.json]

> This have been replaced with dashboards [provisioning]({{< relref "../administration/provisioning" >}}) in 5.0+
Expand Down
1 change: 1 addition & 0 deletions packages/grafana-runtime/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ interface LicenseInfo {
export class GrafanaBootConfig {
datasources: { [str: string]: DataSourceInstanceSettings } = {};
panels: { [key: string]: PanelPluginMeta } = {};
minRefreshInterval = '';
appSubUrl = '';
windowTitlePrefix = '';
buildInfo: BuildInfo = {} as BuildInfo;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import memoizeOne from 'memoize-one';
import { GrafanaTheme } from '@grafana/data';
import { withTheme } from '../../themes';

const defaultIntervals = ['5s', '10s', '30s', '1m', '5m', '15m', '30m', '1h', '2h', '1d'];
export const defaultIntervals = ['5s', '10s', '30s', '1m', '5m', '15m', '30m', '1h', '2h', '1d'];

const getStyles = memoizeOne((theme: GrafanaTheme) => {
return {
Expand Down Expand Up @@ -45,9 +45,7 @@ export class RefreshPickerBase extends PureComponent<Props> {

intervalsToOptions = (intervals: string[] | undefined): Array<SelectableValue<string>> => {
const intervalsOrDefault = intervals || defaultIntervals;
const options = intervalsOrDefault
.filter(str => str !== '')
.map(interval => ({ label: interval, value: interval }));
const options = intervalsOrDefault.map(interval => ({ label: interval, value: interval }));

if (this.props.hasLiveOption) {
options.unshift(RefreshPicker.liveOption);
Expand Down
1 change: 1 addition & 0 deletions pkg/api/dashboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ func dashboardSaveErrorToApiResponse(err error) Response {
err == m.ErrFolderNotFound ||
err == m.ErrDashboardFolderCannotHaveParent ||
err == m.ErrDashboardFolderNameExists ||
err == m.ErrDashboardRefreshIntervalTooShort ||
err == m.ErrDashboardCannotSaveProvisionedDashboard {
return Error(400, err.Error(), nil)
}
Expand Down
1 change: 1 addition & 0 deletions pkg/api/frontendsettings.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *m.ReqContext) (map[string]interf
jsonObj := map[string]interface{}{
"defaultDatasource": defaultDatasource,
"datasources": datasources,
"minRefreshInterval": setting.MinRefreshInterval,
"panels": panels,
"appSubUrl": setting.AppSubUrl,
"allowOrgCreate": (setting.AllowUserOrgCreate && c.IsSignedIn) || c.IsGrafanaAdmin,
Expand Down
1 change: 1 addition & 0 deletions pkg/models/dashboards.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ var (
ErrDashboardInvalidUid = errors.New("uid contains illegal characters")
ErrDashboardUidToLong = errors.New("uid to long. max 40 characters")
ErrDashboardCannotSaveProvisionedDashboard = errors.New("Cannot save provisioned dashboard")
ErrDashboardRefreshIntervalTooShort = errors.New("Dashboard refresh interval is too low")
ErrDashboardCannotDeleteProvisionedDashboard = errors.New("provisioned dashboard cannot be deleted")
ErrDashboardIdentifierNotSet = errors.New("Unique identfier needed to be able to get a dashboard")
RootFolderName = "General"
Expand Down
38 changes: 38 additions & 0 deletions pkg/services/dashboards/dashboard_service.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package dashboards

import (
"github.com/grafana/grafana/pkg/components/gtime"
"github.com/grafana/grafana/pkg/setting"
"strings"
"time"

Expand Down Expand Up @@ -103,6 +105,10 @@ func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO,
return nil, models.ErrDashboardUidToLong
}

if err := validateDashboardRefreshInterval(dash); err != nil {
return nil, err
}

if validateAlerts {
validateAlertsCmd := models.ValidateDashboardAlertsCommand{
OrgId: dto.OrgId,
Expand Down Expand Up @@ -172,6 +178,33 @@ func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO,
return cmd, nil
}

func validateDashboardRefreshInterval(dash *models.Dashboard) error {
if setting.MinRefreshInterval == "" {
return nil
}

refresh := dash.Data.Get("refresh").MustString("")
if refresh == "" {
// since no refresh is set it is a valid refresh rate
return nil
}

minRefreshInterval, err := gtime.ParseInterval(setting.MinRefreshInterval)
if err != nil {
return err
}
d, err := gtime.ParseInterval(refresh)
if err != nil {
return err
}

if d < minRefreshInterval {
return models.ErrDashboardRefreshIntervalTooShort
}

return nil
}

func (dr *dashboardServiceImpl) updateAlerting(cmd *models.SaveDashboardCommand, dto *SaveDashboardDTO) error {
alertCmd := models.UpdateDashboardAlertsCommand{
OrgId: dto.OrgId,
Expand All @@ -183,6 +216,11 @@ func (dr *dashboardServiceImpl) updateAlerting(cmd *models.SaveDashboardCommand,
}

func (dr *dashboardServiceImpl) SaveProvisionedDashboard(dto *SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) {
if err := validateDashboardRefreshInterval(dto.Dashboard); err != nil {
dr.log.Warn("Changing refresh interval for provisioned dashboard to minimum refresh interval", "dashboardUid", dto.Dashboard.Uid, "dashboardTitle", dto.Dashboard.Title, "minRefreshInterval", setting.MinRefreshInterval)
dto.Dashboard.Data.Set("refresh", setting.MinRefreshInterval)
}

dto.User = &models.SignedInUser{
UserId: 0,
OrgRole: models.ROLE_ADMIN,
Expand Down
43 changes: 42 additions & 1 deletion pkg/services/dashboards/dashboard_service_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package dashboards

import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/setting"
"testing"

"github.com/grafana/grafana/pkg/bus"
Expand All @@ -14,7 +16,9 @@ func TestDashboardService(t *testing.T) {
Convey("Dashboard service tests", t, func() {
bus.ClearBusHandlers()

service := &dashboardServiceImpl{}
service := &dashboardServiceImpl{
log: log.New("test.logger"),
}

origNewDashboardGuardian := guardian.New
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true})
Expand Down Expand Up @@ -184,6 +188,43 @@ func TestDashboardService(t *testing.T) {
So(err, ShouldBeNil)
So(provisioningValidated, ShouldBeFalse)
})

Convey("Should override invalid refresh interval if dashboard is provisioned", func() {
oldRefreshInterval := setting.MinRefreshInterval
setting.MinRefreshInterval = "5m"
defer func() { setting.MinRefreshInterval = oldRefreshInterval }()

bus.AddHandler("test", func(cmd *models.GetProvisionedDashboardDataByIdQuery) error {
cmd.Result = &models.DashboardProvisioning{}
return nil
})

bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error {
return nil
})

bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error {
cmd.Result = &models.ValidateDashboardBeforeSaveResult{}
return nil
})

bus.AddHandler("test", func(cmd *models.SaveProvisionedDashboardCommand) error {
return nil
})

bus.AddHandler("test", func(cmd *models.UpdateDashboardAlertsCommand) error {
return nil
})

dto.Dashboard = models.NewDashboard("Dash")
dto.Dashboard.SetId(3)
dto.User = &models.SignedInUser{UserId: 1}
dto.Dashboard.Data.Set("refresh", "1s")
_, err := service.SaveProvisionedDashboard(dto, nil)
So(err, ShouldBeNil)
So(dto.Dashboard.Data.Get("refresh").MustString(), ShouldEqual, "5m")

})
})

Convey("Import dashboard validation", func() {
Expand Down
1 change: 1 addition & 0 deletions pkg/services/sqlstore/dashboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -623,6 +623,7 @@ func getExistingDashboardByTitleAndFolder(sess *DBSession, cmd *models.ValidateD

func ValidateDashboardBeforeSave(cmd *models.ValidateDashboardBeforeSaveCommand) (err error) {
cmd.Result = &models.ValidateDashboardBeforeSaveResult{}

return inTransaction(func(sess *DBSession) error {
if err = getExistingDashboardByIdOrUidForUpdate(sess, cmd); err != nil {
return err
Expand Down
5 changes: 5 additions & 0 deletions pkg/setting/setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ var (

// Dashboard history
DashboardVersionsToKeep int
MinRefreshInterval string

// User settings
AllowUserSignUp bool
Expand Down Expand Up @@ -763,6 +764,10 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
// read dashboard settings
dashboards := iniFile.Section("dashboards")
DashboardVersionsToKeep = dashboards.Key("versions_to_keep").MustInt(20)
MinRefreshInterval, err = valueAsString(dashboards, "min_refresh_interval", "")
if err != nil {
return err
}

// read data source proxy white list
DataProxyWhiteList = make(map[string]bool)
Expand Down
20 changes: 19 additions & 1 deletion public/app/core/services/context_srv.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import config from 'app/core/config';
import config from '../../core/config';
import _ from 'lodash';
import coreModule from 'app/core/core_module';
import kbn from '../utils/kbn';

export class User {
id: number;
Expand Down Expand Up @@ -32,6 +33,7 @@ export class ContextSrv {
isEditor: any;
sidemenuSmallBreakpoint = false;
hasEditPermissionInFolders: boolean;
minRefreshInterval: string;

constructor() {
if (!config.bootData) {
Expand All @@ -43,6 +45,7 @@ export class ContextSrv {
this.isGrafanaAdmin = this.user.isGrafanaAdmin;
this.isEditor = this.hasRole('Editor') || this.hasRole('Admin');
this.hasEditPermissionInFolders = this.user.hasEditPermissionInFolders;
this.minRefreshInterval = config.minRefreshInterval;
}

hasRole(role: string) {
Expand All @@ -53,6 +56,21 @@ export class ContextSrv {
return !!(document.visibilityState === undefined || document.visibilityState === 'visible');
}

// checks whether the passed interval is longer than the configured minimum refresh rate
isAllowedInterval(interval: string) {
if (!config.minRefreshInterval) {
return true;
}
return kbn.interval_to_ms(interval) >= kbn.interval_to_ms(config.minRefreshInterval);
}

getValidInterval(interval: string) {
if (!this.isAllowedInterval(interval)) {
return config.minRefreshInterval;
}
return interval;
}

hasAccessToExplore() {
return (this.isEditor || config.viewersCanEdit) && config.exploreEnabled;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { TimePickerWithHistory } from 'app/core/components/TimePicker/TimePicker

// Utils & Services
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { defaultIntervals } from '@grafana/ui/src/components/RefreshPicker/RefreshPicker';
import { appEvents } from 'app/core/core';

const getStyles = stylesFactory((theme: GrafanaTheme) => {
Expand Down Expand Up @@ -92,7 +93,9 @@ class UnthemedDashNavTimeControls extends Component<Props> {

render() {
const { dashboard, theme } = this.props;
const intervals = dashboard.timepicker.refresh_intervals;
const { refresh_intervals } = dashboard.timepicker;
const intervals = getTimeSrv().getValidIntervals(refresh_intervals || defaultIntervals);

const timePickerValue = getTimeSrv().timeRange();
const timeZone = dashboard.getTimezone();
const styles = getStyles(theme);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import coreModule from 'app/core/core_module';
import { DashboardModel } from 'app/features/dashboard/state';
import { config } from 'app/core/config';
import kbn from 'app/core/utils/kbn';

export class TimePickerCtrl {
panel: any;
Expand All @@ -19,6 +21,15 @@ export class TimePickerCtrl {
'2h',
'1d',
];
if (config.minRefreshInterval) {
this.panel.refresh_intervals = this.filterRefreshRates(this.panel.refresh_intervals);
}
}

filterRefreshRates(refreshRates: string[]) {
return refreshRates.filter(rate => {
return kbn.interval_to_ms(rate) > kbn.interval_to_ms(config.minRefreshInterval);
});
}
}

Expand Down
26 changes: 23 additions & 3 deletions public/app/features/dashboard/services/TimeSrv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import { getZoomedTimeRange, getShiftedTimeRange } from 'app/core/utils/timePick
import { appEvents } from '../../../core/core';
import { CoreEvents } from '../../../types';

import { config } from 'app/core/config';

export class TimeSrv {
time: any;
refreshTimer: any;
Expand Down Expand Up @@ -72,6 +74,19 @@ export class TimeSrv {
}
}

getValidIntervals(intervals: string[]): string[] {
if (!this.contextSrv.minRefreshInterval) {
return intervals;
}

const validIntervals = intervals.filter(str => str !== '').filter(this.contextSrv.isAllowedInterval);

if (validIntervals.indexOf(this.contextSrv.minRefreshInterval) === -1) {
validIntervals.unshift(this.contextSrv.minRefreshInterval);
}
return validIntervals;
}

private parseTime() {
// when absolute time is saved in json it is turned to a string
if (_.isString(this.time.from) && this.time.from.indexOf('Z') >= 0) {
Expand Down Expand Up @@ -138,7 +153,11 @@ export class TimeSrv {
}
// but if refresh explicitly set then use that
if (params.refresh) {
this.refresh = params.refresh || this.refresh;
if (!this.contextSrv.isAllowedInterval(params.refresh)) {
this.refresh = config.minRefreshInterval;
} else {
this.refresh = params.refresh || this.refresh;
}
}
}

Expand Down Expand Up @@ -170,7 +189,8 @@ export class TimeSrv {
this.cancelNextRefresh();

if (interval) {
const intervalMs = kbn.interval_to_ms(interval);
const validInterval = this.contextSrv.getValidInterval(interval);
const intervalMs = kbn.interval_to_ms(validInterval);

this.refreshTimer = this.timer.register(
this.$timeout(() => {
Expand All @@ -184,7 +204,7 @@ export class TimeSrv {
this.$timeout(() => {
const params = this.$location.search();
if (interval) {
params.refresh = interval;
params.refresh = this.contextSrv.getValidInterval(interval);
this.$location.search(params);
} else if (params.refresh) {
delete params.refresh;
Expand Down
Loading

0 comments on commit 72628c8

Please sign in to comment.