0
0
mirror of https://github.com/go-gitea/gitea.git synced 2025-07-13 11:24:43 +02:00

Merge f23be74c868f84607a9a1bce6332f1576cc78bf3 into 56eccb49954dbb561f4360481c3e52de92080f20

This commit is contained in:
TheFox0x7 2025-07-11 14:46:54 +02:00 committed by GitHub
commit ce032d9573
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 252 additions and 5 deletions

1
go.mod
View File

@ -219,6 +219,7 @@ require (
github.com/josharian/intern v1.0.0 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/libdns/libdns v1.0.0-beta.1 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/markbates/going v1.0.3 // indirect

View File

@ -11,6 +11,8 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"xorm.io/xorm/contexts"
)
@ -21,12 +23,22 @@ type EngineHook struct {
var _ contexts.Hook = (*EngineHook)(nil)
// follows: https://opentelemetry.io/docs/specs/semconv/database/database-metrics/#metric-dbclientoperationduration
var durationHistogram = promauto.NewHistogram(prometheus.HistogramOpts{
Namespace: "db",
Subsystem: "client",
Name: "operation_duration_seconds",
Help: "Duration of database client operations.",
// ConstLabels: prometheus.Labels{"db.system.name": BuilderDialect()}, //TODO: add type of database per spec.
})
func (*EngineHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) {
ctx, _ := gtprof.GetTracer().Start(c.Ctx, gtprof.TraceSpanDatabase)
return ctx, nil
}
func (h *EngineHook) AfterProcess(c *contexts.ContextHook) error {
durationHistogram.Observe(c.ExecuteTime.Seconds())
span := gtprof.GetContextSpan(c.Ctx)
if span != nil {
// Do not record SQL parameters here:

View File

@ -12,10 +12,39 @@ import (
"code.gitea.io/gitea/modules/setting"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
_ "gitea.com/go-chi/cache/memcache" //nolint:depguard // memcache plugin for cache, it is required for config "ADAPTER=memcache"
)
var defaultCache StringCache
var (
defaultCache StringCache
// TODO: Combine hit and miss into one
hitCounter = promauto.NewCounter(prometheus.CounterOpts{
Namespace: "gitea",
Help: "Cache count",
Subsystem: "cache",
Name: "response",
ConstLabels: prometheus.Labels{"state": "hit"},
})
missCounter = promauto.NewCounter(prometheus.CounterOpts{
Namespace: "gitea",
Help: "Cache count",
Subsystem: "cache",
Name: "response",
ConstLabels: prometheus.Labels{"state": "miss"},
})
latencyHistogram = promauto.NewHistogram(
prometheus.HistogramOpts{
Namespace: "gitea",
Help: "Cache latency",
Subsystem: "cache",
Name: "duration",
},
)
)
// Init start cache service
func Init() error {

View File

@ -6,6 +6,7 @@ package cache
import (
"errors"
"strings"
"time"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/setting"
@ -63,10 +64,15 @@ func (sc *stringCache) Ping() error {
}
func (sc *stringCache) Get(key string) (string, bool) {
start := time.Now()
v := sc.chiCache.Get(key)
elapsed := time.Since(start).Seconds()
latencyHistogram.Observe(elapsed)
if v == nil {
missCounter.Add(1)
return "", false
}
hitCounter.Add(1)
s, ok := v.(string)
return s, ok
}

View File

@ -22,6 +22,9 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/util"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
// TrustedCmdArgs returns the trusted arguments for git command.
@ -29,12 +32,29 @@ import (
// In most cases, it shouldn't be used. Use AddXxx function instead
type TrustedCmdArgs []internal.CmdArg
// const gitOperation = "command"
var (
// globalCommandArgs global command args for external package setting
globalCommandArgs TrustedCmdArgs
// defaultCommandExecutionTimeout default command execution timeout duration
defaultCommandExecutionTimeout = 360 * time.Second
reqInflightGauge = promauto.NewGauge(prometheus.GaugeOpts{
Namespace: "gitea",
Subsystem: "git",
Name: "active_commands",
Help: "Number of active git subprocesses.",
})
// reqDurationHistogram tracks the time taken by git call
reqDurationHistogram = promauto.NewHistogram(prometheus.HistogramOpts{
Namespace: "gitea",
Subsystem: "git",
Name: "command_duration_seconds", // diverge from spec to store the unit in metric.
Help: "Measures the time taken by git subprocesses",
Buckets: []float64{0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 2, 5, 10, 30, 60, 120, 300}, // based on dotnet buckets https://github.com/open-telemetry/semantic-conventions/issues/336
})
)
// DefaultLocale is the default LC_ALL to run git commands in.
@ -315,6 +335,10 @@ func (c *Command) run(ctx context.Context, skip int, opts *RunOpts) error {
desc := fmt.Sprintf("git.Run(by:%s, repo:%s): %s", callerInfo, logArgSanitize(opts.Dir), cmdLogString)
log.Debug("git.Command: %s", desc)
inflight := reqInflightGauge // add command type
inflight.Inc()
defer inflight.Dec()
_, span := gtprof.GetTracer().Start(ctx, gtprof.TraceSpanGitRun)
defer span.End()
span.SetAttributeString(gtprof.TraceAttrFuncCaller, callerInfo)
@ -364,6 +388,7 @@ func (c *Command) run(ctx context.Context, skip int, opts *RunOpts) error {
if elapsed > time.Second {
log.Debug("slow git.Command.Run: %s (%s)", c, elapsed)
}
reqDurationHistogram.Observe(elapsed.Seconds())
// We need to check if the context is canceled by the program on Windows.
// This is because Windows does not have signal checking when terminating the process.

View File

@ -8,6 +8,7 @@ import (
activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/system"
"code.gitea.io/gitea/modules/setting"
"github.com/prometheus/client_golang/prometheus"
@ -41,6 +42,7 @@ type Collector struct {
Releases *prometheus.Desc
Repositories *prometheus.Desc
Stars *prometheus.Desc
SystemNotices *prometheus.Desc
Teams *prometheus.Desc
UpdateTasks *prometheus.Desc
Users *prometheus.Desc
@ -89,7 +91,7 @@ func NewCollector() Collector {
Issues: prometheus.NewDesc(
namespace+"issues",
"Number of Issues",
nil, nil,
[]string{"state"}, nil,
),
IssuesByLabel: prometheus.NewDesc(
namespace+"issues_by_label",
@ -103,12 +105,12 @@ func NewCollector() Collector {
),
IssuesOpen: prometheus.NewDesc(
namespace+"issues_open",
"Number of open Issues",
"DEPRECATED: Use Issues with state: open",
nil, nil,
),
IssuesClosed: prometheus.NewDesc(
namespace+"issues_closed",
"Number of closed Issues",
"DEPRECATED: Use Issues with state: closed",
nil, nil,
),
Labels: prometheus.NewDesc(
@ -171,6 +173,10 @@ func NewCollector() Collector {
"Number of Stars",
nil, nil,
),
SystemNotices: prometheus.NewDesc(
namespace+"system_notices",
"Number of system notices",
nil, nil),
Teams: prometheus.NewDesc(
namespace+"teams",
"Number of Teams",
@ -234,6 +240,7 @@ func (c Collector) Describe(ch chan<- *prometheus.Desc) {
// Collect returns the metrics with values
func (c Collector) Collect(ch chan<- prometheus.Metric) {
stats := activities_model.GetStatistic(db.DefaultContext)
noticeCount := system.CountNotices(db.DefaultContext)
ch <- prometheus.MustNewConstMetric(
c.Accesses,
@ -272,8 +279,14 @@ func (c Collector) Collect(ch chan<- prometheus.Metric) {
ch <- prometheus.MustNewConstMetric(
c.Issues,
prometheus.GaugeValue,
float64(stats.Counter.Issue),
float64(stats.Counter.IssueOpen), "open",
)
ch <- prometheus.MustNewConstMetric(
c.Issues,
prometheus.GaugeValue,
float64(stats.Counter.IssueClosed), "closed",
)
for _, il := range stats.Counter.IssueByLabel {
ch <- prometheus.MustNewConstMetric(
c.IssuesByLabel,
@ -360,6 +373,11 @@ func (c Collector) Collect(ch chan<- prometheus.Metric) {
prometheus.GaugeValue,
float64(stats.Counter.Star),
)
ch <- prometheus.MustNewConstMetric(
c.SystemNotices,
prometheus.GaugeValue,
float64(noticeCount),
)
ch <- prometheus.MustNewConstMetric(
c.Teams,
prometheus.GaugeValue,

View File

@ -6,7 +6,9 @@ package common
import (
"fmt"
"net/http"
"strconv"
"strings"
"time"
"code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/gtprof"
@ -19,6 +21,52 @@ import (
"gitea.com/go-chi/session"
"github.com/chi-middleware/proxy"
"github.com/go-chi/chi/v5"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
const (
httpRequestMethod = "http_request_method"
httpResponseStatusCode = "http_response_status_code"
httpRoute = "http_route"
kb = 1000
mb = kb * kb
)
// reference: https://opentelemetry.io/docs/specs/semconv/http/http-metrics/#http-server
var (
sizeBuckets = []float64{1 * kb, 2 * kb, 5 * kb, 10 * kb, 100 * kb, 500 * kb, 1 * mb, 2 * mb, 5 * mb, 10 * mb}
// reqInflightGauge tracks the amount of currently handled requests
reqInflightGauge = promauto.NewGaugeVec(prometheus.GaugeOpts{
Namespace: "http",
Subsystem: "server",
Name: "active_requests",
Help: "Number of active HTTP server requests.",
}, []string{httpRequestMethod})
// reqDurationHistogram tracks the time taken by http request
reqDurationHistogram = promauto.NewHistogramVec(prometheus.HistogramOpts{
Namespace: "http",
Subsystem: "server",
Name: "request_duration_seconds", // diverge from spec to store the unit in metric.
Help: "Measures the latency of HTTP requests processed by the server",
Buckets: []float64{0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 2, 5, 10, 30, 60, 120, 300}, // based on dotnet buckets https://github.com/open-telemetry/semantic-conventions/issues/336
}, []string{httpRequestMethod, httpResponseStatusCode, httpRoute})
// reqSizeHistogram tracks the size of request
reqSizeHistogram = promauto.NewHistogramVec(prometheus.HistogramOpts{
Namespace: "http",
Subsystem: "server_request",
Name: "body_size",
Help: "Size of HTTP server request bodies.",
Buckets: sizeBuckets,
}, []string{httpRequestMethod, httpResponseStatusCode, httpRoute})
// respSizeHistogram tracks the size of the response
respSizeHistogram = promauto.NewHistogramVec(prometheus.HistogramOpts{
Namespace: "http",
Subsystem: "server_response",
Name: "body_size",
Help: "Size of HTTP server response bodies.",
Buckets: sizeBuckets,
}, []string{httpRequestMethod, httpResponseStatusCode, httpRoute})
)
// ProtocolMiddlewares returns HTTP protocol related middlewares, and it provides a global panic recovery
@ -38,6 +86,9 @@ func ProtocolMiddlewares() (handlers []any) {
if setting.IsAccessLogEnabled() {
handlers = append(handlers, context.AccessLogger())
}
if setting.Metrics.Enabled {
handlers = append(handlers, RouteMetrics())
}
return handlers
}
@ -107,6 +158,28 @@ func ForwardedHeadersHandler(limit int, trustedProxies []string) func(h http.Han
return proxy.ForwardedHeaders(opt)
}
// RouteMetrics instruments http requests and responses
func RouteMetrics() func(h http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
inflight := reqInflightGauge.WithLabelValues(req.Method)
inflight.Inc()
defer inflight.Dec()
start := time.Now()
next.ServeHTTP(resp, req)
m := context.WrapResponseWriter(resp)
route := chi.RouteContext(req.Context()).RoutePattern()
code := strconv.Itoa(m.WrittenStatus())
reqDurationHistogram.WithLabelValues(req.Method, code, route).Observe(time.Since(start).Seconds())
respSizeHistogram.WithLabelValues(req.Method, code, route).Observe(float64(m.WrittenSize()))
size := max(req.ContentLength, 0)
reqSizeHistogram.WithLabelValues(req.Method, code, route).Observe(float64(size))
})
}
}
func Sessioner() func(next http.Handler) http.Handler {
return session.Sessioner(session.Options{
Provider: setting.SessionConfig.Provider,

View File

@ -0,0 +1,41 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package common
import (
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/go-chi/chi/v5"
"github.com/prometheus/client_golang/prometheus/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMetricsMiddlewere(t *testing.T) {
middleware := RouteMetrics()
r := chi.NewRouter()
r.Use(middleware)
r.Get("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("test"))
time.Sleep(5 * time.Millisecond)
}))
testServer := httptest.NewServer(r)
// Check all defined metrics
verify := func(i int) {
assert.Equal(t, testutil.CollectAndCount(reqDurationHistogram, "http_server_request_duration_seconds"), i)
assert.Equal(t, testutil.CollectAndCount(reqSizeHistogram, "http_server_request_body_size"), i)
assert.Equal(t, testutil.CollectAndCount(respSizeHistogram, "http_server_response_body_size"), i)
assert.Equal(t, testutil.CollectAndCount(reqInflightGauge, "http_server_active_requests"), i)
}
// Check they don't exist before making a request
verify(0)
_, err := http.Get(testServer.URL)
require.NoError(t, err)
// Check they do exist after making the request
verify(1)
}

View File

@ -35,6 +35,8 @@ import (
user_service "code.gitea.io/gitea/services/user"
"github.com/markbates/goth"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
const (
@ -180,6 +182,8 @@ func prepareSignInPageData(ctx *context.Context) {
}
}
var loginCounter = promauto.NewCounterVec(prometheus.CounterOpts{Namespace: "gitea", Subsystem: "auth", Name: "login"}, []string{"status"}) // TODO: Add source/provider in the future?
// SignIn render sign in page
func SignIn(ctx *context.Context) {
if CheckAutoLogin(ctx) {
@ -217,6 +221,7 @@ func SignInPost(ctx *context.Context) {
u, source, err := auth_service.UserSignIn(ctx, form.UserName, form.Password)
if err != nil {
loginCounter.WithLabelValues("failure").Inc()
if errors.Is(err, util.ErrNotExist) || errors.Is(err, util.ErrInvalidArgument) {
ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tplSignIn, &form)
log.Warn("Failed authentication attempt for %s from %s: %v", form.UserName, ctx.RemoteAddr(), err)
@ -237,6 +242,7 @@ func SignInPost(ctx *context.Context) {
ctx.HTML(http.StatusOK, "user/auth/prohibit_login")
}
} else {
loginCounter.WithLabelValues("success").Inc()
ctx.ServerError("UserSignIn", err)
}
return

View File

@ -53,6 +53,7 @@ import (
"github.com/go-chi/cors"
"github.com/klauspost/compress/gzhttp"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/collectors"
)
var GzipMinSize = 1400 // min size to compress for the body size of response
@ -259,6 +260,7 @@ func Routes() *web.Router {
}
if setting.Metrics.Enabled {
prometheus.MustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{Namespace: "gitea"}))
prometheus.MustRegister(metrics.NewCollector())
routes.Get("/metrics", append(mid, Metrics)...)
}

View File

@ -19,9 +19,18 @@ import (
"code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/translation"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
var (
cronInflight = promauto.NewGauge(prometheus.GaugeOpts{
Namespace: "gitea",
Subsystem: "cron",
Name: "active_tasks",
Help: "Number of running cron tasks",
})
lock = sync.Mutex{}
started = false
tasks = []*Task{}
@ -86,6 +95,8 @@ func (t *Task) RunWithUser(doer *user_model.User, config Config) {
taskStatusTable.Stop(t.Name)
}()
graceful.GetManager().RunWithShutdownContext(func(baseCtx context.Context) {
cronInflight.Inc()
defer cronInflight.Dec()
defer func() {
if err := recover(); err != nil {
// Recover a panic within the execution of the task.

View File

@ -22,6 +22,14 @@ import (
base "code.gitea.io/gitea/modules/migration"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
var (
repoMigrationsInflightGauge = promauto.NewGauge(prometheus.GaugeOpts{Namespace: "gitea", Subsystem: "repository", Name: "inflight_migrations", Help: "Number of inflight repository migrations"})
repoMigrationsCounter = promauto.NewGaugeVec(prometheus.GaugeOpts{Namespace: "gitea", Subsystem: "repository", Name: "migrations", Help: "Total migrations"}, []string{"result"})
)
// MigrateOptions is equal to base.MigrateOptions
@ -123,6 +131,9 @@ func MigrateRepository(ctx context.Context, doer *user_model.User, ownerName str
return nil, err
}
repoMigrationsInflightGauge.Inc()
defer repoMigrationsInflightGauge.Dec()
uploader := NewGiteaLocalUploader(ctx, doer, ownerName, opts.RepoName)
uploader.gitServiceType = opts.GitServiceType
@ -133,8 +144,10 @@ func MigrateRepository(ctx context.Context, doer *user_model.User, ownerName str
if err2 := system_model.CreateRepositoryNotice(fmt.Sprintf("Migrate repository from %s failed: %v", opts.OriginalURL, err)); err2 != nil {
log.Error("create respotiry notice failed: ", err2)
}
repoMigrationsCounter.WithLabelValues("fail").Inc()
return nil, err
}
repoMigrationsCounter.WithLabelValues("success").Inc()
return uploader.repo, nil
}

View File

@ -32,6 +32,8 @@ import (
webhook_module "code.gitea.io/gitea/modules/webhook"
"github.com/gobwas/glob"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
func newDefaultRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (req *http.Request, body []byte, err error) {
@ -148,6 +150,8 @@ func addDefaultHeaders(req *http.Request, secret []byte, w *webhook_model.Webhoo
return nil
}
var webhookCounter = promauto.NewCounterVec(prometheus.CounterOpts{Namespace: "gitea", Subsystem: "webhook", Name: "deliveries", Help: "Number of webhook deliveries"}, []string{"success"})
// Deliver creates the [http.Request] (depending on the webhook type), sends it
// and records the status and response.
func Deliver(ctx context.Context, t *webhook_model.HookTask) error {
@ -265,6 +269,12 @@ func Deliver(ctx context.Context, t *webhook_model.HookTask) error {
t.ResponseInfo.Headers[k] = strings.Join(vals, ",")
}
if t.IsSucceed {
webhookCounter.WithLabelValues("success").Inc()
} else {
webhookCounter.WithLabelValues("failure").Inc()
}
p, err := io.ReadAll(resp.Body)
if err != nil {
t.ResponseInfo.Body = fmt.Sprintf("read body: %s", err)