Преглед изворни кода

Merge branch 'master' into alerting_definitions

Torkel Ödegaard пре 9 година
родитељ
комит
26d93d7130

+ 5 - 0
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)

+ 15 - 5
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/

+ 28 - 4
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/

+ 3 - 3
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
 }

+ 2 - 2
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 {

+ 43 - 21
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("")
+
+			if err := sysLogHandler.Init(); err != nil {
+				Root.Error("Failed to init syslog log handler", "error", err)
+				os.Exit(1)
+			}
 
-			// 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(""),
-			// 	}
+			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)
 	}

+ 85 - 92
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
+}

+ 4 - 3
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)

+ 6 - 0
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,

+ 3 - 82
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;
 
-      $scope._ = _;
-
-      $rootScope.profilingEnabled = store.getBool('profilingEnabled');
-      $rootScope.performance = { loadStart: new Date().getTime() };
       $rootScope.appSubUrl = config.appSubUrl;
+      $scope._ = _;
 
-      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 - 0
public/app/core/profiler.ts

@@ -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};

+ 1 - 1
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() {

+ 9 - 6
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');
     };
 

+ 2 - 0
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);
         }

+ 0 - 2
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;
     };
 

+ 5 - 1
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);
   }
 

+ 5 - 1
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);
   }
 

+ 1 - 1
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() {

+ 23 - 5
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

+ 1 - 2
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);
       });