diff --git a/docs/sources/reference/http_api.md b/docs/sources/reference/http_api.md index 7cdd8a872d5..4b0dff6c4b6 100644 --- a/docs/sources/reference/http_api.md +++ b/docs/sources/reference/http_api.md @@ -1422,6 +1422,34 @@ Keys: } } +### Grafana Stats + +`GET /api/admin/stats` + +**Example Request**: + + GET /api/admin/stats + Accept: application/json + Content-Type: application/json + Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk + +**Example Response**: + + HTTP/1.1 200 + Content-Type: application/json + + { + "user_count":2, + "org_count":1, + "dashboard_count":4, + "db_snapshot_count":2, + "db_tag_count":6, + "data_source_count":1, + "playlist_count":1, + "starred_db_count":2, + "grafana_admin_count":2 + } + ### Global Users `POST /api/admin/users` diff --git a/pkg/api/admin_settings.go b/pkg/api/admin.go similarity index 66% rename from pkg/api/admin_settings.go rename to pkg/api/admin.go index 1f800cfe558..d7f5a240416 100644 --- a/pkg/api/admin_settings.go +++ b/pkg/api/admin.go @@ -3,7 +3,9 @@ package api import ( "strings" + "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/middleware" + m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" ) @@ -27,3 +29,15 @@ func AdminGetSettings(c *middleware.Context) { c.JSON(200, settings) } + +func AdminGetStats(c *middleware.Context) { + + statsQuery := m.GetAdminStatsQuery{} + + if err := bus.Dispatch(&statsQuery); err != nil { + c.JsonApiErr(500, "Failed to get admin stats from database", err) + return + } + + c.JSON(200, statsQuery.Result) +} diff --git a/pkg/api/api.go b/pkg/api/api.go index 8bbb64eb1b2..99ab8a51ff2 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -40,6 +40,7 @@ func Register(r *macaron.Macaron) { r.Get("/admin/users/edit/:id", reqGrafanaAdmin, Index) r.Get("/admin/orgs", reqGrafanaAdmin, Index) r.Get("/admin/orgs/edit/:id", reqGrafanaAdmin, Index) + r.Get("/admin/stats", reqGrafanaAdmin, Index) r.Get("/apps", reqSignedIn, Index) r.Get("/apps/edit/*", reqSignedIn, Index) @@ -210,6 +211,7 @@ func Register(r *macaron.Macaron) { r.Delete("/users/:id", AdminDeleteUser) r.Get("/users/:id/quotas", wrap(GetUserQuotas)) r.Put("/users/:id/quotas/:target", bind(m.UpdateUserQuotaCmd{}), wrap(UpdateUserQuota)) + r.Get("/stats", AdminGetStats) }, reqGrafanaAdmin) // rendering diff --git a/pkg/models/stats.go b/pkg/models/stats.go index 30c09deb768..63f946b956b 100644 --- a/pkg/models/stats.go +++ b/pkg/models/stats.go @@ -19,3 +19,19 @@ type GetSystemStatsQuery struct { type GetDataSourceStatsQuery struct { Result []*DataSourceStats } + +type AdminStats struct { + UserCount int `json:"user_count"` + OrgCount int `json:"org_count"` + DashboardCount int `json:"dashboard_count"` + DbSnapshotCount int `json:"db_snapshot_count"` + DbTagCount int `json:"db_tag_count"` + DataSourceCount int `json:"data_source_count"` + PlaylistCount int `json:"playlist_count"` + StarredDbCount int `json:"starred_db_count"` + GrafanaAdminCount int `json:"grafana_admin_count"` +} + +type GetAdminStatsQuery struct { + Result *AdminStats +} diff --git a/pkg/services/sqlstore/stats.go b/pkg/services/sqlstore/stats.go index 6c5dfaea906..0465c6999a9 100644 --- a/pkg/services/sqlstore/stats.go +++ b/pkg/services/sqlstore/stats.go @@ -8,6 +8,7 @@ import ( func init() { bus.AddHandler("sql", GetSystemStats) bus.AddHandler("sql", GetDataSourceStats) + bus.AddHandler("sql", GetAdminStats) } func GetDataSourceStats(query *m.GetDataSourceStatsQuery) error { @@ -50,3 +51,54 @@ func GetSystemStats(query *m.GetSystemStatsQuery) error { query.Result = &stats return err } + +func GetAdminStats(query *m.GetAdminStatsQuery) 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, + ( + SELECT COUNT(*) + FROM ` + dialect.Quote("dashboard_snapshot") + ` + ) AS db_snapshot_count, + ( + SELECT COUNT(*) + FROM ` + dialect.Quote("dashboard_tag") + ` + ) AS db_tag_count, + ( + SELECT COUNT(*) + FROM ` + dialect.Quote("data_source") + ` + ) AS data_source_count, + ( + SELECT COUNT(*) + FROM ` + dialect.Quote("playlist") + ` + ) AS playlist_count, + ( + SELECT COUNT (DISTINCT ` + dialect.Quote("dashboard_id") + ` ) + FROM ` + dialect.Quote("star") + ` + ) AS starred_db_count, + ( + SELECT COUNT(*) + FROM ` + dialect.Quote("user") + ` + WHERE ` + dialect.Quote("is_admin") + ` = 1 + ) AS grafana_admin_count + ` + + var stats m.AdminStats + _, err := x.Sql(rawSql).Get(&stats) + if err != nil { + return err + } + + query.Result = &stats + return err +} diff --git a/public/app/core/components/sidemenu/sidemenu.ts b/public/app/core/components/sidemenu/sidemenu.ts index d2a640c1345..8f0c57bfade 100644 --- a/public/app/core/components/sidemenu/sidemenu.ts +++ b/public/app/core/components/sidemenu/sidemenu.ts @@ -107,6 +107,12 @@ export class SideMenuCtrl { url: this.getUrl("/admin/settings"), }); + this.mainLinks.push({ + text: "Grafana stats", + icon: "fa fa-fw fa-bar-chart", + url: this.getUrl("/admin/stats"), + }); + this.mainLinks.push({ text: "Global Users", icon: "fa fa-fw fa-user", @@ -118,6 +124,7 @@ export class SideMenuCtrl { icon: "fa fa-fw fa-users", url: this.getUrl("/admin/orgs"), }); + } updateMenu() { diff --git a/public/app/core/routes/all.js b/public/app/core/routes/all.js index cc4d73ef708..d9726ee782c 100644 --- a/public/app/core/routes/all.js +++ b/public/app/core/routes/all.js @@ -112,6 +112,11 @@ define([ templateUrl: 'app/features/admin/partials/edit_org.html', controller : 'AdminEditOrgCtrl', }) + .when('/admin/stats', { + templateUrl: 'app/features/admin/partials/stats.html', + controller : 'AdminStatsCtrl', + controllerAs: 'ctrl', + }) .when('/login', { templateUrl: 'app/partials/login.html', controller : 'LoginCtrl', diff --git a/public/app/features/admin/adminStatsCtrl.ts b/public/app/features/admin/adminStatsCtrl.ts new file mode 100644 index 00000000000..aa3ed6de343 --- /dev/null +++ b/public/app/features/admin/adminStatsCtrl.ts @@ -0,0 +1,18 @@ +/// + +import angular from 'angular'; + +export class AdminStatsCtrl { + stats: any; + + /** @ngInject */ + constructor(private backendSrv: any) {} + + init() { + this.backendSrv.get('/api/admin/stats').then(stats => { + this.stats = stats; + }); + } +} + +angular.module('grafana.controllers').controller('AdminStatsCtrl', AdminStatsCtrl); diff --git a/public/app/features/admin/all.js b/public/app/features/admin/all.js index 14bff249b0e..786210f064f 100644 --- a/public/app/features/admin/all.js +++ b/public/app/features/admin/all.js @@ -4,4 +4,5 @@ define([ './adminEditOrgCtrl', './adminEditUserCtrl', './adminSettingsCtrl', + './adminStatsCtrl', ], function () {}); diff --git a/public/app/features/admin/partials/stats.html b/public/app/features/admin/partials/stats.html new file mode 100644 index 00000000000..4949e71e441 --- /dev/null +++ b/public/app/features/admin/partials/stats.html @@ -0,0 +1,60 @@ + + + + +
+
+

+ Overview +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameValue
Total dashboards{{ctrl.stats.dashboard_count}}
Total users{{ctrl.stats.user_count}}
Total grafana admins{{ctrl.stats.grafana_admin_count}}
Total organizations{{ctrl.stats.org_count}}
Total datasources{{ctrl.stats.data_source_count}}
Total playlists{{ctrl.stats.playlist_count}}
Total snapshots{{ctrl.stats.db_snapshot_count}}
Total dashboard tags{{ctrl.stats.db_tag_count}}
Total starred dashboards{{ctrl.stats.starred_db_count}}
+
+