Use a separate admin page to show global stats, remove `actions` stat (#25062)
Before, Gitea shows the database table stats on the `admin dashboard` page. It has some problems: * `count(*)` is quite heavy. If tables have many records, this blocks loading the admin page blocks for a long time * Some users had even reported issues that they can't visit their admin page because this page causes blocking or `50x error (reverse proxy timeout)` * The `actions` stat is not useful. The table is simply too large. Does it really matter if it contains 1,000,000 rows or 9,999,999 rows? * The translation `admin.dashboard.statistic_info` is difficult to maintain. So, this PR uses a separate page to show the stats and removes the `actions` stat.  ## ⚠️ BREAKING The `actions` Prometheus metrics collector has been removed for the reasons mentioned beforehand. Please do not rely on its output anymore.
This commit is contained in:
		
							parent
							
								
									4486dd39e7
								
							
						
					
					
						commit
						520eb57d76
					
				|  | @ -21,7 +21,7 @@ import ( | ||||||
| type Statistic struct { | type Statistic struct { | ||||||
| 	Counter struct { | 	Counter struct { | ||||||
| 		User, Org, PublicKey, | 		User, Org, PublicKey, | ||||||
| 		Repo, Watch, Star, Action, Access, | 		Repo, Watch, Star, Access, | ||||||
| 		Issue, IssueClosed, IssueOpen, | 		Issue, IssueClosed, IssueOpen, | ||||||
| 		Comment, Oauth, Follow, | 		Comment, Oauth, Follow, | ||||||
| 		Mirror, Release, AuthSource, Webhook, | 		Mirror, Release, AuthSource, Webhook, | ||||||
|  | @ -55,7 +55,6 @@ func GetStatistic() (stats Statistic) { | ||||||
| 	stats.Counter.Repo, _ = repo_model.CountRepositories(db.DefaultContext, repo_model.CountRepositoryOptions{}) | 	stats.Counter.Repo, _ = repo_model.CountRepositories(db.DefaultContext, repo_model.CountRepositoryOptions{}) | ||||||
| 	stats.Counter.Watch, _ = e.Count(new(repo_model.Watch)) | 	stats.Counter.Watch, _ = e.Count(new(repo_model.Watch)) | ||||||
| 	stats.Counter.Star, _ = e.Count(new(repo_model.Star)) | 	stats.Counter.Star, _ = e.Count(new(repo_model.Star)) | ||||||
| 	stats.Counter.Action, _ = db.EstimateCount(db.DefaultContext, new(Action)) |  | ||||||
| 	stats.Counter.Access, _ = e.Count(new(access_model.Access)) | 	stats.Counter.Access, _ = e.Count(new(access_model.Access)) | ||||||
| 
 | 
 | ||||||
| 	type IssueCount struct { | 	type IssueCount struct { | ||||||
|  | @ -83,7 +82,7 @@ func GetStatistic() (stats Statistic) { | ||||||
| 			Find(&stats.Counter.IssueByRepository) | 			Find(&stats.Counter.IssueByRepository) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	issueCounts := []IssueCount{} | 	var issueCounts []IssueCount | ||||||
| 
 | 
 | ||||||
| 	_ = e.Select("COUNT(*) AS count, is_closed").Table("issue").GroupBy("is_closed").Find(&issueCounts) | 	_ = e.Select("COUNT(*) AS count, is_closed").Table("issue").GroupBy("is_closed").Find(&issueCounts) | ||||||
| 	for _, c := range issueCounts { | 	for _, c := range issueCounts { | ||||||
|  |  | ||||||
|  | @ -9,7 +9,6 @@ import ( | ||||||
| 
 | 
 | ||||||
| 	"xorm.io/builder" | 	"xorm.io/builder" | ||||||
| 	"xorm.io/xorm" | 	"xorm.io/xorm" | ||||||
| 	"xorm.io/xorm/schemas" |  | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // DefaultContext is the default context to run xorm queries in
 | // DefaultContext is the default context to run xorm queries in
 | ||||||
|  | @ -241,30 +240,6 @@ func TableName(bean interface{}) string { | ||||||
| 	return x.TableName(bean) | 	return x.TableName(bean) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // EstimateCount returns an estimate of total number of rows in table
 |  | ||||||
| func EstimateCount(ctx context.Context, bean interface{}) (int64, error) { |  | ||||||
| 	e := GetEngine(ctx) |  | ||||||
| 	e.Context(ctx) |  | ||||||
| 
 |  | ||||||
| 	var rows int64 |  | ||||||
| 	var err error |  | ||||||
| 	tablename := TableName(bean) |  | ||||||
| 	switch x.Dialect().URI().DBType { |  | ||||||
| 	case schemas.MYSQL: |  | ||||||
| 		_, err = e.Context(ctx).SQL("SELECT table_rows FROM information_schema.tables WHERE tables.table_name = ? AND tables.table_schema = ?;", tablename, x.Dialect().URI().DBName).Get(&rows) |  | ||||||
| 	case schemas.POSTGRES: |  | ||||||
| 		// the table can live in multiple schemas of a postgres database
 |  | ||||||
| 		// See https://wiki.postgresql.org/wiki/Count_estimate
 |  | ||||||
| 		tablename = x.TableName(bean, true) |  | ||||||
| 		_, err = e.Context(ctx).SQL("SELECT reltuples::bigint AS estimate FROM pg_class WHERE oid = ?::regclass;", tablename).Get(&rows) |  | ||||||
| 	case schemas.MSSQL: |  | ||||||
| 		_, err = e.Context(ctx).SQL("sp_spaceused ?;", tablename).Get(&rows) |  | ||||||
| 	default: |  | ||||||
| 		return e.Context(ctx).Count(tablename) |  | ||||||
| 	} |  | ||||||
| 	return rows, err |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // InTransaction returns true if the engine is in a transaction otherwise return false
 | // InTransaction returns true if the engine is in a transaction otherwise return false
 | ||||||
| func InTransaction(ctx context.Context) bool { | func InTransaction(ctx context.Context) bool { | ||||||
| 	_, ok := inTransaction(ctx) | 	_, ok := inTransaction(ctx) | ||||||
|  |  | ||||||
|  | @ -18,7 +18,6 @@ const namespace = "gitea_" | ||||||
| // exposes gitea metrics for prometheus
 | // exposes gitea metrics for prometheus
 | ||||||
| type Collector struct { | type Collector struct { | ||||||
| 	Accesses           *prometheus.Desc | 	Accesses           *prometheus.Desc | ||||||
| 	Actions            *prometheus.Desc |  | ||||||
| 	Attachments        *prometheus.Desc | 	Attachments        *prometheus.Desc | ||||||
| 	BuildInfo          *prometheus.Desc | 	BuildInfo          *prometheus.Desc | ||||||
| 	Comments           *prometheus.Desc | 	Comments           *prometheus.Desc | ||||||
|  | @ -56,11 +55,6 @@ func NewCollector() Collector { | ||||||
| 			"Number of Accesses", | 			"Number of Accesses", | ||||||
| 			nil, nil, | 			nil, nil, | ||||||
| 		), | 		), | ||||||
| 		Actions: prometheus.NewDesc( |  | ||||||
| 			namespace+"actions", |  | ||||||
| 			"Number of Actions", |  | ||||||
| 			nil, nil, |  | ||||||
| 		), |  | ||||||
| 		Attachments: prometheus.NewDesc( | 		Attachments: prometheus.NewDesc( | ||||||
| 			namespace+"attachments", | 			namespace+"attachments", | ||||||
| 			"Number of Attachments", | 			"Number of Attachments", | ||||||
|  | @ -207,7 +201,6 @@ func NewCollector() Collector { | ||||||
| // Describe returns all possible prometheus.Desc
 | // Describe returns all possible prometheus.Desc
 | ||||||
| func (c Collector) Describe(ch chan<- *prometheus.Desc) { | func (c Collector) Describe(ch chan<- *prometheus.Desc) { | ||||||
| 	ch <- c.Accesses | 	ch <- c.Accesses | ||||||
| 	ch <- c.Actions |  | ||||||
| 	ch <- c.Attachments | 	ch <- c.Attachments | ||||||
| 	ch <- c.BuildInfo | 	ch <- c.BuildInfo | ||||||
| 	ch <- c.Comments | 	ch <- c.Comments | ||||||
|  | @ -246,11 +239,6 @@ func (c Collector) Collect(ch chan<- prometheus.Metric) { | ||||||
| 		prometheus.GaugeValue, | 		prometheus.GaugeValue, | ||||||
| 		float64(stats.Counter.Access), | 		float64(stats.Counter.Access), | ||||||
| 	) | 	) | ||||||
| 	ch <- prometheus.MustNewConstMetric( |  | ||||||
| 		c.Actions, |  | ||||||
| 		prometheus.GaugeValue, |  | ||||||
| 		float64(stats.Counter.Action), |  | ||||||
| 	) |  | ||||||
| 	ch <- prometheus.MustNewConstMetric( | 	ch <- prometheus.MustNewConstMetric( | ||||||
| 		c.Attachments, | 		c.Attachments, | ||||||
| 		prometheus.GaugeValue, | 		prometheus.GaugeValue, | ||||||
|  |  | ||||||
|  | @ -2619,7 +2619,6 @@ dashboard.new_version_hint = Gitea %s is now available, you are running %s. Chec | ||||||
| dashboard.statistic = Summary | dashboard.statistic = Summary | ||||||
| dashboard.operations = Maintenance Operations | dashboard.operations = Maintenance Operations | ||||||
| dashboard.system_status = System Status | dashboard.system_status = System Status | ||||||
| dashboard.statistic_info = The Gitea database holds <b>%d</b> users, <b>%d</b> organizations, <b>%d</b> public keys, <b>%d</b> repositories, <b>%d</b> watches, <b>%d</b> stars, ~<b>%d</b> actions, <b>%d</b> accesses, <b>%d</b> issues, <b>%d</b> comments, <b>%d</b> social accounts, <b>%d</b> follows, <b>%d</b> mirrors, <b>%d</b> releases, <b>%d</b> authentication sources, <b>%d</b> webhooks, <b>%d</b> milestones, <b>%d</b> labels, <b>%d</b> hook tasks, <b>%d</b> teams, <b>%d</b> update tasks, <b>%d</b> attachments. |  | ||||||
| dashboard.operation_name = Operation Name | dashboard.operation_name = Operation Name | ||||||
| dashboard.operation_switch = Switch | dashboard.operation_switch = Switch | ||||||
| dashboard.operation_run = Run | dashboard.operation_run = Run | ||||||
|  | @ -3060,6 +3059,8 @@ config.xorm_log_sql = Log SQL | ||||||
| config.get_setting_failed = Get setting %s failed | config.get_setting_failed = Get setting %s failed | ||||||
| config.set_setting_failed = Set setting %s failed | config.set_setting_failed = Set setting %s failed | ||||||
| 
 | 
 | ||||||
|  | monitor.stats = Stats | ||||||
|  | 
 | ||||||
| monitor.cron = Cron Tasks | monitor.cron = Cron Tasks | ||||||
| monitor.name = Name | monitor.name = Name | ||||||
| monitor.schedule = Schedule | monitor.schedule = Schedule | ||||||
|  |  | ||||||
|  | @ -8,11 +8,13 @@ import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"runtime" | 	"runtime" | ||||||
|  | 	"sort" | ||||||
| 	"time" | 	"time" | ||||||
| 
 | 
 | ||||||
| 	activities_model "code.gitea.io/gitea/models/activities" | 	activities_model "code.gitea.io/gitea/models/activities" | ||||||
| 	"code.gitea.io/gitea/modules/base" | 	"code.gitea.io/gitea/modules/base" | ||||||
| 	"code.gitea.io/gitea/modules/context" | 	"code.gitea.io/gitea/modules/context" | ||||||
|  | 	"code.gitea.io/gitea/modules/json" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
| 	"code.gitea.io/gitea/modules/updatechecker" | 	"code.gitea.io/gitea/modules/updatechecker" | ||||||
| 	"code.gitea.io/gitea/modules/web" | 	"code.gitea.io/gitea/modules/web" | ||||||
|  | @ -26,6 +28,7 @@ const ( | ||||||
| 	tplQueue       base.TplName = "admin/queue" | 	tplQueue       base.TplName = "admin/queue" | ||||||
| 	tplStacktrace  base.TplName = "admin/stacktrace" | 	tplStacktrace  base.TplName = "admin/stacktrace" | ||||||
| 	tplQueueManage base.TplName = "admin/queue_manage" | 	tplQueueManage base.TplName = "admin/queue_manage" | ||||||
|  | 	tplStats       base.TplName = "admin/stats" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| var sysStatus struct { | var sysStatus struct { | ||||||
|  | @ -111,7 +114,6 @@ func updateSystemStatus() { | ||||||
| func Dashboard(ctx *context.Context) { | func Dashboard(ctx *context.Context) { | ||||||
| 	ctx.Data["Title"] = ctx.Tr("admin.dashboard") | 	ctx.Data["Title"] = ctx.Tr("admin.dashboard") | ||||||
| 	ctx.Data["PageIsAdminDashboard"] = true | 	ctx.Data["PageIsAdminDashboard"] = true | ||||||
| 	ctx.Data["Stats"] = activities_model.GetStatistic() |  | ||||||
| 	ctx.Data["NeedUpdate"] = updatechecker.GetNeedUpdate() | 	ctx.Data["NeedUpdate"] = updatechecker.GetNeedUpdate() | ||||||
| 	ctx.Data["RemoteVersion"] = updatechecker.GetRemoteVersion() | 	ctx.Data["RemoteVersion"] = updatechecker.GetRemoteVersion() | ||||||
| 	// FIXME: update periodically
 | 	// FIXME: update periodically
 | ||||||
|  | @ -126,7 +128,6 @@ func DashboardPost(ctx *context.Context) { | ||||||
| 	form := web.GetForm(ctx).(*forms.AdminDashboardForm) | 	form := web.GetForm(ctx).(*forms.AdminDashboardForm) | ||||||
| 	ctx.Data["Title"] = ctx.Tr("admin.dashboard") | 	ctx.Data["Title"] = ctx.Tr("admin.dashboard") | ||||||
| 	ctx.Data["PageIsAdminDashboard"] = true | 	ctx.Data["PageIsAdminDashboard"] = true | ||||||
| 	ctx.Data["Stats"] = activities_model.GetStatistic() |  | ||||||
| 	updateSystemStatus() | 	updateSystemStatus() | ||||||
| 	ctx.Data["SysStatus"] = sysStatus | 	ctx.Data["SysStatus"] = sysStatus | ||||||
| 
 | 
 | ||||||
|  | @ -153,3 +154,30 @@ func CronTasks(ctx *context.Context) { | ||||||
| 	ctx.Data["Entries"] = cron.ListTasks() | 	ctx.Data["Entries"] = cron.ListTasks() | ||||||
| 	ctx.HTML(http.StatusOK, tplCron) | 	ctx.HTML(http.StatusOK, tplCron) | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func MonitorStats(ctx *context.Context) { | ||||||
|  | 	ctx.Data["Title"] = ctx.Tr("admin.monitor.stats") | ||||||
|  | 	ctx.Data["PageIsAdminMonitorStats"] = true | ||||||
|  | 	bs, err := json.Marshal(activities_model.GetStatistic().Counter) | ||||||
|  | 	if err != nil { | ||||||
|  | 		ctx.ServerError("MonitorStats", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	statsCounter := map[string]any{} | ||||||
|  | 	err = json.Unmarshal(bs, &statsCounter) | ||||||
|  | 	if err != nil { | ||||||
|  | 		ctx.ServerError("MonitorStats", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	statsKeys := make([]string, 0, len(statsCounter)) | ||||||
|  | 	for k := range statsCounter { | ||||||
|  | 		if statsCounter[k] == nil { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		statsKeys = append(statsKeys, k) | ||||||
|  | 	} | ||||||
|  | 	sort.Strings(statsKeys) | ||||||
|  | 	ctx.Data["StatsKeys"] = statsKeys | ||||||
|  | 	ctx.Data["StatsCounter"] = statsCounter | ||||||
|  | 	ctx.HTML(http.StatusOK, tplStats) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -538,8 +538,8 @@ func registerRoutes(m *web.Route) { | ||||||
| 
 | 
 | ||||||
| 	// ***** START: Admin *****
 | 	// ***** START: Admin *****
 | ||||||
| 	m.Group("/admin", func() { | 	m.Group("/admin", func() { | ||||||
| 		m.Get("", adminReq, admin.Dashboard) | 		m.Get("", admin.Dashboard) | ||||||
| 		m.Post("", adminReq, web.Bind(forms.AdminDashboardForm{}), admin.DashboardPost) | 		m.Post("", web.Bind(forms.AdminDashboardForm{}), admin.DashboardPost) | ||||||
| 
 | 
 | ||||||
| 		m.Group("/config", func() { | 		m.Group("/config", func() { | ||||||
| 			m.Get("", admin.Config) | 			m.Get("", admin.Config) | ||||||
|  | @ -548,6 +548,7 @@ func registerRoutes(m *web.Route) { | ||||||
| 		}) | 		}) | ||||||
| 
 | 
 | ||||||
| 		m.Group("/monitor", func() { | 		m.Group("/monitor", func() { | ||||||
|  | 			m.Get("/stats", admin.MonitorStats) | ||||||
| 			m.Get("/cron", admin.CronTasks) | 			m.Get("/cron", admin.CronTasks) | ||||||
| 			m.Get("/stacktrace", admin.Stacktrace) | 			m.Get("/stacktrace", admin.Stacktrace) | ||||||
| 			m.Post("/stacktrace/cancel/{pid}", admin.StacktraceCancel) | 			m.Post("/stacktrace/cancel/{pid}", admin.StacktraceCancel) | ||||||
|  |  | ||||||
|  | @ -5,14 +5,6 @@ | ||||||
| 				<p>{{(.locale.Tr "admin.dashboard.new_version_hint" .RemoteVersion AppVer) | Str2html}}</p> | 				<p>{{(.locale.Tr "admin.dashboard.new_version_hint" .RemoteVersion AppVer) | Str2html}}</p> | ||||||
| 			</div> | 			</div> | ||||||
| 		{{end}} | 		{{end}} | ||||||
| 		<h4 class="ui top attached header"> |  | ||||||
| 			{{.locale.Tr "admin.dashboard.statistic"}} |  | ||||||
| 		</h4> |  | ||||||
| 		<div class="ui attached segment"> |  | ||||||
| 			<p> |  | ||||||
| 				{{.locale.Tr "admin.dashboard.statistic_info" .Stats.Counter.User .Stats.Counter.Org .Stats.Counter.PublicKey .Stats.Counter.Repo .Stats.Counter.Watch .Stats.Counter.Star .Stats.Counter.Action .Stats.Counter.Access .Stats.Counter.Issue .Stats.Counter.Comment .Stats.Counter.Oauth .Stats.Counter.Follow .Stats.Counter.Mirror .Stats.Counter.Release .Stats.Counter.AuthSource .Stats.Counter.Webhook .Stats.Counter.Milestone .Stats.Counter.Label .Stats.Counter.HookTask .Stats.Counter.Team .Stats.Counter.UpdateTask .Stats.Counter.Attachment | Str2html}} |  | ||||||
| 			</p> |  | ||||||
| 		</div> |  | ||||||
| 		<h4 class="ui top attached header"> | 		<h4 class="ui top attached header"> | ||||||
| 			{{.locale.Tr "admin.dashboard.operations"}} | 			{{.locale.Tr "admin.dashboard.operations"}} | ||||||
| 		</h4> | 		</h4> | ||||||
|  |  | ||||||
|  | @ -53,6 +53,9 @@ | ||||||
| 		<div class="item"> | 		<div class="item"> | ||||||
| 			{{.locale.Tr "admin.monitor"}} | 			{{.locale.Tr "admin.monitor"}} | ||||||
| 			<div class="menu"> | 			<div class="menu"> | ||||||
|  | 				<a class="{{if .PageIsAdminMonitorStats}}active {{end}}item" href="{{AppSubUrl}}/admin/monitor/stats"> | ||||||
|  | 					{{.locale.Tr "admin.monitor.stats"}} | ||||||
|  | 				</a> | ||||||
| 				<a class="{{if .PageIsAdminMonitorCron}}active {{end}}item" href="{{AppSubUrl}}/admin/monitor/cron"> | 				<a class="{{if .PageIsAdminMonitorCron}}active {{end}}item" href="{{AppSubUrl}}/admin/monitor/cron"> | ||||||
| 					{{.locale.Tr "admin.monitor.cron"}} | 					{{.locale.Tr "admin.monitor.cron"}} | ||||||
| 				</a> | 				</a> | ||||||
|  |  | ||||||
|  | @ -0,0 +1,17 @@ | ||||||
|  | {{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin monitor")}} | ||||||
|  | <div class="admin-setting-content"> | ||||||
|  | 	<h4 class="ui top attached header"> | ||||||
|  | 		{{.locale.Tr "admin.dashboard.statistic"}} | ||||||
|  | 	</h4> | ||||||
|  | 	<div class="ui attached table segment"> | ||||||
|  | 		<table class="ui very basic striped table unstackable"> | ||||||
|  | 			{{range $statsKey := .StatsKeys}} | ||||||
|  | 			<tr> | ||||||
|  | 				<td width="200">{{$statsKey}}</td> | ||||||
|  | 				<td>{{index $.StatsCounter $statsKey}}</td> | ||||||
|  | 			</tr> | ||||||
|  | 			{{end}} | ||||||
|  | 		</table> | ||||||
|  | 	</div> | ||||||
|  | </div> | ||||||
|  | {{template "admin/layout_footer" .}} | ||||||
		Loading…
	
		Reference in New Issue