Merge branch 'master' into alerting_definitions

This commit is contained in:
Torkel Ödegaard 2016-06-08 09:36:50 +02:00
commit 26d93d7130
20 changed files with 374 additions and 231 deletions

View File

@ -8,6 +8,11 @@
* **Theme**: Add default theme to config file [#5011](https://github.com/grafana/grafana/pull/5011)
* **Page Footer**: Added page footer with links to docs, shows Grafana version and info if new version is available, closes [#4889](https://github.com/grafana/grafana/pull/4889)
* **InfluxDB**: Add spread function, closes [#5211](https://github.com/grafana/grafana/issues/5211)
* **Scripts**: Use restart instead of start for deb package script, closes [#5282](https://github.com/grafana/grafana/pull/5282)
* **Logging**: Moved to structured logging lib, and moved to component specific level filters via config file, closes [#4590](https://github.com/grafana/grafana/issues/4590)
## Breaking changes
* **Logging** : Changed default logging output format (now structured into message, and key value pairs, with logger key acting as component). You can also no change in config to json log ouput.
# 3.0.4 Patch release (2016-05-25)
* **Panel**: Fixed blank dashboard issue when switching to other dashboard while in fullscreen edit mode, fixes [#5163](https://github.com/grafana/grafana/pull/5163)

View File

@ -251,18 +251,23 @@ templates_pattern = emails/*.html
# Use space to separate multiple modes, e.g. "console file"
mode = console, file
# Either "Trace", "Debug", "Info", "Warn", "Error", "Critical", default is "Info"
level = Info
# Either "trace", "debug", "info", "warn", "error", "critical", default is "info"
level = info
# For "console" mode only
[log.console]
level =
# Set formatting to "false" to disable color formatting of console logs
formatting = false
# log line format, valid options are text, console and json
format = console
# For "file" mode only
[log.file]
level =
# log line format, valid options are text, console and json
format = text
# This enables automated log rotate(switch of following options), default is true
log_rotate = true
@ -280,6 +285,10 @@ max_days = 7
[log.syslog]
level =
# log line format, valid options are text, console and json
format = text
# Syslog network type and address. This can be udp, tcp, or unix. If left blank, the default unix endpoints will be used.
network =
address =
@ -290,7 +299,8 @@ facility =
# Syslog tag. By default, the process' argv[0] is used.
tag =
#################################### AMPQ Event Publisher ##########################
#################################### AMQP Event Publisher ##########################
[event_publisher]
enabled = false
rabbitmq_url = amqp://localhost/

View File

@ -230,19 +230,26 @@ check_for_updates = true
#################################### Logging ##########################
[log]
# Either "console", "file", "syslog". Default is console and file
# Use comma to separate multiple modes, e.g. "console, file"
# Use space to separate multiple modes, e.g. "console file"
;mode = console, file
# Either "Trace", "Debug", "Info", "Warn", "Error", "Critical", default is "Info"
;level = Info
# Either "trace", "debug", "info", "warn", "error", "critical", default is "info"
;level = info
# For "console" mode only
[log.console]
;level =
# log line format, valid options are text, console and json
;format = console
# For "file" mode only
[log.file]
;level =
# log line format, valid options are text, console and json
;format = text
# This enables automated log rotate(switch of following options), default is true
;log_rotate = true
@ -258,7 +265,24 @@ check_for_updates = true
# Expired days of log file(delete after max days), default is 7
;max_days = 7
#################################### AMPQ Event Publisher ##########################
[log.syslog]
;level =
# log line format, valid options are text, console and json
;format = text
# Syslog network type and address. This can be udp, tcp, or unix. If left blank, the default unix endpoints will be used.
;network =
;address =
# Syslog facility. user, daemon and local0 through local7 are valid.
;facility =
# Syslog tag. By default, the process' argv[0] is used.
;tag =
#################################### AMQP Event Publisher ##########################
[event_publisher]
;enabled = false
;rabbitmq_url = amqp://localhost/

View File

@ -7,12 +7,12 @@ set -e
startGrafana() {
if [ -x /bin/systemctl ]; then
/bin/systemctl daemon-reload
/bin/systemctl start grafana-server
/bin/systemctl restart grafana-server
elif [ -x "/etc/init.d/grafana-server" ]; then
if [ -x "`which invoke-rc.d 2>/dev/null`" ]; then
invoke-rc.d grafana-server start || true
invoke-rc.d grafana-server restart || true
else
/etc/init.d/grafana-server start || true
/etc/init.d/grafana-server restart || true
fi
fi
}

View File

@ -12,7 +12,7 @@ import (
"github.com/grafana/grafana/pkg/util"
)
func GetTestMetrics(c *middleware.Context) {
func GetTestMetrics(c *middleware.Context) Response {
from := c.QueryInt64("from")
to := c.QueryInt64("to")
maxDataPoints := c.QueryInt64("maxDataPoints")
@ -37,7 +37,7 @@ func GetTestMetrics(c *middleware.Context) {
result.Data[seriesIndex].DataPoints = points
}
c.JSON(200, &result)
return Json(200, &result)
}
func GetInternalMetrics(c *middleware.Context) Response {

View File

@ -13,6 +13,7 @@ import (
"gopkg.in/ini.v1"
"github.com/inconshreveable/log15"
"github.com/inconshreveable/log15/term"
)
var Root log15.Logger
@ -82,16 +83,17 @@ func Close() {
}
var logLevels = map[string]log15.Lvl{
"Trace": log15.LvlDebug,
"Debug": log15.LvlDebug,
"Info": log15.LvlInfo,
"Warn": log15.LvlWarn,
"Error": log15.LvlError,
"Critical": log15.LvlCrit,
"trace": log15.LvlDebug,
"debug": log15.LvlDebug,
"info": log15.LvlInfo,
"warn": log15.LvlWarn,
"error": log15.LvlError,
"critical": log15.LvlCrit,
}
func getLogLevelFromConfig(key string, defaultName string, cfg *ini.File) (string, log15.Lvl) {
levelName := cfg.Section(key).Key("level").In(defaultName, []string{"Trace", "Debug", "Info", "Warn", "Error", "Critical"})
levelName := cfg.Section(key).Key("level").MustString("info")
levelName = strings.ToLower(levelName)
level := getLogLevelFromString(levelName)
return levelName, level
}
@ -118,10 +120,26 @@ func getFilters(filterStrArray []string) map[string]log15.Lvl {
return filterMap
}
func getLogFormat(format string) log15.Format {
switch format {
case "console":
if term.IsTty(os.Stdout.Fd()) {
return log15.TerminalFormat()
}
return log15.LogfmtFormat()
case "text":
return log15.LogfmtFormat()
case "json":
return log15.JsonFormat()
default:
return log15.LogfmtFormat()
}
}
func ReadLoggingConfig(modes []string, logsPath string, cfg *ini.File) {
Close()
defaultLevelName, _ := getLogLevelFromConfig("log", "Info", cfg)
defaultLevelName, _ := getLogLevelFromConfig("log", "info", cfg)
defaultFilters := getFilters(cfg.Section("log").Key("filters").Strings(" "))
handlers := make([]log15.Handler, 0)
@ -136,18 +154,20 @@ func ReadLoggingConfig(modes []string, logsPath string, cfg *ini.File) {
// Log level.
_, level := getLogLevelFromConfig("log."+mode, defaultLevelName, cfg)
modeFilters := getFilters(sec.Key("filters").Strings(" "))
format := getLogFormat(sec.Key("format").MustString(""))
var handler log15.Handler
// Generate log configuration.
switch mode {
case "console":
handler = log15.StdoutHandler
handler = log15.StreamHandler(os.Stdout, format)
case "file":
fileName := sec.Key("file_name").MustString(filepath.Join(logsPath, "grafana.log"))
os.MkdirAll(filepath.Dir(fileName), os.ModePerm)
fileHandler := NewFileWriter()
fileHandler.Filename = fileName
fileHandler.Format = format
fileHandler.Rotate = sec.Key("log_rotate").MustBool(true)
fileHandler.Maxlines = sec.Key("max_lines").MustInt(1000000)
fileHandler.Maxsize = 1 << uint(sec.Key("max_size_shift").MustInt(28))
@ -157,15 +177,21 @@ func ReadLoggingConfig(modes []string, logsPath string, cfg *ini.File) {
loggersToClose = append(loggersToClose, fileHandler)
handler = fileHandler
case "syslog":
sysLogHandler := NewSyslog()
sysLogHandler.Format = format
sysLogHandler.Network = sec.Key("network").MustString("")
sysLogHandler.Address = sec.Key("address").MustString("")
sysLogHandler.Facility = sec.Key("facility").MustString("local7")
sysLogHandler.Tag = sec.Key("tag").MustString("")
// case "syslog":
// LogConfigs[i] = util.DynMap{
// "level": level,
// "network": sec.Key("network").MustString(""),
// "address": sec.Key("address").MustString(""),
// "facility": sec.Key("facility").MustString("local7"),
// "tag": sec.Key("tag").MustString(""),
// }
if err := sysLogHandler.Init(); err != nil {
Root.Error("Failed to init syslog log handler", "error", err)
os.Exit(1)
}
loggersToClose = append(loggersToClose, sysLogHandler)
handler = sysLogHandler
}
for key, value := range defaultFilters {
@ -174,10 +200,6 @@ func ReadLoggingConfig(modes []string, logsPath string, cfg *ini.File) {
}
}
for key, value := range modeFilters {
fmt.Printf("key: %v, value: %v \n", key, value)
}
handler = LogFilterHandler(level, modeFilters, handler)
handlers = append(handlers, handler)
}

View File

@ -2,95 +2,88 @@
package log
//
// import (
// "encoding/json"
// "errors"
// "log/syslog"
// )
//
// type SyslogWriter struct {
// syslog *syslog.Writer
// Network string `json:"network"`
// Address string `json:"address"`
// Facility string `json:"facility"`
// Tag string `json:"tag"`
// }
//
// func NewSyslog() LoggerInterface {
// return new(SyslogWriter)
// }
//
// func (sw *SyslogWriter) Init(config string) error {
// if err := json.Unmarshal([]byte(config), sw); err != nil {
// return err
// }
//
// prio, err := parseFacility(sw.Facility)
// if err != nil {
// return err
// }
//
// w, err := syslog.Dial(sw.Network, sw.Address, prio, sw.Tag)
// if err != nil {
// return err
// }
//
// sw.syslog = w
// return nil
// }
//
// func (sw *SyslogWriter) WriteMsg(msg string, skip int, level LogLevel) error {
// var err error
//
// switch level {
// case TRACE, DEBUG:
// err = sw.syslog.Debug(msg)
// case INFO:
// err = sw.syslog.Info(msg)
// case WARN:
// err = sw.syslog.Warning(msg)
// case ERROR:
// err = sw.syslog.Err(msg)
// case CRITICAL:
// err = sw.syslog.Crit(msg)
// case FATAL:
// err = sw.syslog.Alert(msg)
// default:
// err = errors.New("invalid syslog level")
// }
//
// return err
// }
//
// func (sw *SyslogWriter) Destroy() {
// sw.syslog.Close()
// }
//
// func (sw *SyslogWriter) Flush() {}
//
// var facilities = map[string]syslog.Priority{
// "user": syslog.LOG_USER,
// "daemon": syslog.LOG_DAEMON,
// "local0": syslog.LOG_LOCAL0,
// "local1": syslog.LOG_LOCAL1,
// "local2": syslog.LOG_LOCAL2,
// "local3": syslog.LOG_LOCAL3,
// "local4": syslog.LOG_LOCAL4,
// "local5": syslog.LOG_LOCAL5,
// "local6": syslog.LOG_LOCAL6,
// "local7": syslog.LOG_LOCAL7,
// }
//
// func parseFacility(facility string) (syslog.Priority, error) {
// prio, ok := facilities[facility]
// if !ok {
// return syslog.LOG_LOCAL0, errors.New("invalid syslog facility")
// }
//
// return prio, nil
// }
//
// func init() {
// Register("syslog", NewSyslog)
// }
import (
"errors"
"log/syslog"
"github.com/inconshreveable/log15"
)
type SysLogHandler struct {
syslog *syslog.Writer
Network string
Address string
Facility string
Tag string
Format log15.Format
}
func NewSyslog() *SysLogHandler {
return &SysLogHandler{
Format: log15.LogfmtFormat(),
}
}
func (sw *SysLogHandler) Init() error {
prio, err := parseFacility(sw.Facility)
if err != nil {
return err
}
w, err := syslog.Dial(sw.Network, sw.Address, prio, sw.Tag)
if err != nil {
return err
}
sw.syslog = w
return nil
}
func (sw *SysLogHandler) Log(r *log15.Record) error {
var err error
msg := string(sw.Format.Format(r))
switch r.Lvl {
case log15.LvlDebug:
err = sw.syslog.Debug(msg)
case log15.LvlInfo:
err = sw.syslog.Info(msg)
case log15.LvlWarn:
err = sw.syslog.Warning(msg)
case log15.LvlError:
err = sw.syslog.Err(msg)
case log15.LvlCrit:
err = sw.syslog.Crit(msg)
default:
err = errors.New("invalid syslog level")
}
return err
}
func (sw *SysLogHandler) Close() {
sw.syslog.Close()
}
var facilities = map[string]syslog.Priority{
"user": syslog.LOG_USER,
"daemon": syslog.LOG_DAEMON,
"local0": syslog.LOG_LOCAL0,
"local1": syslog.LOG_LOCAL1,
"local2": syslog.LOG_LOCAL2,
"local3": syslog.LOG_LOCAL3,
"local4": syslog.LOG_LOCAL4,
"local5": syslog.LOG_LOCAL5,
"local6": syslog.LOG_LOCAL6,
"local7": syslog.LOG_LOCAL7,
}
func parseFacility(facility string) (syslog.Priority, error) {
prio, ok := facilities[facility]
if !ok {
return syslog.LOG_LOCAL0, errors.New("invalid syslog facility")
}
return prio, nil
}

View File

@ -219,7 +219,8 @@ func (a *ldapAuther) syncOrgRoles(user *m.User, ldapUser *ldapUserInfo) error {
// add role
cmd := m.AddOrgUserCommand{UserId: user.Id, Role: group.OrgRole, OrgId: group.OrgId}
if err := bus.Dispatch(&cmd); err != nil {
err := bus.Dispatch(&cmd)
if err != nil && err != m.ErrOrgNotFound {
return err
}
@ -290,7 +291,7 @@ func (a *ldapAuther) searchForUser(username string) (*ldapUserInfo, error) {
a.server.Attr.Name,
a.server.Attr.MemberOf,
},
Filter: strings.Replace(a.server.SearchFilter, "%s", username, -1),
Filter: strings.Replace(a.server.SearchFilter, "%s", ldap.EscapeFilter(username), -1),
}
searchResult, err = a.conn.Search(&searchReq)
@ -323,7 +324,7 @@ func (a *ldapAuther) searchForUser(username string) (*ldapUserInfo, error) {
if a.server.GroupSearchFilterUserAttribute == "" {
filter_replace = getLdapAttr(a.server.Attr.Username, searchResult)
}
filter := strings.Replace(a.server.GroupSearchFilter, "%s", filter_replace, -1)
filter := strings.Replace(a.server.GroupSearchFilter, "%s", ldap.EscapeFilter(filter_replace), -1)
if ldapCfg.VerboseLogging {
log.Info("LDAP: Searching for user's groups: %s", filter)

View File

@ -26,6 +26,12 @@ func AddOrgUser(cmd *m.AddOrgUserCommand) error {
return m.ErrOrgUserAlreadyAdded
}
if res, err := sess.Query("SELECT 1 from org WHERE id=?", cmd.OrgId); err != nil {
return err
} else if len(res) != 1 {
return m.ErrOrgNotFound
}
entity := m.OrgUser{
OrgId: cmd.OrgId,
UserId: cmd.UserId,

View File

@ -6,6 +6,7 @@ import _ from 'lodash';
import angular from 'angular';
import $ from 'jquery';
import coreModule from 'app/core/core_module';
import {profiler} from 'app/core/profiler';
export class GrafanaCtrl {
@ -15,14 +16,10 @@ export class GrafanaCtrl {
$scope.init = function() {
$scope.contextSrv = contextSrv;
$rootScope.appSubUrl = config.appSubUrl;
$scope._ = _;
$rootScope.profilingEnabled = store.getBool('profilingEnabled');
$rootScope.performance = { loadStart: new Date().getTime() };
$rootScope.appSubUrl = config.appSubUrl;
if ($rootScope.profilingEnabled) { $scope.initProfiling(); }
profiler.init(config, $rootScope);
alertSrv.init();
utilSrv.init();
@ -59,82 +56,6 @@ export class GrafanaCtrl {
"#E0F9D7","#FCEACA","#CFFAFF","#F9E2D2","#FCE2DE","#BADFF4","#F9D9F9","#DEDAF7"
];
$scope.getTotalWatcherCount = function() {
var count = 0;
var scopes = 0;
var root = $(document.getElementsByTagName('body'));
var f = function (element) {
if (element.data().hasOwnProperty('$scope')) {
scopes++;
angular.forEach(element.data().$scope.$$watchers, function () {
count++;
});
}
angular.forEach(element.children(), function (childElement) {
f($(childElement));
});
};
f(root);
$rootScope.performance.scopeCount = scopes;
return count;
};
$scope.initProfiling = function() {
var count = 0;
$scope.$watch(function digestCounter() {
count++;
}, function() {
// something
});
$rootScope.performance.panels = [];
$scope.$on('refresh', function() {
if ($rootScope.performance.panels.length > 0) {
var totalRender = 0;
var totalQuery = 0;
_.each($rootScope.performance.panels, function(panelTiming: any) {
totalRender += panelTiming.render;
totalQuery += panelTiming.query;
});
console.log('total query: ' + totalQuery);
console.log('total render: ' + totalRender);
console.log('avg render: ' + totalRender / $rootScope.performance.panels.length);
}
$rootScope.performance.panels = [];
});
$scope.onAppEvent('dashboard-loaded', function() {
count = 0;
setTimeout(function() {
console.log("Dashboard::Performance Total Digests: " + count);
console.log("Dashboard::Performance Total Watchers: " + $scope.getTotalWatcherCount());
console.log("Dashboard::Performance Total ScopeCount: " + $rootScope.performance.scopeCount);
var timeTaken = $rootScope.performance.allPanelsInitialized - $rootScope.performance.dashboardLoadStart;
console.log("Dashboard::Performance - All panels initialized in " + timeTaken + " ms");
// measure digest performance
var rootDigestStart = window.performance.now();
for (var i = 0; i < 30; i++) {
$rootScope.$apply();
}
console.log("Dashboard::Performance Root Digest " + ((window.performance.now() - rootDigestStart) / 30));
}, 3000);
});
};
$scope.init();
}
}

133
public/app/core/profiler.ts Normal file
View File

@ -0,0 +1,133 @@
///<reference path="../headers/common.d.ts" />
//
import $ from 'jquery';
import _ from 'lodash';
import angular from 'angular';
export class Profiler {
panelsRendered: number;
enabled: boolean;
panels: any[];
panelsInitCount: any;
timings: any;
digestCounter: any;
$rootScope: any;
scopeCount: any;
init(config, $rootScope) {
this.enabled = config.buildInfo.env === 'development';
this.timings = {};
this.timings.appStart = { loadStart: new Date().getTime() };
this.$rootScope = $rootScope;
if (!this.enabled) {
return;
}
$rootScope.$watch(() => {
this.digestCounter++;
return false;
}, () => {});
$rootScope.$on('refresh', this.refresh.bind(this));
$rootScope.onAppEvent('dashboard-fetched', this.dashboardFetched.bind(this));
$rootScope.onAppEvent('dashboard-initialized', this.dashboardInitialized.bind(this));
$rootScope.onAppEvent('panel-initialized', this.panelInitialized.bind(this));
}
refresh() {
this.panels = [];
setTimeout(() => {
var totalRender = 0;
var totalQuery = 0;
for (let panelTiming of this.panels) {
totalRender += panelTiming.render;
totalQuery += panelTiming.query;
}
console.log('panel count: ' + this.panels.length);
console.log('total query: ' + totalQuery);
console.log('total render: ' + totalRender);
console.log('avg render: ' + totalRender / this.panels.length);
}, 5000);
}
dashboardFetched() {
this.timings.dashboardLoadStart = new Date().getTime();
this.panelsInitCount = 0;
this.digestCounter = 0;
this.panelsInitCount = 0;
this.panelsRendered = 0;
this.panels = [];
}
dashboardInitialized() {
setTimeout(() => {
console.log("Dashboard::Performance Total Digests: " + this.digestCounter);
console.log("Dashboard::Performance Total Watchers: " + this.getTotalWatcherCount());
console.log("Dashboard::Performance Total ScopeCount: " + this.scopeCount);
var timeTaken = this.timings.lastPanelInitializedAt - this.timings.dashboardLoadStart;
console.log("Dashboard::Performance All panels initialized in " + timeTaken + " ms");
// measure digest performance
var rootDigestStart = window.performance.now();
for (var i = 0; i < 30; i++) {
this.$rootScope.$apply();
}
console.log("Dashboard::Performance Root Digest " + ((window.performance.now() - rootDigestStart) / 30));
}, 3000);
}
getTotalWatcherCount() {
var count = 0;
var scopes = 0;
var root = $(document.getElementsByTagName('body'));
var f = function (element) {
if (element.data().hasOwnProperty('$scope')) {
scopes++;
angular.forEach(element.data().$scope.$$watchers, function () {
count++;
});
}
angular.forEach(element.children(), function (childElement) {
f($(childElement));
});
};
f(root);
this.scopeCount = scopes;
return count;
}
renderingCompleted(panelId, panelTimings) {
this.panelsRendered++;
if (this.enabled) {
panelTimings.renderEnd = new Date().getTime();
this.panels.push({
panelId: panelId,
query: panelTimings.queryEnd - panelTimings.queryStart,
render: panelTimings.renderEnd - panelTimings.renderStart,
});
}
}
panelInitialized() {
if (!this.enabled) {
return;
}
this.panelsInitCount++;
this.timings.lastPanelInitializedAt = new Date().getTime();
}
}
var profiler = new Profiler();
export {profiler};

View File

@ -14,7 +14,7 @@ define([
this.init = function() {
$rootScope.onAppEvent('refresh', this.clearCache, $rootScope);
$rootScope.onAppEvent('dashboard-loaded', this.clearCache, $rootScope);
$rootScope.onAppEvent('dashboard-initialized', this.clearCache, $rootScope);
};
this.clearCache = function() {

View File

@ -35,10 +35,6 @@ function (angular, $, config, moment) {
};
$scope.setupDashboard = function(data) {
$rootScope.performance.dashboardLoadStart = new Date().getTime();
$rootScope.performance.panelsInitialized = 0;
$rootScope.performance.panelsRendered = 0;
var dashboard = dashboardSrv.create(data.dashboard, data.meta);
dashboardSrv.setCurrent(dashboard);
@ -60,7 +56,15 @@ function (angular, $, config, moment) {
$scope.updateSubmenuVisibility();
$scope.setWindowTitleAndTheme();
$scope.appEvent("dashboard-loaded", $scope.dashboard);
if ($scope.profilingEnabled) {
$scope.performance.panels = [];
$scope.performance.panelCount = 0;
$scope.dashboard.rows.forEach(function(row) {
$scope.performance.panelCount += row.panels.length;
});
}
$scope.appEvent("dashboard-initialized", $scope.dashboard);
}).catch(function(err) {
if (err.data && err.data.message) { err.message = err.data.message; }
$scope.appEvent("alert-error", ['Dashboard init failed', 'Template variables could not be initialized: ' + err.message]);
@ -76,7 +80,6 @@ function (angular, $, config, moment) {
};
$scope.broadcastRefresh = function() {
$rootScope.performance.panelsRendered = 0;
$rootScope.$broadcast('refresh');
};

View File

@ -47,6 +47,8 @@ function (angular, moment, _, $, kbn, dateMath, impressionStore) {
}
promise.then(function(result) {
$rootScope.appEvent("dashboard-fetched", result.dashboard);
if (result.meta.dashboardNotFound !== true) {
impressionStore.impressions.addDashboardImpression(result.dashboard.id);
}

View File

@ -92,7 +92,6 @@ function (angular, _, $) {
state.fullscreen = state.fullscreen ? true : null;
state.edit = (state.edit === "true" || state.edit === true) || null;
state.editview = state.editview || null;
state.org = contextSrv.user.orgId;
return state;
};
@ -100,7 +99,6 @@ function (angular, _, $) {
var urlState = _.clone(this.state);
urlState.fullscreen = this.state.fullscreen ? true : null;
urlState.edit = this.state.edit ? true : null;
urlState.org = contextSrv.user.orgId;
return urlState;
};

View File

@ -95,7 +95,6 @@ class MetricsPanelCtrl extends PanelCtrl {
}
setTimeQueryStart() {
this.timing = {};
this.timing.queryStart = new Date().getTime();
}
@ -200,6 +199,11 @@ class MetricsPanelCtrl extends PanelCtrl {
this.panel.snapshotData = result.data;
}
if (!result || !result.data) {
console.log('Data source query result invalid, missing data field:', result);
result = {data: []};
}
return this.events.emit('data-received', result.data);
}

View File

@ -4,6 +4,7 @@ import config from 'app/core/config';
import _ from 'lodash';
import angular from 'angular';
import $ from 'jquery';
import {profiler} from 'app/core/profiler';
const TITLE_HEIGHT = 25;
const EMPTY_TITLE_HEIGHT = 9;
@ -31,6 +32,7 @@ export class PanelCtrl {
height: any;
containerHeight: any;
events: Emitter;
timing: any;
constructor($scope, $injector) {
this.$injector = $injector;
@ -38,6 +40,7 @@ export class PanelCtrl {
this.$timeout = $injector.get('$timeout');
this.editorTabIndex = 0;
this.events = new Emitter();
this.timing = {};
var plugin = config.panels[this.panel.type];
if (plugin) {
@ -57,7 +60,7 @@ export class PanelCtrl {
}
renderingCompleted() {
this.$scope.$root.performance.panelsRendered++;
profiler.renderingCompleted(this.panel.id, this.timing);
}
refresh() {
@ -169,6 +172,7 @@ export class PanelCtrl {
}
this.calculatePanelHeight();
this.timing.renderStart = new Date().getTime();
this.events.emit('render', payload);
}

View File

@ -25,7 +25,7 @@ function (angular, $) {
$scope.initDashboard(result, $scope);
});
$scope.onAppEvent("dashboard-loaded", $scope.initPanelScope);
$scope.onAppEvent("dashboard-initialized", $scope.initPanelScope);
};
$scope.initPanelScope = function() {

View File

@ -18,6 +18,8 @@ function (angular, $, moment, _, kbn, GraphTooltip) {
'use strict';
var module = angular.module('grafana.directives');
var labelWidthCache = {};
var panelWidthCache = {};
module.directive('grafanaGraph', function($rootScope, timeSrv) {
return {
@ -31,6 +33,7 @@ function (angular, $, moment, _, kbn, GraphTooltip) {
var sortedSeries;
var legendSideLastValue = null;
var rootScope = scope.$root;
var panelWidth = 0;
rootScope.onAppEvent('setCrosshair', function(event, info) {
// do not need to to this if event is from this panel
@ -104,11 +107,21 @@ function (angular, $, moment, _, kbn, GraphTooltip) {
return true;
}
if (elem.width() === 0) {
if (panelWidth === 0) {
return true;
}
}
function getLabelWidth(text, elem) {
var labelWidth = labelWidthCache[text];
if (!labelWidth) {
labelWidth = labelWidthCache[text] = elem.width();
}
return labelWidth;
}
function drawHook(plot) {
// Update legend values
var yaxis = plot.getYAxes();
@ -137,7 +150,7 @@ function (angular, $, moment, _, kbn, GraphTooltip) {
.text(panel.yaxes[0].label)
.appendTo(elem);
yaxisLabel.css("margin-top", yaxisLabel.width() / 2);
yaxisLabel[0].style.marginTop = (getLabelWidth(panel.yaxes[0].label, yaxisLabel) / 2) + 'px';
}
// add right axis labels
@ -146,7 +159,7 @@ function (angular, $, moment, _, kbn, GraphTooltip) {
.text(panel.yaxes[1].label)
.appendTo(elem);
rightLabel.css("margin-top", rightLabel.width() / 2);
rightLabel[0].style.marginTop = (getLabelWidth(panel.yaxes[1].label, rightLabel) / 2) + 'px';
}
}
@ -159,6 +172,11 @@ function (angular, $, moment, _, kbn, GraphTooltip) {
// Function for rendering panel
function render_panel() {
panelWidth = panelWidthCache[panel.span];
if (!panelWidth) {
panelWidth = panelWidthCache[panel.span] = elem.width();
}
if (shouldAbortRender()) {
return;
}
@ -276,7 +294,7 @@ function (angular, $, moment, _, kbn, GraphTooltip) {
}
function addTimeAxis(options) {
var ticks = elem.width() / 100;
var ticks = panelWidth / 100;
var min = _.isUndefined(ctrl.range.from) ? null : ctrl.range.from.valueOf();
var max = _.isUndefined(ctrl.range.to) ? null : ctrl.range.to.valueOf();
@ -444,7 +462,7 @@ function (angular, $, moment, _, kbn, GraphTooltip) {
}
function render_panel_as_graphite_png(url) {
url += '&width=' + elem.width();
url += '&width=' + panelWidth;
url += '&height=' + elem.css('height').replace('px', '');
url += '&bgcolor=1f1f1f'; // @grayDarker & @grafanaPanelBackground
url += '&fgcolor=BBBFC2'; // @textColor & @grayLighter

View File

@ -31,7 +31,7 @@ define([
it('should update querystring and view state', function() {
var updateState = {fullscreen: true, edit: true, panelId: 1};
viewState.update(updateState);
expect(location.search()).to.eql({fullscreen: true, edit: true, panelId: 1, org: 19});
expect(location.search()).to.eql({fullscreen: true, edit: true, panelId: 1});
expect(viewState.dashboard.meta.fullscreen).to.be(true);
expect(viewState.state.fullscreen).to.be(true);
});
@ -41,7 +41,6 @@ define([
it('should remove params from query string', function() {
viewState.update({fullscreen: true, panelId: 1, edit: true});
viewState.update({fullscreen: false});
expect(location.search()).to.eql({org: 19});
expect(viewState.dashboard.meta.fullscreen).to.be(false);
expect(viewState.state.fullscreen).to.be(null);
});