diff --git a/CHANGELOG.md b/CHANGELOG.md index b3b6371b324..0b25f025d1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/conf/defaults.ini b/conf/defaults.ini index a40321cc06f..8845abeb439 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -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/ diff --git a/conf/sample.ini b/conf/sample.ini index e9cf05a109c..dbb761c4613 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -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/ diff --git a/packaging/deb/control/postinst b/packaging/deb/control/postinst index b93c8433490..425a7319e62 100755 --- a/packaging/deb/control/postinst +++ b/packaging/deb/control/postinst @@ -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 } diff --git a/pkg/api/metrics.go b/pkg/api/metrics.go index 549d7689b94..154f863af53 100644 --- a/pkg/api/metrics.go +++ b/pkg/api/metrics.go @@ -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 { diff --git a/pkg/log/log.go b/pkg/log/log.go index 6e75a2b9a8c..f74511e4f45 100644 --- a/pkg/log/log.go +++ b/pkg/log/log.go @@ -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) } diff --git a/pkg/log/syslog.go b/pkg/log/syslog.go index cdf03555238..29a22e9fe1e 100644 --- a/pkg/log/syslog.go +++ b/pkg/log/syslog.go @@ -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 +} diff --git a/pkg/login/ldap.go b/pkg/login/ldap.go index 48f226ccfa5..e02c59e1823 100644 --- a/pkg/login/ldap.go +++ b/pkg/login/ldap.go @@ -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) diff --git a/pkg/services/sqlstore/org_users.go b/pkg/services/sqlstore/org_users.go index fdd671d0bfe..11ea558b0ce 100644 --- a/pkg/services/sqlstore/org_users.go +++ b/pkg/services/sqlstore/org_users.go @@ -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, diff --git a/public/app/core/components/grafana_app.ts b/public/app/core/components/grafana_app.ts index 0a2e49e5d72..a871e06ad30 100644 --- a/public/app/core/components/grafana_app.ts +++ b/public/app/core/components/grafana_app.ts @@ -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(); } } diff --git a/public/app/core/profiler.ts b/public/app/core/profiler.ts new file mode 100644 index 00000000000..8684a5d3531 --- /dev/null +++ b/public/app/core/profiler.ts @@ -0,0 +1,133 @@ +/// +// +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}; diff --git a/public/app/features/annotations/annotations_srv.js b/public/app/features/annotations/annotations_srv.js index a693dd602c8..8f84a6ba905 100644 --- a/public/app/features/annotations/annotations_srv.js +++ b/public/app/features/annotations/annotations_srv.js @@ -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() { diff --git a/public/app/features/dashboard/dashboardCtrl.js b/public/app/features/dashboard/dashboardCtrl.js index b6702631155..0a9c0fd7e92 100644 --- a/public/app/features/dashboard/dashboardCtrl.js +++ b/public/app/features/dashboard/dashboardCtrl.js @@ -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'); }; diff --git a/public/app/features/dashboard/dashboardLoaderSrv.js b/public/app/features/dashboard/dashboardLoaderSrv.js index 1af0894b462..70c49967ea5 100644 --- a/public/app/features/dashboard/dashboardLoaderSrv.js +++ b/public/app/features/dashboard/dashboardLoaderSrv.js @@ -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); } diff --git a/public/app/features/dashboard/viewStateSrv.js b/public/app/features/dashboard/viewStateSrv.js index 2138dd37438..035bfb6ae6e 100644 --- a/public/app/features/dashboard/viewStateSrv.js +++ b/public/app/features/dashboard/viewStateSrv.js @@ -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; }; diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index 0bccee8ff35..d3805d5f44f 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -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); } diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index e47e3444564..0f253b5048a 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -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); } diff --git a/public/app/features/panel/solo_panel_ctrl.js b/public/app/features/panel/solo_panel_ctrl.js index 355d5e8b265..0eb271675ee 100644 --- a/public/app/features/panel/solo_panel_ctrl.js +++ b/public/app/features/panel/solo_panel_ctrl.js @@ -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() { diff --git a/public/app/plugins/panel/graph/graph.js b/public/app/plugins/panel/graph/graph.js index 86ad3b5f025..3a42df657a5 100755 --- a/public/app/plugins/panel/graph/graph.js +++ b/public/app/plugins/panel/graph/graph.js @@ -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 diff --git a/public/test/specs/dashboardViewStateSrv-specs.js b/public/test/specs/dashboardViewStateSrv-specs.js index 202a43670b1..90e35810ac0 100644 --- a/public/test/specs/dashboardViewStateSrv-specs.js +++ b/public/test/specs/dashboardViewStateSrv-specs.js @@ -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); });