Skip to content

Commit b748d62

Browse files
earl-warrenGustedGiteaBot6543
authored
Add slow SQL query warning (#27545)
- Databases are one of the most important parts of Forgejo, every interaction uses the database in one way or another. Therefore, it is important to maintain the database and recognize when the server is not doing well with the database. There already is the option to log *every* SQL query along with its execution time, but monitoring becomes impractical for larger instances and takes up unnecessary storage in the logs. - Add a QoL enhancement that allows instance administrators to specify a threshold value beyond which query execution time is logged as a warning in the xorm logger. The default value is a conservative five seconds to avoid this becoming a source of spam in the logs. - The use case for this patch is that with an instance the size of Codeberg, monitoring SQL logs is not very fruitful and most of them are uninteresting. Recently, in the context of persistent deadlock issues (https://codeberg.org/forgejo/forgejo/issues/220), I have noticed that certain queries hold locks on tables like comment and issue for several seconds. This patch helps to identify which queries these are and when they happen. - Added unit test. (cherry picked from commit 9cf501f1af4cd870221cef6af489618785b71186) --------- Co-authored-by: Gusted <postmaster@gusted.xyz> Co-authored-by: Giteabot <teabot@gitea.io> Co-authored-by: 6543 <6543@obermui.de>
1 parent 532e422 commit b748d62

File tree

4 files changed

+58
-20
lines changed

4 files changed

+58
-20
lines changed

custom/conf/app.example.ini

+4
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,10 @@ USER = root
412412
;;
413413
;; Whether execute database models migrations automatically
414414
;AUTO_MIGRATION = true
415+
;;
416+
;; Threshold value (in seconds) beyond which query execution time is logged as a warning in the xorm logger
417+
;;
418+
;SLOW_QUERY_THRESHOLD = 5s
415419

416420
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
417421
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

docs/content/administration/config-cheat-sheet.en-us.md

+1
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,7 @@ The following configuration set `Content-Type: application/vnd.android.package-a
458458
- `MAX_IDLE_CONNS` **2**: Max idle database connections on connection pool, default is 2 - this will be capped to `MAX_OPEN_CONNS`.
459459
- `CONN_MAX_LIFETIME` **0 or 3s**: Sets the maximum amount of time a DB connection may be reused - default is 0, meaning there is no limit (except on MySQL where it is 3s - see #6804 & #7071).
460460
- `AUTO_MIGRATION` **true**: Whether execute database models migrations automatically.
461+
- `SLOW_QUERY_THRESHOLD` **5s**: Threshold value in seconds beyond which query execution time is logged as a warning in the xorm logger.
461462

462463
[^1]: It may be necessary to specify a hostport even when listening on a unix socket, as the port is part of the socket name. see [#24552](https://github.com/go-gitea/gitea/issues/24552#issuecomment-1681649367) for additional details.
463464

models/db/engine.go

+31
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,13 @@ import (
1111
"io"
1212
"reflect"
1313
"strings"
14+
"time"
1415

16+
"code.gitea.io/gitea/modules/log"
1517
"code.gitea.io/gitea/modules/setting"
1618

1719
"xorm.io/xorm"
20+
"xorm.io/xorm/contexts"
1821
"xorm.io/xorm/names"
1922
"xorm.io/xorm/schemas"
2023

@@ -143,6 +146,13 @@ func InitEngine(ctx context.Context) error {
143146
xormEngine.SetConnMaxLifetime(setting.Database.ConnMaxLifetime)
144147
xormEngine.SetDefaultContext(ctx)
145148

149+
if setting.Database.SlowQueryThreshold > 0 {
150+
xormEngine.AddHook(&SlowQueryHook{
151+
Threshold: setting.Database.SlowQueryThreshold,
152+
Logger: log.GetLogger("xorm"),
153+
})
154+
}
155+
146156
SetDefaultEngine(ctx, xormEngine)
147157
return nil
148158
}
@@ -298,3 +308,24 @@ func SetLogSQL(ctx context.Context, on bool) {
298308
sess.Engine().ShowSQL(on)
299309
}
300310
}
311+
312+
type SlowQueryHook struct {
313+
Threshold time.Duration
314+
Logger log.Logger
315+
}
316+
317+
var _ contexts.Hook = &SlowQueryHook{}
318+
319+
func (SlowQueryHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) {
320+
return c.Ctx, nil
321+
}
322+
323+
func (h *SlowQueryHook) AfterProcess(c *contexts.ContextHook) error {
324+
if c.ExecuteTime >= h.Threshold {
325+
// 8 is the amount of skips passed to runtime.Caller, so that in the log the correct function
326+
// is being displayed (the function that ultimately wants to execute the query in the code)
327+
// instead of the function of the slow query hook being called.
328+
h.Logger.Log(8, log.WARN, "[Slow SQL Query] %s %v - %v", c.SQL, c.Args, c.ExecuteTime)
329+
}
330+
return nil
331+
}

modules/setting/database.go

+22-20
Original file line numberDiff line numberDiff line change
@@ -25,26 +25,27 @@ var (
2525

2626
// Database holds the database settings
2727
Database = struct {
28-
Type DatabaseType
29-
Host string
30-
Name string
31-
User string
32-
Passwd string
33-
Schema string
34-
SSLMode string
35-
Path string
36-
LogSQL bool
37-
MysqlCharset string
38-
CharsetCollation string
39-
Timeout int // seconds
40-
SQLiteJournalMode string
41-
DBConnectRetries int
42-
DBConnectBackoff time.Duration
43-
MaxIdleConns int
44-
MaxOpenConns int
45-
ConnMaxLifetime time.Duration
46-
IterateBufferSize int
47-
AutoMigration bool
28+
Type DatabaseType
29+
Host string
30+
Name string
31+
User string
32+
Passwd string
33+
Schema string
34+
SSLMode string
35+
Path string
36+
LogSQL bool
37+
MysqlCharset string
38+
CharsetCollation string
39+
Timeout int // seconds
40+
SQLiteJournalMode string
41+
DBConnectRetries int
42+
DBConnectBackoff time.Duration
43+
MaxIdleConns int
44+
MaxOpenConns int
45+
ConnMaxLifetime time.Duration
46+
IterateBufferSize int
47+
AutoMigration bool
48+
SlowQueryThreshold time.Duration
4849
}{
4950
Timeout: 500,
5051
IterateBufferSize: 50,
@@ -87,6 +88,7 @@ func loadDBSetting(rootCfg ConfigProvider) {
8788
Database.DBConnectRetries = sec.Key("DB_RETRIES").MustInt(10)
8889
Database.DBConnectBackoff = sec.Key("DB_RETRY_BACKOFF").MustDuration(3 * time.Second)
8990
Database.AutoMigration = sec.Key("AUTO_MIGRATION").MustBool(true)
91+
Database.SlowQueryThreshold = sec.Key("SLOW_QUERY_THRESHOLD").MustDuration(5 * time.Second)
9092
}
9193

9294
// DBConnStr returns database connection string

0 commit comments

Comments
 (0)