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. ![image](https://github.com/go-gitea/gitea/assets/2114189/babf7c61-b93b-4a62-bfaa-22983636427e) ## ⚠️ 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