Commit 49a0ea53 by Torkel Ödegaard

Merge branch 'develop' of github.com:grafana/grafana into dashboard_snapshot_poc

Conflicts:
	src/app/features/dashboard/partials/shareDashboard.html
parents f48f5428 44bc2b2d
app_name = Grafana
app_mode = production
# Once every 1 hour Grafana will report anonymous data to
# stats.grafana.org (https). No ip addresses are being tracked.
# only simple counters to track running instances, dashboard
# count and errors. It is very helpful to us.
# Change this option to false to disable reporting.
reporting-enabled = true
[server]
; protocol (http or https)
protocol = http
......
......@@ -5,6 +5,13 @@
app_mode = production
# Once every 1 hour Grafana will report anonymous data to
# stats.grafana.org (https). No ip addresses are being tracked.
# only simple counters to track running instances, dashboard
# counts and errors. It is very helpful to us.
# Change this option to false to disable reporting.
reporting-enabled = true
[server]
; protocol (http or https)
protocol = http
......
......@@ -39,17 +39,7 @@ func main() {
app.Name = "Grafana Backend"
app.Usage = "grafana web"
app.Version = version
app.Commands = []cli.Command{
cmd.ListOrgs,
cmd.CreateOrg,
cmd.DeleteOrg,
cmd.ExportDashboard,
cmd.ImportDashboard,
cmd.ListDataSources,
cmd.CreateDataSource,
cmd.DescribeDataSource,
cmd.DeleteDataSource,
cmd.Web}
app.Commands = []cli.Command{cmd.ImportDashboard, cmd.Web}
app.Flags = append(app.Flags, []cli.Flag{
cli.StringFlag{
Name: "config",
......
......@@ -3,6 +3,7 @@ package api
import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/util"
......@@ -64,6 +65,8 @@ func AdminCreateUser(c *middleware.Context, form dtos.AdminCreateUserForm) {
return
}
metrics.M_Api_Admin_User_Create.Inc(1)
c.JsonOK("User created")
}
......
......@@ -7,6 +7,7 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
......@@ -27,6 +28,8 @@ func isDasboardStarredByUser(c *middleware.Context, dashId int64) (bool, error)
}
func GetDashboard(c *middleware.Context) {
metrics.M_Api_Dashboard_Get.Inc(1)
slug := c.Params(":slug")
query := m.GetDashboardQuery{Slug: slug, OrgId: c.OrgId}
......@@ -88,6 +91,8 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) {
return
}
metrics.M_Api_Dashboard_Post.Inc(1)
c.JSON(200, util.DynMap{"status": "success", "slug": cmd.Result.Slug, "version": cmd.Result.Version})
}
......
......@@ -47,7 +47,7 @@ func Index(c *middleware.Context) {
func NotFound(c *middleware.Context) {
if c.IsApiRequest() {
c.JsonApiErr(200, "Not found", nil)
c.JsonApiErr(404, "Not found", nil)
return
}
......
......@@ -6,6 +6,7 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
......@@ -75,7 +76,6 @@ func LoginView(c *middleware.Context) {
}
func LoginPost(c *middleware.Context, cmd dtos.LoginCommand) {
userQuery := m.GetUserByLoginQuery{LoginOrEmail: cmd.User}
err := bus.Dispatch(&userQuery)
......@@ -112,6 +112,8 @@ func LoginPost(c *middleware.Context, cmd dtos.LoginCommand) {
c.SetCookie("redirect_to", "", -1, setting.AppSubUrl+"/")
}
metrics.M_Api_Login_Post.Inc(1)
c.JSON(200, result)
}
......
......@@ -8,6 +8,7 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
......@@ -81,5 +82,7 @@ func OAuthLogin(ctx *middleware.Context) {
// login
loginUserWithUser(userQuery.Result, ctx)
metrics.M_Api_Login_OAuth.Inc(1)
ctx.Redirect(setting.AppSubUrl + "/")
}
......@@ -2,6 +2,7 @@ package api
import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
)
......@@ -35,6 +36,8 @@ func CreateOrg(c *middleware.Context, cmd m.CreateOrgCommand) {
return
}
metrics.M_Api_Org_Create.Inc(1)
c.JsonOK("Organization created")
}
......
......@@ -12,12 +12,13 @@ import (
func RenderToPng(c *middleware.Context) {
queryReader := util.NewUrlQueryReader(c.Req.URL)
queryParams := fmt.Sprintf("?render=1&%s=%d&%s", middleware.SESS_KEY_USERID, c.UserId, c.Req.URL.RawQuery)
queryParams := fmt.Sprintf("?%s", c.Req.URL.RawQuery)
renderOpts := &renderer.RenderOpts{
Url: c.Params("*") + queryParams,
Width: queryReader.Get("width", "800"),
Height: queryReader.Get("height", "400"),
Url: c.Params("*") + queryParams,
Width: queryReader.Get("width", "800"),
Height: queryReader.Get("height", "400"),
SessionId: c.Session.ID(),
}
renderOpts.Url = setting.ToAbsUrl(renderOpts.Url)
......
......@@ -2,6 +2,7 @@ package api
import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
......@@ -26,4 +27,6 @@ func SignUp(c *middleware.Context, cmd m.CreateUserCommand) {
loginUserWithUser(&user, c)
c.JsonOK("User created and logged in")
metrics.M_Api_User_SignUp.Inc(1)
}
......@@ -21,6 +21,7 @@ import (
"github.com/grafana/grafana/pkg/api"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/eventpublisher"
......@@ -88,6 +89,10 @@ func runWeb(c *cli.Context) {
m := newMacaron()
api.Register(m)
if setting.ReportingEnabled {
go metrics.StartUsageReportLoop()
}
listenAddr := fmt.Sprintf("%s:%s", setting.HttpAddr, setting.HttpPort)
log.Info("Listen: %v://%s%s", setting.Protocol, listenAddr, setting.AppSubUrl)
switch setting.Protocol {
......
......@@ -14,9 +14,10 @@ import (
)
type RenderOpts struct {
Url string
Width string
Height string
Url string
Width string
Height string
SessionId string
}
func RenderToPng(params *RenderOpts) (string, error) {
......@@ -26,7 +27,9 @@ func RenderToPng(params *RenderOpts) (string, error) {
pngPath, _ := filepath.Abs(filepath.Join(setting.ImagesDir, getHash(params.Url)))
pngPath = pngPath + ".png"
cmd := exec.Command(binPath, scriptPath, "url="+params.Url, "width="+params.Width, "height="+params.Height, "png="+pngPath)
cmd := exec.Command(binPath, scriptPath, "url="+params.Url, "width="+params.Width,
"height="+params.Height, "png="+pngPath, "cookiename="+setting.SessionOptions.CookieName,
"domain="+setting.Domain, "sessionid="+params.SessionId)
stdout, err := cmd.StdoutPipe()
if err != nil {
......
package metrics
import "sync/atomic"
// Counters hold an int64 value that can be incremented and decremented.
type Counter interface {
Clear()
Count() int64
Dec(int64)
Inc(int64)
Snapshot() Counter
}
// NewCounter constructs a new StandardCounter.
func NewCounter() Counter {
return &StandardCounter{0}
}
// CounterSnapshot is a read-only copy of another Counter.
type CounterSnapshot int64
// Clear panics.
func (CounterSnapshot) Clear() {
panic("Clear called on a CounterSnapshot")
}
// Count returns the count at the time the snapshot was taken.
func (c CounterSnapshot) Count() int64 { return int64(c) }
// Dec panics.
func (CounterSnapshot) Dec(int64) {
panic("Dec called on a CounterSnapshot")
}
// Inc panics.
func (CounterSnapshot) Inc(int64) {
panic("Inc called on a CounterSnapshot")
}
// Snapshot returns the snapshot.
func (c CounterSnapshot) Snapshot() Counter { return c }
// StandardCounter is the standard implementation of a Counter and uses the
// sync/atomic package to manage a single int64 value.
type StandardCounter struct {
count int64
}
// Clear sets the counter to zero.
func (c *StandardCounter) Clear() {
atomic.StoreInt64(&c.count, 0)
}
// Count returns the current count.
func (c *StandardCounter) Count() int64 {
return atomic.LoadInt64(&c.count)
}
// Dec decrements the counter by the given amount.
func (c *StandardCounter) Dec(i int64) {
atomic.AddInt64(&c.count, -i)
}
// Inc increments the counter by the given amount.
func (c *StandardCounter) Inc(i int64) {
atomic.AddInt64(&c.count, i)
}
// Snapshot returns a read-only copy of the counter.
func (c *StandardCounter) Snapshot() Counter {
return CounterSnapshot(c.Count())
}
package metrics
type comboCounterRef struct {
usageCounter Counter
metricCounter Counter
}
func NewComboCounterRef(name string) Counter {
cr := &comboCounterRef{}
cr.usageCounter = UsageStats.GetOrRegister(name, NewCounter).(Counter)
cr.metricCounter = MetricStats.GetOrRegister(name, NewCounter).(Counter)
return cr
}
func (c comboCounterRef) Clear() {
c.usageCounter.Clear()
c.metricCounter.Clear()
}
func (c comboCounterRef) Count() int64 {
panic("Count called on a combocounter ref")
}
// Dec panics.
func (c comboCounterRef) Dec(i int64) {
c.usageCounter.Dec(i)
c.metricCounter.Dec(i)
}
// Inc panics.
func (c comboCounterRef) Inc(i int64) {
c.usageCounter.Inc(i)
c.metricCounter.Inc(i)
}
// Snapshot returns the snapshot.
func (c comboCounterRef) Snapshot() Counter {
panic("snapshot called on a combocounter ref")
}
package metrics
var UsageStats = NewRegistry()
var MetricStats = NewRegistry()
var (
M_Instance_Start = NewComboCounterRef("instance.start")
M_Page_Status_200 = NewComboCounterRef("page.status.200")
M_Page_Status_500 = NewComboCounterRef("page.status.500")
M_Page_Status_404 = NewComboCounterRef("page.status.404")
M_Api_Status_500 = NewComboCounterRef("api.status.500")
M_Api_Status_404 = NewComboCounterRef("api.status.404")
M_Api_User_SignUp = NewComboCounterRef("api.user.signup")
M_Api_Dashboard_Get = NewComboCounterRef("api.dashboard.get")
M_Api_Dashboard_Post = NewComboCounterRef("api.dashboard.post")
M_Api_Admin_User_Create = NewComboCounterRef("api.admin.user_create")
M_Api_Login_Post = NewComboCounterRef("api.login.post")
M_Api_Login_OAuth = NewComboCounterRef("api.login.oauth")
M_Api_Org_Create = NewComboCounterRef("api.org.create")
M_Models_Dashboard_Insert = NewComboCounterRef("models.dashboard.insert")
)
package metrics
import (
"fmt"
"reflect"
"sync"
)
// DuplicateMetric is the error returned by Registry.Register when a metric
// already exists. If you mean to Register that metric you must first
// Unregister the existing metric.
type DuplicateMetric string
func (err DuplicateMetric) Error() string {
return fmt.Sprintf("duplicate metric: %s", string(err))
}
type Registry interface {
// Call the given function for each registered metric.
Each(func(string, interface{}))
// Get the metric by the given name or nil if none is registered.
Get(string) interface{}
// Gets an existing metric or registers the given one.
// The interface can be the metric to register if not found in registry,
// or a function returning the metric for lazy instantiation.
GetOrRegister(string, interface{}) interface{}
// Register the given metric under the given name.
Register(string, interface{}) error
}
// The standard implementation of a Registry is a mutex-protected map
// of names to metrics.
type StandardRegistry struct {
metrics map[string]interface{}
mutex sync.Mutex
}
// Create a new registry.
func NewRegistry() Registry {
return &StandardRegistry{metrics: make(map[string]interface{})}
}
// Call the given function for each registered metric.
func (r *StandardRegistry) Each(f func(string, interface{})) {
for name, i := range r.registered() {
f(name, i)
}
}
// Get the metric by the given name or nil if none is registered.
func (r *StandardRegistry) Get(name string) interface{} {
r.mutex.Lock()
defer r.mutex.Unlock()
return r.metrics[name]
}
// Gets an existing metric or creates and registers a new one. Threadsafe
// alternative to calling Get and Register on failure.
// The interface can be the metric to register if not found in registry,
// or a function returning the metric for lazy instantiation.
func (r *StandardRegistry) GetOrRegister(name string, i interface{}) interface{} {
r.mutex.Lock()
defer r.mutex.Unlock()
if metric, ok := r.metrics[name]; ok {
return metric
}
if v := reflect.ValueOf(i); v.Kind() == reflect.Func {
i = v.Call(nil)[0].Interface()
}
r.register(name, i)
return i
}
// Register the given metric under the given name. Returns a DuplicateMetric
// if a metric by the given name is already registered.
func (r *StandardRegistry) Register(name string, i interface{}) error {
r.mutex.Lock()
defer r.mutex.Unlock()
return r.register(name, i)
}
func (r *StandardRegistry) register(name string, i interface{}) error {
if _, ok := r.metrics[name]; ok {
return DuplicateMetric(name)
}
r.metrics[name] = i
return nil
}
func (r *StandardRegistry) registered() map[string]interface{} {
metrics := make(map[string]interface{}, len(r.metrics))
r.mutex.Lock()
defer r.mutex.Unlock()
for name, i := range r.metrics {
metrics[name] = i
}
return metrics
}
package metrics
import (
"bytes"
"encoding/json"
"net/http"
"strings"
"time"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/setting"
)
func StartUsageReportLoop() chan struct{} {
M_Instance_Start.Inc(1)
ticker := time.NewTicker(time.Hour)
for {
select {
case <-ticker.C:
sendUsageStats()
}
}
}
func sendUsageStats() {
log.Trace("Sending anonymous usage stats to stats.grafana.org")
version := strings.Replace(setting.BuildVersion, ".", "_", -1)
metrics := map[string]interface{}{}
report := map[string]interface{}{
"version": version,
"metrics": metrics,
}
// statsQuery := m.GetSystemStatsQuery{}
// if err := bus.Dispatch(&statsQuery); err != nil {
// log.Error(3, "Failed to get system stats", err)
// return
// }
UsageStats.Each(func(name string, i interface{}) {
switch metric := i.(type) {
case Counter:
if metric.Count() > 0 {
metrics[name+".count"] = metric.Count()
metric.Clear()
}
}
})
// metrics["stats.dashboards.count"] = statsQuery.Result.DashboardCount
// metrics["stats.users.count"] = statsQuery.Result.UserCount
// metrics["stats.orgs.count"] = statsQuery.Result.OrgCount
out, _ := json.Marshal(report)
data := bytes.NewBuffer(out)
client := http.Client{Timeout: time.Duration(5 * time.Second)}
go client.Post("https://stats.grafana.org/grafana-usage-report", "application/json", data)
}
......@@ -22,13 +22,6 @@ func getRequestUserId(c *Context) int64 {
return userId.(int64)
}
// TODO: figure out a way to secure this
if c.Req.URL.Query().Get("render") == "1" {
userId := c.QueryInt64(SESS_KEY_USERID)
c.Session.Set(SESS_KEY_USERID, userId)
return userId
}
return 0
}
......
......@@ -10,6 +10,7 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/apikeygen"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/metrics"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
)
......@@ -99,6 +100,15 @@ func (ctx *Context) Handle(status int, title string, err error) {
}
}
switch status {
case 200:
metrics.M_Page_Status_200.Inc(1)
case 404:
metrics.M_Page_Status_404.Inc(1)
case 500:
metrics.M_Page_Status_500.Inc(1)
}
ctx.Data["Title"] = title
ctx.HTML(status, strconv.Itoa(status))
}
......@@ -128,7 +138,9 @@ func (ctx *Context) JsonApiErr(status int, message string, err error) {
switch status {
case 404:
resp["message"] = "Not Found"
metrics.M_Api_Status_500.Inc(1)
case 500:
metrics.M_Api_Status_404.Inc(1)
resp["message"] = "Internal Server Error"
}
......
package models
type SystemStats struct {
DashboardCount int
UserCount int
OrgCount int
}
type GetSystemStatsQuery struct {
Result *SystemStats
}
......@@ -6,6 +6,7 @@ import (
"github.com/go-xorm/xorm"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/metrics"
m "github.com/grafana/grafana/pkg/models"
)
......@@ -48,6 +49,7 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error {
}
if dash.Id == 0 {
metrics.M_Models_Dashboard_Insert.Inc(1)
_, err = sess.Insert(dash)
} else {
dash.Version += 1
......
package sqlstore
import (
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
)
func init() {
bus.AddHandler("sql", GetSystemStats)
}
func GetSystemStats(query *m.GetSystemStatsQuery) error {
var rawSql = `SELECT
(
SELECT COUNT(*)
FROM ` + dialect.Quote("user") + `
) AS user_count,
(
SELECT COUNT(*)
FROM ` + dialect.Quote("org") + `
) AS org_count,
(
SELECT COUNT(*)
FROM ` + dialect.Quote("dashboard") + `
) AS dashboard_count
`
var stats m.SystemStats
_, err := x.Sql(rawSql).Get(&stats)
if err != nil {
return err
}
query.Result = &stats
return err
}
......@@ -96,6 +96,8 @@ var (
PhantomDir string
configFiles []string
ReportingEnabled bool
)
func init() {
......@@ -233,6 +235,8 @@ func NewConfigContext(config string) {
ImagesDir = "data/png"
PhantomDir = "vendor/phantomjs"
ReportingEnabled = Cfg.Section("").Key("reporting-enabled").MustBool(true)
readSessionConfig()
}
......
......@@ -2,23 +2,23 @@
<div class="section">
<h5>Drilldown / detail link<tip>These links appear in the dropdown menu in the panel menu. </tip></h5>
<div class="tight-form" ng-repeat="link in panel.links"j>
<div class="tight-form" ng-repeat="link in panel.links">
<ul class="tight-form-list">
<li class="tight-form-item">
<i class="fa fa-remove pointer" ng-click="deleteLink(link)"></i>
</li>
<li class="tight-form-item">title</li>
<li class="tight-form-item" style="width: 80px;">Link title</li>
<li>
<input type="text" ng-model="link.title" class="input-medium tight-form-input">
</li>
<li class="tight-form-item">type</li>
<li class="tight-form-item">Type</li>
<li>
<select class="input-medium tight-form-input" style="width: 101px;" ng-model="link.type" ng-options="f for f in ['dashboard','absolute']"></select>
</li>
<li class="tight-form-item" ng-show="link.type === 'dashboard'">dashboard</li>
<li class="tight-form-item" ng-show="link.type === 'dashboard'">Dashboard</li>
<li ng-show="link.type === 'dashboard'">
<input type="text"
ng-model="link.dashboard"
......@@ -26,20 +26,30 @@
class="input-large tight-form-input">
</li>
<li class="tight-form-item" ng-show="link.type === 'absolute'">url</li>
<li class="tight-form-item" ng-show="link.type === 'absolute'">Url</li>
<li ng-show="link.type === 'absolute'">
<input type="text" ng-model="link.url" class="input-large tight-form-input">
<input type="text" ng-model="link.url" class="input-xlarge tight-form-input">
</li>
</ul>
<div class="clearfix"></div>
</div>
<li class="tight-form-item">params
<div class="tight-form">
<ul class="tight-form-list" role="menu">
<li class="tight-form-item">
<i class="fa fa-remove invisible"></i>
</li>
<li class="tight-form-item" style="width: 80px;">
Params
<tip>Use var-variableName=value to pass templating variables.</tip>
</li>
<li>
<input type="text" ng-model="link.params" class="input-medium tight-form-input">
<input type="text" ng-model="link.params" class="input-xxlarge tight-form-input">
</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
</div>
......
......@@ -396,8 +396,8 @@ function (angular, $, kbn, moment, _, GraphTooltip) {
}
}
axis.min = axis.min !== null ? axis.min : 1;
axis.ticks = [1];
axis.min = axis.min !== null ? axis.min : 0;
axis.ticks = [0, 1];
var nextTick = 1;
while (true) {
......@@ -409,10 +409,10 @@ function (angular, $, kbn, moment, _, GraphTooltip) {
}
if (axis.logBase === 10) {
axis.transform = function(v) { return Math.log(v+0.0001); };
axis.transform = function(v) { return Math.log(v+0.1); };
axis.inverseTransform = function (v) { return Math.pow(10,v); };
} else {
axis.transform = function(v) { return Math.log(v+0.0001) / Math.log(axis.logBase); };
axis.transform = function(v) { return Math.log(v+0.1) / Math.log(axis.logBase); };
axis.inverseTransform = function (v) { return Math.pow(axis.logBase,v); };
}
}
......
......@@ -153,9 +153,9 @@ define([
it('should apply axis transform and ticks', function() {
var axis = ctx.plotOptions.yaxes[0];
expect(axis.transform(100)).to.be(Math.log(100+0.0001));
expect(axis.ticks[0]).to.be(1);
expect(axis.ticks[1]).to.be(10);
expect(axis.transform(100)).to.be(Math.log(100+0.1));
expect(axis.ticks[0]).to.be(0);
expect(axis.ticks[1]).to.be(1);
});
});
......
......@@ -9,13 +9,19 @@ args.forEach(function(arg) {
params[parts[1]] = parts[2];
});
var usage = "url=<url> png=<filename> width=<width> height=<height>";
var usage = "url=<url> png=<filename> width=<width> height=<height> cookiename=<cookiename> sessionid=<sessionid> domain=<domain>";
if (!params.url || !params.png) {
if (!params.url || !params.png || !params.cookiename || ! params.sessionid || !params.domain) {
console.log(usage);
phantom.exit();
}
phantom.addCookie({
'name': params.cookiename,
'value': params.sessionid,
'domain': params.domain
});
page.viewportSize = {
width: params.width || '800',
height: params.height || '400'
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment