Compare commits

..

1 Commits

Author SHA1 Message Date
Jonathan Miller d300cde639 feat(metrics): add active users, actions queue/running to Prometheus (#42)
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Extend the existing /metrics endpoint with 3 new application metrics:
- gitea_active_users_30d: users active in last 30 days
- gitea_actions_queue_length: pending action jobs
- gitea_actions_running_jobs: currently running jobs

No new dependencies — extends existing collector and statistic model.

Closes #42

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-19 21:44:37 -05:00
7 changed files with 63 additions and 151 deletions
+21
View File
@@ -6,6 +6,7 @@ package activities
import (
"context"
actions_model "code.gitea.io/gitea/models/actions"
asymkey_model "code.gitea.io/gitea/models/asymkey"
"code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
@@ -18,6 +19,7 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
)
@@ -37,6 +39,11 @@ type Statistic struct {
Branches, Tags, CommitStatus int64
IssueByLabel []IssueByLabelCount
IssueByRepository []IssueByRepositoryCount
// MokoGitea extended metrics
ActiveUsers30d int64
ActionsQueueLength int64
ActionsRunningJobs int64
}
}
@@ -131,5 +138,19 @@ func GetStatistic(ctx context.Context) (stats Statistic) {
stats.Counter.Attachment, _ = e.Count(new(repo_model.Attachment))
stats.Counter.Project, _ = e.Count(new(project_model.Project))
stats.Counter.ProjectColumn, _ = e.Count(new(project_model.Column))
// MokoGitea extended metrics
// Active users in last 30 days (users who performed any action)
stats.Counter.ActiveUsers30d, _ = e.Where("last_login_unix > ?",
timeutil.TimeStampNow()-30*24*60*60).Count(new(user_model.User))
// Actions queue and running jobs (if actions enabled)
if setting.Actions.Enabled {
stats.Counter.ActionsQueueLength, _ = e.Where("status = ?", 1). // StatusWaiting
Count(new(actions_model.ActionRunJob))
stats.Counter.ActionsRunningJobs, _ = e.Where("status = ?", 2). // StatusRunning
Count(new(actions_model.ActionRunJob))
}
return stats
}
+40
View File
@@ -46,6 +46,11 @@ type Collector struct {
Users *prometheus.Desc
Watches *prometheus.Desc
Webhooks *prometheus.Desc
// MokoGitea extended metrics
ActiveUsers30d *prometheus.Desc
ActionsQueueLength *prometheus.Desc
ActionsRunningJobs *prometheus.Desc
}
// NewCollector returns a new Collector with all prometheus.Desc initialized
@@ -196,6 +201,21 @@ func NewCollector() Collector {
"Number of Webhooks",
nil, nil,
),
ActiveUsers30d: prometheus.NewDesc(
namespace+"active_users_30d",
"Number of active users in the last 30 days",
nil, nil,
),
ActionsQueueLength: prometheus.NewDesc(
namespace+"actions_queue_length",
"Number of actions jobs waiting to run",
nil, nil,
),
ActionsRunningJobs: prometheus.NewDesc(
namespace+"actions_running_jobs",
"Number of actions jobs currently running",
nil, nil,
),
}
}
@@ -229,6 +249,9 @@ func (c Collector) Describe(ch chan<- *prometheus.Desc) {
ch <- c.Users
ch <- c.Watches
ch <- c.Webhooks
ch <- c.ActiveUsers30d
ch <- c.ActionsQueueLength
ch <- c.ActionsRunningJobs
}
// Collect returns the metrics with values
@@ -392,4 +415,21 @@ func (c Collector) Collect(ch chan<- prometheus.Metric) {
prometheus.GaugeValue,
float64(stats.Counter.Webhook),
)
// MokoGitea extended metrics
ch <- prometheus.MustNewConstMetric(
c.ActiveUsers30d,
prometheus.GaugeValue,
float64(stats.Counter.ActiveUsers30d),
)
ch <- prometheus.MustNewConstMetric(
c.ActionsQueueLength,
prometheus.GaugeValue,
float64(stats.Counter.ActionsQueueLength),
)
ch <- prometheus.MustNewConstMetric(
c.ActionsRunningJobs,
prometheus.GaugeValue,
float64(stats.Counter.ActionsRunningJobs),
)
}
-16
View File
@@ -28,15 +28,6 @@ var (
CfgProvider ConfigProvider
IsWindows bool
// UpdateChecker configuration for MokoGitea version checking
UpdateChecker = struct {
Enabled bool
Endpoint string
}{
Enabled: true,
Endpoint: "https://git.mokoconsulting.tech/api/v1/repos/MokoConsulting/MokoGitea/releases/latest",
}
// IsInTesting indicates whether the testing is running (unit test or integration test). It can be used for:
// * Skip nonsense error logs during testing caused by unreliable code (TODO: this is only a temporary solution, we should make the test code more reliable)
// * Panic in dev or testing mode to make the problem more obvious and easier to debug
@@ -167,16 +158,9 @@ func loadCommonSettingsFrom(cfg ConfigProvider) error {
loadMarkupFrom(cfg)
loadGlobalLockFrom(cfg)
loadOtherFrom(cfg)
loadUpdateCheckerFrom(cfg)
return nil
}
func loadUpdateCheckerFrom(cfg ConfigProvider) {
sec := cfg.Section("update_checker")
UpdateChecker.Enabled = sec.Key("ENABLED").MustBool(true)
UpdateChecker.Endpoint = sec.Key("ENDPOINT").MustString(UpdateChecker.Endpoint)
}
func loadRunModeFrom(rootCfg ConfigProvider) {
rootSec := rootCfg.Section("")
RunUser = rootSec.Key("RUN_USER").MustString(user.CurrentUsername())
-106
View File
@@ -1,106 +0,0 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package updatechecker
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"sync"
"time"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
)
// UpdateInfo holds the result of the latest update check.
type UpdateInfo struct {
UpdateAvailable bool
LatestVersion string
ReleaseURL string
CheckedAt time.Time
}
var (
cachedInfo *UpdateInfo
mu sync.RWMutex
)
// giteaRelease is the subset of Gitea's release API response we need.
type giteaRelease struct {
TagName string `json:"tag_name"`
HTMLURL string `json:"html_url"`
Draft bool `json:"draft"`
}
// CheckForUpdate fetches the latest release from the configured endpoint
// and compares it to the running version.
func CheckForUpdate() error {
if !setting.UpdateChecker.Enabled || setting.UpdateChecker.Endpoint == "" {
return nil
}
client := &http.Client{Timeout: 15 * time.Second}
resp, err := client.Get(setting.UpdateChecker.Endpoint)
if err != nil {
return fmt.Errorf("update check failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("update check returned status %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("reading update response: %w", err)
}
var release giteaRelease
if err := json.Unmarshal(body, &release); err != nil {
return fmt.Errorf("parsing update response: %w", err)
}
if release.Draft || release.TagName == "" {
return nil
}
latestVersion := strings.TrimPrefix(release.TagName, "v")
currentVersion := setting.AppVer
info := &UpdateInfo{
LatestVersion: latestVersion,
ReleaseURL: release.HTMLURL,
CheckedAt: time.Now(),
}
// Simple comparison: if latest != current, update is available.
// This handles both upgrades and the case where versions differ
// in any way (patch, upstream bump, etc.)
info.UpdateAvailable = latestVersion != "" && !strings.HasPrefix(currentVersion, latestVersion)
mu.Lock()
cachedInfo = info
mu.Unlock()
if info.UpdateAvailable {
log.Info("MokoGitea update available: %s (current: %s)", latestVersion, currentVersion)
} else {
log.Debug("MokoGitea is up to date: %s", currentVersion)
}
return nil
}
// GetUpdateInfo returns the cached update check result.
func GetUpdateInfo() *UpdateInfo {
mu.RLock()
defer mu.RUnlock()
if cachedInfo == nil {
return &UpdateInfo{}
}
return cachedInfo
}
+2 -8
View File
@@ -21,7 +21,6 @@ import (
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/updatechecker"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
@@ -136,13 +135,8 @@ func prepareStartupProblemsAlert(ctx *context.Context) {
func Dashboard(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("admin.dashboard")
ctx.Data["PageIsAdminDashboard"] = true
// MokoGitea update checker
info := updatechecker.GetUpdateInfo()
ctx.Data["NeedUpdate"] = info.UpdateAvailable
ctx.Data["LatestVersion"] = info.LatestVersion
ctx.Data["ReleaseURL"] = info.ReleaseURL
// MokoGitea: upstream update checker removed — this is an independent fork
ctx.Data["NeedUpdate"] = false
updateSystemStatus()
ctx.Data["SysStatus"] = sysStatus
ctx.Data["SSH"] = setting.SSH
-14
View File
@@ -13,7 +13,6 @@ import (
"code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/updatechecker"
"code.gitea.io/gitea/services/auth"
"code.gitea.io/gitea/services/migrations"
mirror_service "code.gitea.io/gitea/services/mirror"
@@ -184,17 +183,4 @@ func initBasicTasks() {
registerCleanupPackages()
}
registerSyncRepoLicenses()
if setting.UpdateChecker.Enabled {
registerUpdateChecker()
}
}
func registerUpdateChecker() {
RegisterTaskFatal("update_checker", &BaseConfig{
Enabled: true,
RunAtStart: true,
Schedule: "@every 24h",
}, func(ctx context.Context, _ *user_model.User, _ Config) error {
return updatechecker.CheckForUpdate()
})
}
-7
View File
@@ -1,12 +1,5 @@
{{template "admin/layout_head" (dict "pageClass" "admin dashboard")}}
<div class="admin-setting-content">
{{if .NeedUpdate}}
<div class="ui positive message">
<div class="header">{{svg "octicon-info"}} MokoGitea Update Available</div>
<p>A new version <strong>{{.LatestVersion}}</strong> is available.
{{if .ReleaseURL}}<a href="{{.ReleaseURL}}" target="_blank" rel="noopener noreferrer">View release notes</a>{{end}}</p>
</div>
{{end}}
<h4 class="ui top attached header">
{{ctx.Locale.Tr "admin.dashboard.maintenance_operations"}}
</h4>