Rewrite logger system (#24726)

## ⚠️ Breaking

The `log.<mode>.<logger>` style config has been dropped. If you used it,
please check the new config manual & app.example.ini to make your
instance output logs as expected.

Although many legacy options still work, it's encouraged to upgrade to
the new options.

The SMTP logger is deleted because SMTP is not suitable to collect logs.

If you have manually configured Gitea log options, please confirm the
logger system works as expected after upgrading.

## Description

Close #12082 and maybe more log-related issues, resolve some related
FIXMEs in old code (which seems unfixable before)

Just like rewriting queue #24505 : make code maintainable, clear legacy
bugs, and add the ability to support more writers (eg: JSON, structured
log)

There is a new document (with examples): `logging-config.en-us.md`

This PR is safer than the queue rewriting, because it's just for
logging, it won't break other logic.

## The old problems

The logging system is quite old and difficult to maintain:
* Unclear concepts: Logger, NamedLogger, MultiChannelledLogger,
SubLogger, EventLogger, WriterLogger etc
* Some code is diffuclt to konw whether it is right:
`log.DelNamedLogger("console")` vs `log.DelNamedLogger(log.DEFAULT)` vs
`log.DelLogger("console")`
* The old system heavily depends on ini config system, it's difficult to
create new logger for different purpose, and it's very fragile.
* The "color" trick is difficult to use and read, many colors are
unnecessary, and in the future structured log could help
* It's difficult to add other log formats, eg: JSON format
* The log outputer doesn't have full control of its goroutine, it's
difficult to make outputer have advanced behaviors
* The logs could be lost in some cases: eg: no Fatal error when using
CLI.
* Config options are passed by JSON, which is quite fragile.
* INI package makes the KEY in `[log]` section visible in `[log.sub1]`
and `[log.sub1.subA]`, this behavior is quite fragile and would cause
more unclear problems, and there is no strong requirement to support
`log.<mode>.<logger>` syntax.


## The new design

See `logger.go` for documents.


## Screenshot

<details>


![image](https://github.com/go-gitea/gitea/assets/2114189/4462d713-ba39-41f5-bb08-de912e67e1ff)


![image](https://github.com/go-gitea/gitea/assets/2114189/b188035e-f691-428b-8b2d-ff7b2199b2f9)


![image](https://github.com/go-gitea/gitea/assets/2114189/132e9745-1c3b-4e00-9e0d-15eaea495dee)

</details>

## TODO

* [x] add some new tests
* [x] fix some tests
* [x] test some sub-commands (manually ....)

---------

Co-authored-by: Jason Song <i@wolfogre.com>
Co-authored-by: delvh <dev.lh@web.de>
Co-authored-by: Giteabot <teabot@gitea.io>
pull/24827/head^2
wxiaoguang 2 years ago committed by GitHub
parent 65dff8e364
commit 4647660776
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 17
      cmd/cmd.go
  2. 90
      cmd/doctor.go
  3. 5
      cmd/dump.go
  4. 8
      cmd/embedded.go
  5. 120
      cmd/manager_logging.go
  6. 5
      cmd/serv.go
  7. 10
      cmd/web.go
  8. 92
      custom/conf/app.example.ini
  9. 40
      docs/content/doc/administration/config-cheat-sheet.en-us.md
  10. 295
      docs/content/doc/administration/logging-config.en-us.md
  11. 524
      docs/content/doc/administration/logging-documentation.en-us.md
  12. 2
      main.go
  13. 50
      models/db/log.go
  14. 39
      models/issues/pull.go
  15. 6
      models/migrations/base/tests.go
  16. 16
      models/organization/team.go
  17. 33
      models/perm/access/repo_permission.go
  18. 9
      models/perm/access_mode.go
  19. 16
      models/repo/repo.go
  20. 7
      models/unit/unit.go
  21. 14
      models/user/user.go
  22. 5
      modules/context/access_log.go
  23. 74
      modules/doctor/doctor.go
  24. 3
      modules/git/git_test.go
  25. 2
      modules/graceful/manager.go
  26. 3
      modules/graceful/manager_unix.go
  27. 61
      modules/graceful/releasereopen/releasereopen.go
  28. 43
      modules/graceful/releasereopen/releasereopen_test.go
  29. 20
      modules/indexer/code/elastic_search.go
  30. 21
      modules/indexer/issues/elastic_search.go
  31. 12
      modules/lfs/pointer.go
  32. 115
      modules/log/color.go
  33. 14
      modules/log/color_console.go
  34. 0
      modules/log/color_console_other.go
  35. 2
      modules/log/color_console_windows.go
  36. 60
      modules/log/color_router.go
  37. 435
      modules/log/colors.go
  38. 137
      modules/log/conn.go
  39. 230
      modules/log/conn_test.go
  40. 93
      modules/log/console.go
  41. 137
      modules/log/console_test.go
  42. 61
      modules/log/errors.go
  43. 460
      modules/log/event.go
  44. 246
      modules/log/event_format.go
  45. 57
      modules/log/event_format_test.go
  46. 54
      modules/log/event_writer.go
  47. 160
      modules/log/event_writer_base.go
  48. 111
      modules/log/event_writer_conn.go
  49. 75
      modules/log/event_writer_conn_test.go
  50. 40
      modules/log/event_writer_console.go
  51. 48
      modules/log/event_writer_file.go
  52. 283
      modules/log/file.go
  53. 235
      modules/log/file_test.go
  54. 112
      modules/log/flags.go
  55. 30
      modules/log/flags_test.go
  56. 35
      modules/log/init.go
  57. 71
      modules/log/level.go
  58. 305
      modules/log/log.go
  59. 152
      modules/log/log_test.go
  60. 163
      modules/log/logger.go
  61. 83
      modules/log/logger_global.go
  62. 239
      modules/log/logger_impl.go
  63. 145
      modules/log/logger_test.go
  64. 142
      modules/log/manager.go
  65. 42
      modules/log/manager_test.go
  66. 78
      modules/log/misc.go
  67. 104
      modules/log/multichannel.go
  68. 25
      modules/log/provider.go
  69. 114
      modules/log/smtp.go
  70. 85
      modules/log/smtp_test.go
  71. 6
      modules/log/stack.go
  72. 269
      modules/log/writer.go
  73. 275
      modules/log/writer_test.go
  74. 14
      modules/private/manager.go
  75. 52
      modules/setting/config_provider.go
  76. 66
      modules/setting/config_provider_test.go
  77. 2
      modules/setting/database.go
  78. 481
      modules/setting/log.go
  79. 387
      modules/setting/log_test.go
  80. 1
      modules/setting/repository.go
  81. 1
      modules/setting/server.go
  82. 15
      modules/setting/setting.go
  83. 6
      modules/ssh/ssh.go
  84. 2
      modules/templates/htmlrenderer.go
  85. 67
      modules/test/logchecker.go
  86. 2
      modules/test/logchecker_test.go
  87. 98
      modules/testlogger/testlogger.go
  88. 246
      modules/util/rotatingfilewriter/writer.go
  89. 48
      modules/util/rotatingfilewriter/writer_test.go
  90. 32
      modules/web/routing/logger.go
  91. 9
      options/locale/locale_en-US.ini
  92. 4
      routers/common/middleware.go
  93. 2
      routers/private/internal.go
  94. 146
      routers/private/manager.go
  95. 7
      routers/web/admin/config.go
  96. 6
      routers/web/repo/issue.go
  97. 2
      routers/web/repo/issue_watch.go
  98. 8
      services/migrations/codebase.go
  99. 8
      services/migrations/gitbucket.go
  100. 8
      services/migrations/gitea_downloader.go
  101. Some files were not shown because too many files have changed in this diff Show More

@ -9,6 +9,7 @@ import (
"context"
"errors"
"fmt"
"io"
"os"
"os/signal"
"strings"
@ -59,7 +60,7 @@ func confirm() (bool, error) {
func initDB(ctx context.Context) error {
setting.Init(&setting.Options{})
setting.LoadDBSetting()
setting.InitSQLLog(false)
setting.InitSQLLoggersForCli(log.INFO)
if setting.Database.Type == "" {
log.Fatal(`Database settings are missing from the configuration file: %q.
@ -93,3 +94,17 @@ func installSignals() (context.Context, context.CancelFunc) {
return ctx, cancel
}
func setupConsoleLogger(level log.Level, colorize bool, out io.Writer) {
if out != os.Stdout && out != os.Stderr {
panic("setupConsoleLogger can only be used with os.Stdout or os.Stderr")
}
writeMode := log.WriterMode{
Level: level,
Colorize: colorize,
WriterOption: log.WriterConsoleOption{Stderr: out == os.Stderr},
}
writer := log.NewEventWriterConsole("console-default", writeMode)
log.GetManager().GetLogger(log.DEFAULT).RemoveAllWriters().AddWriters(writer)
}

@ -4,10 +4,10 @@
package cmd
import (
"errors"
"fmt"
golog "log"
"os"
"path/filepath"
"strings"
"text/tabwriter"
@ -82,23 +82,25 @@ You should back-up your database before doing this and ensure that your database
}
func runRecreateTable(ctx *cli.Context) error {
stdCtx, cancel := installSignals()
defer cancel()
// Redirect the default golog to here
golog.SetFlags(0)
golog.SetPrefix("")
golog.SetOutput(log.NewLoggerAsWriter("INFO", log.GetLogger(log.DEFAULT)))
golog.SetOutput(log.LoggerToWriter(log.GetLogger(log.DEFAULT).Info))
debug := ctx.Bool("debug")
setting.Init(&setting.Options{})
setting.LoadDBSetting()
setting.Log.EnableXORMLog = ctx.Bool("debug")
setting.Database.LogSQL = ctx.Bool("debug")
// FIXME: don't use CfgProvider directly
setting.CfgProvider.Section("log").Key("XORM").SetValue(",")
setting.InitSQLLog(!ctx.Bool("debug"))
stdCtx, cancel := installSignals()
defer cancel()
if debug {
setting.InitSQLLoggersForCli(log.DEBUG)
} else {
setting.InitSQLLoggersForCli(log.INFO)
}
setting.Database.LogSQL = debug
if err := db.InitEngine(stdCtx); err != nil {
fmt.Println(err)
fmt.Println("Check if you are using the right config file. You can use a --config directive to specify one.")
@ -125,44 +127,31 @@ func runRecreateTable(ctx *cli.Context) error {
})
}
func setDoctorLogger(ctx *cli.Context) {
func setupDoctorDefaultLogger(ctx *cli.Context, colorize bool) {
// Silence the default loggers
setupConsoleLogger(log.FATAL, log.CanColorStderr, os.Stderr)
logFile := ctx.String("log-file")
if !ctx.IsSet("log-file") {
logFile = "doctor.log"
}
colorize := log.CanColorStdout
if ctx.IsSet("color") {
colorize = ctx.Bool("color")
}
if len(logFile) == 0 {
log.NewLogger(1000, "doctor", "console", fmt.Sprintf(`{"level":"NONE","stacktracelevel":"NONE","colorize":%t}`, colorize))
return
}
defer func() {
recovered := recover()
if recovered == nil {
// if no doctor log-file is set, do not show any log from default logger
return
}
err, ok := recovered.(error)
if !ok {
panic(recovered)
}
if errors.Is(err, os.ErrPermission) {
fmt.Fprintf(os.Stderr, "ERROR: Unable to write logs to provided file due to permissions error: %s\n %v\n", logFile, err)
} else {
fmt.Fprintf(os.Stderr, "ERROR: Unable to write logs to provided file: %s\n %v\n", logFile, err)
}
fmt.Fprintf(os.Stderr, "WARN: Logging will be disabled\n Use `--log-file` to configure log file location\n")
log.NewLogger(1000, "doctor", "console", fmt.Sprintf(`{"level":"NONE","stacktracelevel":"NONE","colorize":%t}`, colorize))
}()
if logFile == "-" {
log.NewLogger(1000, "doctor", "console", fmt.Sprintf(`{"level":"trace","stacktracelevel":"NONE","colorize":%t}`, colorize))
setupConsoleLogger(log.TRACE, colorize, os.Stdout)
} else {
log.NewLogger(1000, "doctor", "file", fmt.Sprintf(`{"filename":%q,"level":"trace","stacktracelevel":"NONE"}`, logFile))
logFile, _ = filepath.Abs(logFile)
writeMode := log.WriterMode{Level: log.TRACE, WriterOption: log.WriterFileOption{FileName: logFile}}
writer, err := log.NewEventWriter("console-to-file", "file", writeMode)
if err != nil {
log.FallbackErrorf("unable to create file log writer: %v", err)
return
}
log.GetManager().GetLogger(log.DEFAULT).RemoveAllWriters().AddWriters(writer)
}
}
@ -170,22 +159,17 @@ func runDoctor(ctx *cli.Context) error {
stdCtx, cancel := installSignals()
defer cancel()
// Silence the default loggers
log.DelNamedLogger("console")
log.DelNamedLogger(log.DEFAULT)
// Now setup our own
setDoctorLogger(ctx)
colorize := log.CanColorStdout
if ctx.IsSet("color") {
colorize = ctx.Bool("color")
}
// Finally redirect the default golog to here
setupDoctorDefaultLogger(ctx, colorize)
// Finally redirect the default golang's log to here
golog.SetFlags(0)
golog.SetPrefix("")
golog.SetOutput(log.NewLoggerAsWriter("INFO", log.GetLogger(log.DEFAULT)))
golog.SetOutput(log.LoggerToWriter(log.GetLogger(log.DEFAULT).Info))
if ctx.IsSet("list") {
w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', 0)
@ -233,17 +217,5 @@ func runDoctor(ctx *cli.Context) error {
}
}
// Now we can set up our own logger to return information about what the doctor is doing
if err := log.NewNamedLogger("doctorouter",
0,
"console",
"console",
fmt.Sprintf(`{"level":"INFO","stacktracelevel":"NONE","colorize":%t,"flags":-1}`, colorize)); err != nil {
fmt.Println(err)
return err
}
logger := log.GetLogger("doctorouter")
defer logger.Close()
return doctor.RunChecks(stdCtx, logger, ctx.Bool("fix"), checks)
return doctor.RunChecks(stdCtx, colorize, ctx.Bool("fix"), checks)
}

@ -172,10 +172,7 @@ func runDump(ctx *cli.Context) error {
outType := ctx.String("type")
if fileName == "-" {
file = os.Stdout
err := log.DelLogger("console")
if err != nil {
fatal("Deleting default logger failed. Can not write to stdout: %v", err)
}
setupConsoleLogger(log.FATAL, log.CanColorStderr, os.Stderr)
} else {
for _, suffix := range outputTypeEnum.Enum {
if strings.HasSuffix(fileName, "."+suffix) {

@ -97,13 +97,7 @@ type assetFile struct {
}
func initEmbeddedExtractor(c *cli.Context) error {
// FIXME: there is a bug, if the user runs `gitea embedded` with a different user or root,
// The setting.Init (loadRunModeFrom) will fail and do log.Fatal
// But the console logger has been deleted, so nothing is printed, the user sees nothing and Gitea just exits.
// Silence the console logger
log.DelNamedLogger("console")
log.DelNamedLogger(log.DEFAULT)
setupConsoleLogger(log.ERROR, log.CanColorStderr, os.Stderr)
// Read configuration file
setting.Init(&setting.Options{

@ -16,13 +16,13 @@ import (
var (
defaultLoggingFlags = []cli.Flag{
cli.StringFlag{
Name: "group, g",
Usage: "Group to add logger to - will default to \"default\"",
Name: "logger",
Usage: `Logger name - will default to "default"`,
}, cli.StringFlag{
Name: "name, n",
Usage: "Name of the new logger - will default to mode",
Name: "writer",
Usage: "Name of the log writer - will default to mode",
}, cli.StringFlag{
Name: "level, l",
Name: "level",
Usage: "Logging level for the new logger",
}, cli.StringFlag{
Name: "stacktrace-level, L",
@ -83,8 +83,8 @@ var (
cli.BoolFlag{
Name: "debug",
}, cli.StringFlag{
Name: "group, g",
Usage: "Group to add logger to - will default to \"default\"",
Name: "logger",
Usage: `Logger name - will default to "default"`,
},
},
Action: runRemoveLogger,
@ -93,15 +93,6 @@ var (
Usage: "Add a logger",
Subcommands: []cli.Command{
{
Name: "console",
Usage: "Add a console logger",
Flags: append(defaultLoggingFlags,
cli.BoolFlag{
Name: "stderr",
Usage: "Output console logs to stderr - only relevant for console",
}),
Action: runAddConsoleLogger,
}, {
Name: "file",
Usage: "Add a file logger",
Flags: append(defaultLoggingFlags, []cli.Flag{
@ -148,28 +139,6 @@ var (
},
}...),
Action: runAddConnLogger,
}, {
Name: "smtp",
Usage: "Add an SMTP logger",
Flags: append(defaultLoggingFlags, []cli.Flag{
cli.StringFlag{
Name: "username, u",
Usage: "Mail server username",
}, cli.StringFlag{
Name: "password, P",
Usage: "Mail server password",
}, cli.StringFlag{
Name: "host, H",
Usage: "Mail server host (defaults to: 127.0.0.1:25)",
}, cli.StringSliceFlag{
Name: "send-to, s",
Usage: "Email address(es) to send to",
}, cli.StringFlag{
Name: "subject, S",
Usage: "Subject header of sent emails",
},
}...),
Action: runAddSMTPLogger,
},
},
}, {
@ -194,50 +163,16 @@ func runRemoveLogger(c *cli.Context) error {
defer cancel()
setup(ctx, c.Bool("debug"))
group := c.String("group")
if len(group) == 0 {
group = log.DEFAULT
logger := c.String("logger")
if len(logger) == 0 {
logger = log.DEFAULT
}
name := c.Args().First()
writer := c.Args().First()
extra := private.RemoveLogger(ctx, group, name)
extra := private.RemoveLogger(ctx, logger, writer)
return handleCliResponseExtra(extra)
}
func runAddSMTPLogger(c *cli.Context) error {
ctx, cancel := installSignals()
defer cancel()
setup(ctx, c.Bool("debug"))
vals := map[string]interface{}{}
mode := "smtp"
if c.IsSet("host") {
vals["host"] = c.String("host")
} else {
vals["host"] = "127.0.0.1:25"
}
if c.IsSet("username") {
vals["username"] = c.String("username")
}
if c.IsSet("password") {
vals["password"] = c.String("password")
}
if !c.IsSet("send-to") {
return fmt.Errorf("Some recipients must be provided")
}
vals["sendTos"] = c.StringSlice("send-to")
if c.IsSet("subject") {
vals["subject"] = c.String("subject")
} else {
vals["subject"] = "Diagnostic message from Gitea"
}
return commonAddLogger(c, mode, vals)
}
func runAddConnLogger(c *cli.Context) error {
ctx, cancel := installSignals()
defer cancel()
@ -301,25 +236,12 @@ func runAddFileLogger(c *cli.Context) error {
return commonAddLogger(c, mode, vals)
}
func runAddConsoleLogger(c *cli.Context) error {
ctx, cancel := installSignals()
defer cancel()
setup(ctx, c.Bool("debug"))
vals := map[string]interface{}{}
mode := "console"
if c.IsSet("stderr") && c.Bool("stderr") {
vals["stderr"] = c.Bool("stderr")
}
return commonAddLogger(c, mode, vals)
}
func commonAddLogger(c *cli.Context, mode string, vals map[string]interface{}) error {
if len(c.String("level")) > 0 {
vals["level"] = log.FromString(c.String("level")).String()
vals["level"] = log.LevelFromString(c.String("level")).String()
}
if len(c.String("stacktrace-level")) > 0 {
vals["stacktraceLevel"] = log.FromString(c.String("stacktrace-level")).String()
vals["stacktraceLevel"] = log.LevelFromString(c.String("stacktrace-level")).String()
}
if len(c.String("expression")) > 0 {
vals["expression"] = c.String("expression")
@ -333,18 +255,18 @@ func commonAddLogger(c *cli.Context, mode string, vals map[string]interface{}) e
if c.IsSet("color") {
vals["colorize"] = c.Bool("color")
}
group := "default"
if c.IsSet("group") {
group = c.String("group")
logger := log.DEFAULT
if c.IsSet("logger") {
logger = c.String("logger")
}
name := mode
if c.IsSet("name") {
name = c.String("name")
writer := mode
if c.IsSet("writer") {
writer = c.String("writer")
}
ctx, cancel := installSignals()
defer cancel()
extra := private.AddLogger(ctx, group, name, mode, vals)
extra := private.AddLogger(ctx, logger, writer, mode, vals)
return handleCliResponseExtra(extra)
}

@ -56,11 +56,10 @@ var CmdServ = cli.Command{
}
func setup(ctx context.Context, debug bool) {
_ = log.DelLogger("console")
if debug {
_ = log.NewLogger(1000, "console", "console", `{"level":"trace","stacktracelevel":"NONE","stderr":true}`)
setupConsoleLogger(log.TRACE, false, os.Stderr)
} else {
_ = log.NewLogger(1000, "console", "console", `{"level":"fatal","stacktracelevel":"NONE","stderr":true}`)
setupConsoleLogger(log.FATAL, false, os.Stderr)
}
setting.Init(&setting.Options{})
if debug {

@ -103,11 +103,9 @@ func createPIDFile(pidPath string) {
func runWeb(ctx *cli.Context) error {
if ctx.Bool("verbose") {
_ = log.DelLogger("console")
log.NewLogger(0, "console", "console", fmt.Sprintf(`{"level": "trace", "colorize": %t, "stacktraceLevel": "none"}`, log.CanColorStdout))
setupConsoleLogger(log.TRACE, log.CanColorStdout, os.Stdout)
} else if ctx.Bool("quiet") {
_ = log.DelLogger("console")
log.NewLogger(0, "console", "console", fmt.Sprintf(`{"level": "fatal", "colorize": %t, "stacktraceLevel": "none"}`, log.CanColorStdout))
setupConsoleLogger(log.FATAL, log.CanColorStdout, os.Stdout)
}
defer func() {
if panicked := recover(); panicked != nil {
@ -156,7 +154,7 @@ func runWeb(ctx *cli.Context) error {
case <-graceful.GetManager().IsShutdown():
<-graceful.GetManager().Done()
log.Info("PID: %d Gitea Web Finished", os.Getpid())
log.Close()
log.GetManager().Close()
return err
default:
}
@ -199,7 +197,7 @@ func runWeb(ctx *cli.Context) error {
err := listen(c, true)
<-graceful.GetManager().Done()
log.Info("PID: %d Gitea Web Finished", os.Getpid())
log.Close()
log.GetManager().Close()
return err
}

@ -230,7 +230,6 @@ RUN_MODE = ; prod
;;
;; Disable CDN even in "prod" mode
;OFFLINE_MODE = false
;DISABLE_ROUTER_LOG = false
;;
;; TLS Settings: Either ACME or manual
;; (Other common TLS configuration are found before)
@ -387,7 +386,7 @@ USER = root
;ITERATE_BUFFER_SIZE = 50
;;
;; Show the database generated SQL
LOG_SQL = false ; if unset defaults to true
;LOG_SQL = false
;;
;; Maximum number of DB Connect retries
;DB_RETRIES = 10
@ -550,34 +549,32 @@ ENABLE = true
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Main Logger
;;
;; Either "console", "file", "conn", "smtp" or "database", default is "console"
;; Either "console", "file" or "conn", default is "console"
;; Use comma to separate multiple modes, e.g. "console, file"
MODE = console
;;
;; Either "Trace", "Debug", "Info", "Warn", "Error", "Critical" or "None", default is "Info"
;; Either "Trace", "Debug", "Info", "Warn", "Error" or "None", default is "Info"
LEVEL = Info
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Router Logger
;;
;; Switch off the router log
;DISABLE_ROUTER_LOG=false
;; Print Stacktrace with logs (rarely helpful, do not set) Either "Trace", "Debug", "Info", "Warn", "Error", default is "None"
;STACKTRACE_LEVEL = None
;;
;; Set the log "modes" for the router log (if file is set the log file will default to router.log)
ROUTER = console
;; Buffer length of the channel, keep it as it is if you don't know what it is.
;BUFFER_LEN = 10000
;;
;; The router will log different things at different levels.
;; Sub logger modes, a single comma means use default MODE above, empty means disable it
;logger.access.MODE=
;logger.router.MODE=,
;logger.xorm.MODE=,
;;
;; * started messages will be logged at TRACE level
;; * polling/completed routers will be logged at INFO
;; * slow routers will be logged at WARN
;; * failed routers will be logged at WARN
;; Collect SSH logs (Creates log from ssh git request)
;;
;; The routing level will default to that of the system but individual router level can be set in
;; [log.<mode>.router] LEVEL
;ENABLE_SSH_LOG = false
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; Access Logger (Creates log in NCSA common log format)
;;
;; Print request id which parsed from request headers in access log, when access log is enabled.
;; * E.g:
;; * In request Header: X-Request-ID: test-id-123
@ -587,57 +584,32 @@ ROUTER = console
;; If you configure more than one in the .ini file, it will match in the order of configuration,
;; and the first match will be finally printed in the log.
;; * E.g:
;; * In reuqest Header: X-Trace-ID: trace-id-1q2w3e4r
;; * In request Header: X-Trace-ID: trace-id-1q2w3e4r
;; * Configuration in app.ini: REQUEST_ID_HEADERS = X-Request-ID, X-Trace-ID, X-Req-ID
;; * Print in log: 127.0.0.1:58384 - - [14/Feb/2023:16:33:51 +0800] "trace-id-1q2w3e4r"
;;
;; REQUEST_ID_HEADERS =
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; Access Logger (Creates log in NCSA common log format)
;;
;ENABLE_ACCESS_LOG = false
;;
;; Set the log "modes" for the access log (if file is set the log file will default to access.log)
;ACCESS = file
;REQUEST_ID_HEADERS =
;;
;; Sets the template used to create the access log.
;ACCESS_LOG_TEMPLATE = {{.Ctx.RemoteHost}} - {{.Identity}} {{.Start.Format "[02/Jan/2006:15:04:05 -0700]" }} "{{.Ctx.Req.Method}} {{.Ctx.Req.URL.RequestURI}} {{.Ctx.Req.Proto}}" {{.ResponseWriter.Status}} {{.ResponseWriter.Size}} "{{.Ctx.Req.Referer}}" "{{.Ctx.Req.UserAgent}}"
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; SSH log (Creates log from ssh git request)
;;
;ENABLE_SSH_LOG = false
;;
;; Other Settings
;;
;; Print Stacktraces with logs. (Rarely helpful.) Either "Trace", "Debug", "Info", "Warn", "Error", "Critical", default is "None"
;STACKTRACE_LEVEL = None
;;
;; Buffer length of the channel, keep it as it is if you don't know what it is.
;BUFFER_LEN = 10000
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Creating specific log configuration
;;
;; You can set specific configuration for individual modes and subloggers
;; Log modes (aka log writers)
;;
;; Configuration available to all log modes/subloggers
;[log.%(WriterMode)]
;MODE=console/file/conn/...
;LEVEL=
;FLAGS = stdflags
;EXPRESSION =
;PREFIX =
;COLORIZE = false
;;
;; For "console" mode only
;[log.console]
;STDERR = false
;;
;; For "file" mode only
;LEVEL =
;; Set the file_name for the logger. If this is a relative path this
;; will be relative to ROOT_PATH
;[log.file]
;; Set the file_name for the logger. If this is a relative path this will be relative to ROOT_PATH
;FILE_NAME =
;; This enables automated log rotate(switch of following options), default is true
;LOG_ROTATE = true
@ -651,9 +623,8 @@ ROUTER = console
;COMPRESS = true
;; compression level see godoc for compress/gzip
;COMPRESSION_LEVEL = -1
;
;; For "conn" mode only
;LEVEL =
;;
;[log.conn]
;; Reconnect host for every single message, default is false
;RECONNECT_ON_MSG = false
;; Try to reconnect when connection is lost, default is false
@ -662,19 +633,6 @@ ROUTER = console
;PROTOCOL = tcp
;; Host address
;ADDR =
;
;; For "smtp" mode only
;LEVEL =
;; Name displayed in mail title, default is "Diagnostic message from server"
;SUBJECT = Diagnostic message from server
;; Mail server
;HOST =
;; Mailer user name and password
;USER =
;; Use PASSWD = `your password` for quoting if you use special characters in the password.
;PASSWD =
;; Receivers, can be one or more, e.g. 1@example.com,2@example.com
;RECEIVERS =
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

@ -835,22 +835,16 @@ Default templates for project boards:
## Log (`log`)
- `ROOT_PATH`: **\<empty\>**: Root path for log files.
- `MODE`: **console**: Logging mode. For multiple modes, use a comma to separate values. You can configure each mode in per mode log subsections `\[log.modename\]`. By default the file mode will log to `$ROOT_PATH/gitea.log`.
- `MODE`: **console**: Logging mode. For multiple modes, use a comma to separate values. You can configure each mode in per mode log subsections `\[log.writer-mode-name\]`.
- `LEVEL`: **Info**: General log level. \[Trace, Debug, Info, Warn, Error, Critical, Fatal, None\]
- `STACKTRACE_LEVEL`: **None**: Default log level at which to log create stack traces. \[Trace, Debug, Info, Warn, Error, Critical, Fatal, None\]
- `STACKTRACE_LEVEL`: **None**: Default log level at which to log create stack traces (rarely useful, do not set it). \[Trace, Debug, Info, Warn, Error, Critical, Fatal, None\]
- `ENABLE_SSH_LOG`: **false**: save ssh log to log file
- `ENABLE_XORM_LOG`: **true**: Set whether to perform XORM logging. Please note SQL statement logging can be disabled by setting `LOG_SQL` to false in the `[database]` section.
### Router Log (`log`)
- `DISABLE_ROUTER_LOG`: **false**: Mute printing of the router log.
- `ROUTER`: **console**: The mode or name of the log the router should log to. (If you set this to `,` it will log to default Gitea logger.)
NB: You must have `DISABLE_ROUTER_LOG` set to `false` for this option to take effect. Configure each mode in per mode log subsections `\[log.modename.router\]`.
- `logger.access.MODE`: **\<empty\>**: The "access" logger
- `logger.router.MODE`: **,**: The "router" logger, a single comma means it will use the default MODE above
- `logger.xorm.MODE`: **,**: The "xorm" logger
### Access Log (`log`)
- `ENABLE_ACCESS_LOG`: **false**: Creates an access.log in NCSA common log format, or as per the following template
- `ACCESS`: **file**: Logging mode for the access logger, use a comma to separate values. Configure each mode in per mode log subsections `\[log.modename.access\]`. By default the file mode will log to `$ROOT_PATH/access.log`. (If you set this to `,` it will log to the default Gitea logger.)
- `ACCESS_LOG_TEMPLATE`: **`{{.Ctx.RemoteHost}} - {{.Identity}} {{.Start.Format "[02/Jan/2006:15:04:05 -0700]" }} "{{.Ctx.Req.Method}} {{.Ctx.Req.URL.RequestURI}} {{.Ctx.Req.Proto}}" {{.ResponseWriter.Status}} {{.ResponseWriter.Size}} "{{.Ctx.Req.Referer}}" "{{.Ctx.Req.UserAgent}}"`**: Sets the template used to create the access log.
- The following variables are available:
- `Ctx`: the `context.Context` of the request.
@ -858,31 +852,31 @@ Default templates for project boards:
- `Start`: the start time of the request.
- `ResponseWriter`: the responseWriter from the request.
- `RequestID`: the value matching REQUEST_ID_HEADERS(default: `-`, if not matched).
- You must be very careful to ensure that this template does not throw errors or panics as this template runs outside of the panic/recovery script.
- You must be very careful to ensure that this template does not throw errors or panics as this template runs outside the panic/recovery script.
- `REQUEST_ID_HEADERS`: **\<empty\>**: You can configure multiple values that are splited by comma here. It will match in the order of configuration, and the first match will be finally printed in the access log.
- e.g.
- In the Request Header: X-Request-ID: **test-id-123**
- Configuration in app.ini: REQUEST_ID_HEADERS = X-Request-ID
- Print in log: 127.0.0.1:58384 - - [14/Feb/2023:16:33:51 +0800] "**test-id-123**" ...
### Log subsections (`log.name`, `log.name.*`)
### Log subsections (`log.<writer-mode-name>`)
- `LEVEL`: **log.LEVEL**: Sets the log-level of this sublogger. Defaults to the `LEVEL` set in the global `[log]` section.
- `MODE`: **name**: Sets the mode of this log writer - Defaults to the provided subsection name. This allows you to have two different file loggers at different levels.
- `LEVEL`: **log.LEVEL**: Sets the log-level of this writer. Defaults to the `LEVEL` set in the global `[log]` section.
- `STACKTRACE_LEVEL`: **log.STACKTRACE_LEVEL**: Sets the log level at which to log stack traces.
- `MODE`: **name**: Sets the mode of this sublogger - Defaults to the provided subsection name. This allows you to have two different file loggers at different levels.
- `EXPRESSION`: **""**: A regular expression to match either the function name, file or message. Defaults to empty. Only log messages that match the expression will be saved in the logger.
- `FLAGS`: **stdflags**: A comma separated string representing the log flags. Defaults to `stdflags` which represents the prefix: `2009/01/23 01:23:23 ...a/b/c/d.go:23:runtime.Caller() [I]: message`. `none` means don't prefix log lines. See `modules/log/flags.go` for more information.
- `PREFIX`: **""**: An additional prefix for every log line in this logger. Defaults to empty.
- `COLORIZE`: **false**: Whether to colorize the log lines
### Console log mode (`log.console`, `log.console.*`, or `MODE=console`)
### Console log mode (`log.console`, or `MODE=console`)
- For the console logger `COLORIZE` will default to `true` if not on windows or the terminal is determined to be able to color.
- `STDERR`: **false**: Use Stderr instead of Stdout.
### File log mode (`log.file`, `log.file.*` or `MODE=file`)
### File log mode (`log.file`, or `MODE=file`)
- `FILE_NAME`: Set the file name for this logger. Defaults as described above. If relative will be relative to the `ROOT_PATH`
- `FILE_NAME`: Set the file name for this logger. Defaults to `gitea.log` (exception: access log defaults to `access.log`). If relative will be relative to the `ROOT_PATH`
- `LOG_ROTATE`: **true**: Rotate the log files.
- `MAX_SIZE_SHIFT`: **28**: Maximum size shift of a single file, 28 represents 256Mb.
- `DAILY_ROTATE`: **true**: Rotate logs daily.
@ -890,21 +884,13 @@ Default templates for project boards:
- `COMPRESS`: **true**: Compress old log files by default with gzip
- `COMPRESSION_LEVEL`: **-1**: Compression level
### Conn log mode (`log.conn`, `log.conn.*` or `MODE=conn`)
### Conn log mode (`log.conn`, or `MODE=conn`)
- `RECONNECT_ON_MSG`: **false**: Reconnect host for every single message.
- `RECONNECT`: **false**: Try to reconnect when connection is lost.
- `PROTOCOL`: **tcp**: Set the protocol, either "tcp", "unix" or "udp".
- `ADDR`: **:7020**: Sets the address to connect to.
### SMTP log mode (`log.smtp`, `log.smtp.*` or `MODE=smtp`)
- `USER`: User email address to send from.
- `PASSWD`: Password for the smtp server.
- `HOST`: **127.0.0.1:25**: The SMTP host to connect to.
- `RECEIVERS`: Email addresses to send to.
- `SUBJECT`: **Diagnostic message from Gitea**
## Cron (`cron`)
- `ENABLED`: **false**: Enable to run all cron tasks periodically with default settings.

@ -0,0 +1,295 @@
---
date: "2019-04-02T17:06:00+01:00"
title: "Logging Configuration"
slug: "logging-config"
weight: 40
toc: false
draft: false
aliases:
- /en-us/logging-configuration
menu:
sidebar:
parent: "administration"
name: "Logging Configuration"
weight: 40
identifier: "logging-config"
---
# Logging Configuration
The logging configuration of Gitea mainly consists of 3 types of components:
- The `[log]` section for general configuration
- `[log.<mode-name>]` sections for the configuration of different log writers to output logs, aka: "writer mode", the mode name is also used as "writer name".
- The `[log]` section can also contain sub-logger configurations following the key schema `logger.<logger-name>.<CONFIG-KEY>`
There is a fully functional log output by default, so it is not necessary to define one.
**Table of Contents**
{{< toc >}}
## Collecting Logs for Help
To collect logs for help and issue report, see [Support Options]({{< relref "doc/help/support.en-us.md" >}}).
## The `[log]` section
Configuration of logging facilities in Gitea happen in the `[log]` section and its subsections.
In the top level `[log]` section the following configurations can be placed:
- `ROOT_PATH`: (Default: **%(GITEA_WORK_DIR)/log**): Base path for log files
- `MODE`: (Default: **console**) List of log outputs to use for the Default logger.
- `LEVEL`: (Default: **Info**) Least severe log events to persist, case-insensitive. Possible values are: `Trace`, `Debug`, `Info`, `Warn`, `Error`, `Fatal`.
- `STACKTRACE_LEVEL`: (Default: **None**) For this and more severe events the stacktrace will be printed upon getting logged.
And it can contain the following sub-loggers:
- `logger.router.MODE`: (Default: **,**): List of log outputs to use for the Router logger.
- `logger.access.MODE`: (Default: **\<empty\>**) List of log outputs to use for the Access logger. By default, the access logger is disabled.
- `logger.xorm.MODE`: (Default: **,**) List of log outputs to use for the XORM logger.
Setting a comma (`,`) to sub-logger's mode means making it use the default global `MODE`.
## Quick samples
### Default (empty) Configuration
The empty configuration is equivalent to default:
```ini
[log]
ROOT_PATH = %(GITEA_WORK_DIR)/log
MODE = console
LEVEL = Info
STACKTRACE_LEVEL = None
logger.router.MODE = ,
logger.xorm.MODE = ,
logger.access.MODE =
; this is the config options of "console" mode (used by MODE=console above)
[log.console]
MODE = console
FLAGS = stdflags
PREFIX =
COLORIZE = true
```
This is equivalent to sending all logs to the console, with default Golang log being sent to the console log too.
This is only a sample, and it is the default, do not need to write it into your configuration file.
### Disable Router logs and record some access logs to file
The Router logger is disabled, the access logs (>=Warn) goes into `access.log`:
```ini
[log]
logger.router.MODE =
logger.access.MODE = access-file
[log.access-file]
MODE = file
LEVEL = Warn
FILE_NAME = access.log
```
### Set different log levels for different modes
Default logs (>=Warn) goes into `gitea.log`, while Error logs goes into `file-error.log`:
```ini
[log]
LEVEL = Warn
MODE = file, file-error
; by default, the "file" mode will record logs to %(log.ROOT_PATH)/gitea.log, so we don't need to set it
; [log.file]
[log.file-error]
LEVEL = Error
FILE_NAME = file-error.log
```
## Log outputs (mode and writer)
Gitea provides the following log output writers:
- `console` - Log to `stdout` (or `stderr` if it is set in the config)
- `file` - Log to a file
- `conn` - Log to a socket (network or unix)
### Common configuration
Certain configuration is common to all modes of log output:
- `MODE` is the mode of the log output writer. It will default to the mode name in the ini section. Thus `[log.console]` will default to `MODE = console`.
- `LEVEL` is the lowest level that this output will log.
- `STACKTRACE_LEVEL` is the lowest level that this output will print a stacktrace.
- `COLORIZE` will default to `true` for `console` as described, otherwise it will default to `false`.
#### `EXPRESSION`
`EXPRESSION` represents a regular expression that log events must match to be logged by the output writer.
Either the log message, (with colors removed), must match or the `longfilename:linenumber:functionname` must match.
NB: the whole message or string doesn't need to completely match.
Please note this expression will be run in the writer's goroutine but not the logging event goroutine.
#### `FLAGS`
`FLAGS` represents the preceding logging context information that is
printed before each message. It is a comma-separated string set. The order of values does not matter.
It defaults to `stdflags` (= `date,time,medfile,shortfuncname,levelinitial`)
Possible values are:
- `none` or `,` - No flags.
- `date` - the date in the local time zone: `2009/01/23`.
- `time` - the time in the local time zone: `01:23:23`.
- `microseconds` - microsecond resolution: `01:23:23.123123`. Assumes time.
- `longfile` - full file name and line number: `/a/b/c/d.go:23`.
- `shortfile` - final file name element and line number: `d.go:23`.
- `funcname` - function name of the caller: `runtime.Caller()`.
- `shortfuncname` - last part of the function name. Overrides `funcname`.
- `utc` - if date or time is set, use UTC rather than the local time zone.
- `levelinitial` - initial character of the provided level in brackets eg. `[I]` for info.
- `level` - level in brackets `[INFO]`.
- `gopid` - the Goroutine-PID of the context.
- `medfile` - last 20 characters of the filename - equivalent to `shortfile,longfile`.
- `stdflags` - equivalent to `date,time,medfile,shortfuncname,levelinitial`.
### Console mode
In this mode the logger will forward log messages to the stdout and
stderr streams attached to the Gitea process.
For loggers in console mode, `COLORIZE` will default to `true` if not
on windows, or the Windows terminal can be set into ANSI mode or is a
cygwin or Msys pipe.
Settings:
- `STDERR`: **false**: Whether the logger should print to `stderr` instead of `stdout`.
### File mode
In this mode the logger will save log messages to a file.
Settings:
- `FILE_NAME`: The file to write the log events to, relative to `ROOT_PATH`, Default to `%(ROOT_PATH)/gitea.log`. Exception: access log will default to `%(ROOT_PATH)/access.log`.
- `MAX_SIZE_SHIFT`: **28**: Maximum size shift of a single file. 28 represents 256Mb. For details see below.
- `LOG_ROTATE` **true**: Whether to rotate the log files. TODO: if false, will it delete instead on daily rotate, or do nothing?.
- `DAILY_ROTATE`: **true**: Whether to rotate logs daily.
- `MAX_DAYS`: **7**: Delete rotated log files after this number of days.
- `COMPRESS`: **true**: Whether to compress old log files by default with gzip.
- `COMPRESSION_LEVEL`: **-1**: Compression level. For details see below.
`MAX_SIZE_SHIFT` defines the maximum size of a file by left shifting 1 the given number of times (`1 << x`).
The exact behavior at the time of v1.17.3 can be seen [here](https://github.com/go-gitea/gitea/blob/v1.17.3/modules/setting/log.go#L185).
The useful values of `COMPRESSION_LEVEL` are from 1 to (and including) 9, where higher numbers mean better compression.
Beware that better compression might come with higher resource usage.
Must be preceded with a `-` sign.
### Conn mode
In this mode the logger will send log messages over a network socket.
Settings:
- `ADDR`: **:7020**: Sets the address to connect to.
- `PROTOCOL`: **tcp**: Set the protocol, either "tcp", "unix" or "udp".
- `RECONNECT`: **false**: Try to reconnect when connection is lost.
- `RECONNECT_ON_MSG`: **false**: Reconnect host for every single message.
### The "Router" logger
The Router logger logs the following message types when Gitea's route handlers work:
- `started` messages will be logged at TRACE level
- `polling`/`completed` routers will be logged at INFO. Exception: "/assets" static resource requests are also logged at TRACE.
- `slow` routers will be logged at WARN
- `failed` routers will be logged at WARN
### The "XORM" logger
To make XORM outputs SQL logs, the `LOG_SQL` in `[database]` section should also be set to `true`.
### The "Access" logger
The Access logger is a new logger since Gitea 1.9. It provides a NCSA
Common Log compliant log format. It's highly configurable but caution
should be taken when changing its template. The main benefit of this
logger is that Gitea can now log accesses in a standard log format so
standard tools may be used.
You can enable this logger using `logger.access.MODE = ...`.
If desired the format of the Access logger can be changed by changing
the value of the `ACCESS_LOG_TEMPLATE`.
Please note, the access logger will log at `INFO` level, setting the
`LEVEL` of this logger to `WARN` or above will result in no access logs.
#### The ACCESS_LOG_TEMPLATE
This value represents a go template. Its default value is
```tmpl
{{.Ctx.RemoteHost}} - {{.Identity}} {{.Start.Format "[02/Jan/2006:15:04:05 -0700]" }} "{{.Ctx.Req.Method}} {{.Ctx.Req.URL.RequestURI}} {{.Ctx.Req.Proto}}" {{.ResponseWriter.Status}} {{.ResponseWriter.Size}} "{{.Ctx.Req.Referer}}" "{{.Ctx.Req.UserAgent}}"`
```
The template is passed following options:
- `Ctx` is the `context.Context`
- `Identity` is the `SignedUserName` or `"-"` if the user is not logged in
- `Start` is the start time of the request
- `ResponseWriter` is the `http.ResponseWriter`
Caution must be taken when changing this template as it runs outside of
the standard panic recovery trap. The template should also be as simple
as it runs for every request.
## Releasing-and-Reopening, Pausing and Resuming logging
If you are running on Unix you may wish to release-and-reopen logs in order to use `logrotate` or other tools.
It is possible force Gitea to release and reopen it's logging files and connections by sending `SIGUSR1` to the
running process, or running `gitea manager logging release-and-reopen`.
Alternatively, you may wish to pause and resume logging - this can be accomplished through the use of the
`gitea manager logging pause` and `gitea manager logging resume` commands. Please note that whilst logging
is paused log events below INFO level will not be stored and only a limited number of events will be stored.
Logging may block, albeit temporarily, slowing Gitea considerably whilst paused - therefore it is
recommended that pausing only done for a very short period of time.
## Adding and removing logging whilst Gitea is running
It is possible to add and remove logging whilst Gitea is running using the `gitea manager logging add` and `remove` subcommands.
This functionality can only adjust running log systems and cannot be used to start the access or router loggers if they
were not already initialized. If you wish to start these systems you are advised to adjust the app.ini and (gracefully) restart
the Gitea service.
The main intention of these commands is to easily add a temporary logger to investigate problems on running systems where a restart
may cause the issue to disappear.
## Using `logrotate` instead of built-in log rotation
Gitea includes built-in log rotation, which should be enough for most deployments. However, if you instead want to use the `logrotate` utility:
- Disable built-in log rotation by setting `LOG_ROTATE` to `false` in your `app.ini`.
- Install `logrotate`.
- Configure `logrotate` to match your deployment requirements, see `man 8 logrotate` for configuration syntax details.
In the `postrotate/endscript` block send Gitea a `USR1` signal via `kill -USR1` or `kill -10` to the `gitea` process itself,
or run `gitea manager logging release-and-reopen` (with the appropriate environment).
Ensure that your configurations apply to all files emitted by Gitea loggers as described in the above sections.
- Always do `logrotate /etc/logrotate.conf --debug` to test your configurations.
- If you are using docker and are running from outside the container you can use
`docker exec -u $OS_USER $CONTAINER_NAME sh -c 'gitea manager logging release-and-reopen'`
or `docker exec $CONTAINER_NAME sh -c '/bin/s6-svc -1 /etc/s6/gitea/'` or send `USR1` directly to the Gitea process itself.
The next `logrotate` jobs will include your configurations, so no restart is needed.
You can also immediately reload `logrotate` with `logrotate /etc/logrotate.conf --force`.

@ -1,524 +0,0 @@
---
date: "2019-04-02T17:06:00+01:00"
title: "Logging Configuration"
slug: "logging-configuration"
weight: 40
toc: false
draft: false
aliases:
- /en-us/logging-configuration
menu:
sidebar:
parent: "administration"
name: "Logging Configuration"
weight: 40
identifier: "logging-configuration"
---
# Logging Configuration
The logging configuration of Gitea mainly consists of 3 types of components:
- The `[log]` section for general configuration
- `[log.<sublogger>]` sections for the configuration of different log outputs
- `[log.<sublogger>.<group>]` sections for output specific configuration of a log group
As mentioned below, there is a fully functional log output by default, so it is not necessary to define one.
**Table of Contents**
{{< toc >}}
## Collecting Logs for Help
To collect logs for help and issue report, see [Support Options]({{< relref "doc/help/support.en-us.md" >}}).
## The `[log]` section
Configuration of logging facilities in Gitea happen in the `[log]` section and it's subsections.
In the top level `[log]` section the following configurations can be placed:
- `ROOT_PATH`: (Default: **%(GITEA_WORK_DIR)/log**): Base path for log files
- `MODE`: (Default: **console**) List of log outputs to use for the Default logger.
- `ROUTER`: (Default: **console**): List of log outputs to use for the Router logger.
- `ACCESS`: List of log outputs to use for the Access logger.
- `XORM`: (Default: **,**) List of log outputs to use for the XORM logger.
- `ENABLE_ACCESS_LOG`: (Default: **false**): whether the Access logger is allowed to emit logs
- `ENABLE_XORM_LOG`: (Default: **true**): whether the XORM logger is allowed to emit logs
For details on the loggers check the "Log Groups" section.
Important: log outputs won't be used if you don't enable them for the desired loggers in the corresponding list value.
Lists are specified as comma separated values. This format also works in subsection.
This section may be used for defining default values for subsections.
Examples:
- `LEVEL`: (Default: **Info**) Least severe log events to persist. Case insensitive. The full list of levels as of v1.17.3 can be read [here](https://github.com/go-gitea/gitea/blob/v1.17.3/custom/conf/app.example.ini#L507).
- `STACKTRACE_LEVEL`: (Default: **None**) For this and more severe events the stacktrace will be printed upon getting logged.
Some values are not inherited by subsections. For details see the "Non-inherited default values" section.
## Log outputs
Log outputs are the targets to which log messages will be sent.
The content and the format of the log messages to be saved can be configured in these.
Log outputs are also called subloggers.
Gitea provides 4 possible log outputs:
- `console` - Log to `os.Stdout` or `os.Stderr`
- `file` - Log to a file
- `conn` - Log to a socket (network or unix)
- `smtp` - Log via email
By default, Gitea has a `console` output configured, which is used by the loggers as seen in the section "The log section" above.
### Common configuration
Certain configuration is common to all modes of log output:
- `MODE` is the mode of the log output. It will default to the sublogger
name, thus `[log.console.router]` will default to `MODE = console`.
For mode specific confgurations read further.
- `LEVEL` is the lowest level that this output will log. This value
is inherited from `[log]` and in the case of the non-default loggers
from `[log.sublogger]`.
- `STACKTRACE_LEVEL` is the lowest level that this output will print
a stacktrace. This value is inherited.
- `COLORIZE` will default to `true` for `console` as
described, otherwise it will default to `false`.
### Non-inherited default values
There are several values which are not inherited as described above but
rather default to those specific to type of logger, these are:
`EXPRESSION`, `FLAGS`, `PREFIX` and `FILE_NAME`.
#### `EXPRESSION`
`EXPRESSION` represents a regular expression that log events must match to be logged by the sublogger. Either the log message, (with colors removed), must match or the `longfilename:linenumber:functionname` must match. NB: the whole message or string doesn't need to completely match.
Please note this expression will be run in the sublogger's goroutine
not the logging event subroutine. Therefore it can be complicated.
#### `FLAGS`
`FLAGS` represents the preceding logging context information that is
printed before each message. It is a comma-separated string set. The order of values does not matter.
Possible values are:
- `none` or `,` - No flags.
- `date` - the date in the local time zone: `2009/01/23`.
- `time` - the time in the local time zone: `01:23:23`.
- `microseconds` - microsecond resolution: `01:23:23.123123`. Assumes
time.
- `longfile` - full file name and line number: `/a/b/c/d.go:23`.
- `shortfile` - final file name element and line number: `d.go:23`.
- `funcname` - function name of the caller: `runtime.Caller()`.
- `shortfuncname` - last part of the function name. Overrides
`funcname`.
- `utc` - if date or time is set, use UTC rather than the local time
zone.
- `levelinitial` - Initial character of the provided level in brackets eg. `[I]` for info.
- `level` - Provided level in brackets `[INFO]`
- `medfile` - Last 20 characters of the filename - equivalent to
`shortfile,longfile`.
- `stdflags` - Equivalent to `date,time,medfile,shortfuncname,levelinitial`
### Console mode
In this mode the logger will forward log messages to the stdout and
stderr streams attached to the Gitea process.
For loggers in console mode, `COLORIZE` will default to `true` if not
on windows, or the windows terminal can be set into ANSI mode or is a
cygwin or Msys pipe.
Settings:
- `STDERR`: **false**: Whether the logger should print to `stderr` instead of `stdout`.
### File mode
In this mode the logger will save log messages to a file.
Settings:
- `FILE_NAME`: The file to write the log events to. For details see below.
- `MAX_SIZE_SHIFT`: **28**: Maximum size shift of a single file. 28 represents 256Mb. For details see below.
- `LOG_ROTATE` **true**: Whether to rotate the log files. TODO: if false, will it delete instead on daily rotate, or do nothing?.
- `DAILY_ROTATE`: **true**: Whether to rotate logs daily.
- `MAX_DAYS`: **7**: Delete rotated log files after this number of days.
- `COMPRESS`: **true**: Whether to compress old log files by default with gzip.
- `COMPRESSION_LEVEL`: **-1**: Compression level. For details see below.
The default value of `FILE_NAME` depends on the respective logger facility.
If unset, their own default will be used.
If set it will be relative to the provided `ROOT_PATH` in the master `[log]` section.
`MAX_SIZE_SHIFT` defines the maximum size of a file by left shifting 1 the given number of times (`1 << x`).
The exact behavior at the time of v1.17.3 can be seen [here](https://github.com/go-gitea/gitea/blob/v1.17.3/modules/setting/log.go#L185).
The useful values of `COMPRESSION_LEVEL` are from 1 to (and including) 9, where higher numbers mean better compression.
Beware that better compression might come with higher resource usage.
Must be preceded with a `-` sign.
### Conn mode
In this mode the logger will send log messages over a network socket.
Settings:
- `ADDR`: **:7020**: Sets the address to connect to.
- `PROTOCOL`: **tcp**: Set the protocol, either "tcp", "unix" or "udp".
- `RECONNECT`: **false**: Try to reconnect when connection is lost.
- `RECONNECT_ON_MSG`: **false**: Reconnect host for every single message.
### SMTP mode
In this mode the logger will send log messages in email.
It is not recommended to use this logger to send general logging
messages. However, you could perhaps set this logger to work on `FATAL` messages only.
Settings:
- `HOST`: **127.0.0.1:25**: The SMTP host to connect to.
- `USER`: User email address to send from.
- `PASSWD`: Password for the smtp server.
- `RECEIVERS`: Email addresses to send to.
- `SUBJECT`: **Diagnostic message from Gitea**. The content of the email's subject field.
## Log Groups
The fundamental thing to be aware of in Gitea is that there are several
log groups:
- The "Default" logger
- The Router logger
- The Access logger
- The XORM logger
There is also the go log logger.
### The go log logger
Go provides its own extremely basic logger in the `log` package,
however, this is not sufficient for our purposes as it does not provide
a way of logging at multiple levels, nor does it provide a good way of
controlling where these logs are logged except through setting of a
writer.
We have therefore redirected this logger to our Default logger, and we
will log anything that is logged using the go logger at the INFO level.
### The "Default" logger
Calls to `log.Info`, `log.Debug`, `log.Error` etc. from the `code.gitea.io/gitea/modules/log` package will log to this logger.
You can configure the outputs of this logger by setting the `MODE`
value in the `[log]` section of the configuration.
Each output sublogger is configured in a separate `[log.sublogger.default]`
which inherits from the sublogger `[log.sublogger]` section and from the
generic `[log]` section, but there are certain default values. These will
not be inherited from the `[log]` section:
- `FLAGS` is `stdflags` (Equal to
`date,time,medfile,shortfuncname,levelinitial`)
- `FILE_NAME` will default to `%(ROOT_PATH)/gitea.log`
- `EXPRESSION` will default to `""`
- `PREFIX` will default to `""`
The provider type of the sublogger can be set using the `MODE` value in
its subsection, but will default to the name. This allows you to have
multiple subloggers that will log to files.
### The "Router" logger
The Router logger has been substantially changed in v1.17. If you are using the router logger for fail2ban or other monitoring
you will need to update this configuration.
You can disable Router log by setting `DISABLE_ROUTER_LOG` or by setting all of its sublogger configurations to `none`.
You can configure the outputs of this
router log by setting the `ROUTER` value in the `[log]` section of the
configuration. `ROUTER` will default to `console` if unset and will default to same level as main logger.
The Router logger logs the following:
- `started` messages will be logged at TRACE level
- `polling`/`completed` routers will be logged at INFO
- `slow` routers will be logged at WARN
- `failed` routers will be logged at WARN
The logging level for the router will default to that of the main configuration. Set `[log.<mode>.router]` `LEVEL` to change this.
Each output sublogger for this logger is configured in
`[log.sublogger.router]` sections. There are certain default values
which will not be inherited from the `[log]` or relevant
`[log.sublogger]` sections:
- `FILE_NAME` will default to `%(ROOT_PATH)/router.log`
- `FLAGS` defaults to `date,time`
- `EXPRESSION` will default to `""`
- `PREFIX` will default to `""`
NB: You can redirect the router logger to send its events to the Gitea
log using the value: `ROUTER = ,`
### The "Access" logger
The Access logger is a new logger for version 1.9. It provides a NCSA
Common Log compliant log format. It's highly configurable but caution
should be taken when changing its template. The main benefit of this
logger is that Gitea can now log accesses in a standard log format so
standard tools may be used.
You can enable this logger using `ENABLE_ACCESS_LOG`. Its outputs are
configured by setting the `ACCESS` value in the `[log]` section of the
configuration. `ACCESS` defaults to `file` if unset.
Each output sublogger for this logger is configured in
`[log.sublogger.access]` sections. There are certain default values
which will not be inherited from the `[log]` or relevant
`[log.sublogger]` sections:
- `FILE_NAME` will default to `%(ROOT_PATH)/access.log`
- `FLAGS` defaults to `` or None
- `EXPRESSION` will default to `""`
- `PREFIX` will default to `""`
If desired the format of the Access logger can be changed by changing
the value of the `ACCESS_LOG_TEMPLATE`.
Please note, the access logger will log at `INFO` level, setting the
`LEVEL` of this logger to `WARN` or above will result in no access logs.
NB: You can redirect the access logger to send its events to the Gitea
log using the value: `ACCESS = ,`
#### The ACCESS_LOG_TEMPLATE
This value represent a go template. It's default value is:
`{{.Ctx.RemoteHost}} - {{.Identity}} {{.Start.Format "[02/Jan/2006:15:04:05 -0700]" }} "{{.Ctx.Req.Method}} {{.Ctx.Req.URL.RequestURI}} {{.Ctx.Req.Proto}}" {{.ResponseWriter.Status}} {{.ResponseWriter.Size}} "{{.Ctx.Req.Referer}}" "{{.Ctx.Req.UserAgent}}"`
The template is passed following options:
- `Ctx` is the `context.Context`
- `Identity` is the `SignedUserName` or `"-"` if the user is not logged
in
- `Start` is the start time of the request
- `ResponseWriter` is the `http.ResponseWriter`
Caution must be taken when changing this template as it runs outside of
the standard panic recovery trap. The template should also be as simple
as it runs for every request.
### The "XORM" logger
The XORM logger is a long-standing logger that exists to collect XORM
log events. It is enabled by default but can be switched off by setting
`ENABLE_XORM_LOG` to `false` in the `[log]` section. Its outputs are
configured by setting the `XORM` value in the `[log]` section of the
configuration. `XORM` defaults to `,` if unset, meaning it is redirected
to the main Gitea log.
XORM will log SQL events by default. This can be changed by setting
the `LOG_SQL` value to `false` in the `[database]` section.
Each output sublogger for this logger is configured in
`[log.sublogger.xorm]` sections. There are certain default values
which will not be inherited from the `[log]` or relevant
`[log.sublogger]` sections:
- `FILE_NAME` will default to `%(ROOT_PATH)/xorm.log`
- `FLAGS` defaults to `date,time`
- `EXPRESSION` will default to `""`
- `PREFIX` will default to `""`
## Debugging problems
When submitting logs in Gitea issues it is often helpful to submit
merged logs obtained by either by redirecting the console log to a file or
copying and pasting it. To that end it is recommended to set your logging to:
```ini
[database]
LOG_SQL = false ; SQL logs are rarely helpful unless we specifically ask for them
...
[log]
MODE = console
LEVEL = debug ; please set the level to debug when we are debugging a problem
ROUTER = console
COLORIZE = false ; this can be true if you can strip out the ansi coloring
ENABLE_SSH_LOG = true ; shows logs related to git over SSH.
```
Sometimes it will be helpful get some specific `TRACE` level logging restricted
to messages that match a specific `EXPRESSION`. Adjusting the `MODE` in the
`[log]` section to `MODE = console,traceconsole` to add a new logger output
`traceconsole` and then adding its corresponding section would be helpful:
```ini
[log.traceconsole] ; traceconsole here is just a name
MODE = console ; this is the output that the traceconsole writes to
LEVEL = trace
EXPRESSION = ; putting a string here will restrict this logger to logging only those messages that match this expression
```
(It's worth noting that log messages that match the expression at or above debug
level will get logged twice so don't worry about that.)
`STACKTRACE_LEVEL` should generally be left unconfigured (and hence kept at
`none`). There are only very specific occasions when it useful.
## Empty Configuration
The empty configuration is equivalent to:
```ini
[log]
ROOT_PATH = %(GITEA_WORK_DIR)/log
MODE = console
LEVEL = Info
STACKTRACE_LEVEL = None
ENABLE_ACCESS_LOG = false
ENABLE_XORM_LOG = true
XORM = ,
[log.console]
MODE = console
LEVEL = %(LEVEL)
STACKTRACE_LEVEL = %(STACKTRACE_LEVEL)
FLAGS = stdflags
PREFIX =
COLORIZE = true # Or false if your windows terminal cannot color
```
This is equivalent to sending all logs to the console, with default go log being sent to the console log too.
## Releasing-and-Reopening, Pausing and Resuming logging
If you are running on Unix you may wish to release-and-reopen logs in order to use `logrotate` or other tools.
It is possible force Gitea to release and reopen it's logging files and connections by sending `SIGUSR1` to the
running process, or running `gitea manager logging release-and-reopen`.
Alternatively, you may wish to pause and resume logging - this can be accomplished through the use of the
`gitea manager logging pause` and `gitea manager logging resume` commands. Please note that whilst logging
is paused log events below INFO level will not be stored and only a limited number of events will be stored.
Logging may block, albeit temporarily, slowing Gitea considerably whilst paused - therefore it is
recommended that pausing only done for a very short period of time.
## Adding and removing logging whilst Gitea is running
It is possible to add and remove logging whilst Gitea is running using the `gitea manager logging add` and `remove` subcommands.
This functionality can only adjust running log systems and cannot be used to start the access or router loggers if they
were not already initialized. If you wish to start these systems you are advised to adjust the app.ini and (gracefully) restart
the Gitea service.
The main intention of these commands is to easily add a temporary logger to investigate problems on running systems where a restart
may cause the issue to disappear.
## Log colorization
Logs to the console will be colorized by default when not running on
Windows. Terminal sniffing will occur on Windows and if it is
determined that we are running on a terminal capable of color we will
colorize.
Further, on \*nix it is becoming common to have file logs that are
colored by default. Therefore file logs will be colorised by default
when not running on Windows.
You can switch on or off colorization by using the `COLORIZE` value.
From a development point of view. If you write
`log.Info("A %s string", "formatted")` the `formatted` part of the log
message will be Bolded on colorized logs.
You can change this by either rendering the formatted string yourself.
Or you can wrap the value in a `log.ColoredValue` struct.
The `log.ColoredValue` struct contains a pointer to value, a pointer to
string of bytes which should represent a color and second set of reset
bytes. Pointers were chosen to prevent copying of large numbers of
values. There are several helper methods:
- `log.NewColoredValue` takes a value and 0 or more color attributes
that represent the color. If 0 are provided it will default to a cached
bold. Note, it is recommended that color bytes constructed from
attributes should be cached if this is a commonly used log message.
- `log.NewColoredValuePointer` takes a pointer to a value, and
0 or more color attributes that represent the color.
- `log.NewColoredValueBytes` takes a value and a pointer to an array
of bytes representing the color.
These functions will not double wrap a `log.ColoredValue`. They will
also set the `resetBytes` to the cached `resetBytes`.
The `colorBytes` and `resetBytes` are not exported to prevent
accidental overwriting of internal values.
## ColorFormat & ColorFormatted
Structs may implement the `log.ColorFormatted` interface by implementing the `ColorFormat(fmt.State)` function.
If a `log.ColorFormatted` struct is logged with `%-v` format, its `ColorFormat` will be used instead of the usual `%v`. The full `fmt.State` will be passed to allow implementers to look at additional flags.
In order to help implementers provide `ColorFormat` methods. There is a
`log.ColorFprintf(...)` function in the log module that will wrap values in `log.ColoredValue` and recognise `%-v`.
In general it is recommended not to make the results of this function too verbose to help increase its versatility. Usually this should simply be an `ID`:`Name`. If you wish to make a more verbose result, it is recommended to use `%-+v` as your marker.
## Log Spoofing protection
In order to protect the logs from being spoofed with cleverly
constructed messages. Newlines are now prefixed with a tab and control
characters except those used in an ANSI CSI are escaped with a
preceding `\` and their octal value.
## Creating a new named logger group
Should a developer wish to create a new named logger, `NEWONE`. It is
recommended to add an `ENABLE_NEWONE_LOG` value to the `[log]`
section, and to add a new `NEWONE` value for the modes.
A function like `func newNewOneLogService()` is recommended to manage
construction of the named logger. e.g.
```go
func newNewoneLogService() {
EnableNewoneLog = Cfg.Section("log").Key("ENABLE_NEWONE_LOG").MustBool(false)
Cfg.Section("log").Key("NEWONE").MustString("file") // or console? or "," if you want to send this to default logger by default
if EnableNewoneLog {
options := newDefaultLogOptions()
options.filename = filepath.Join(LogRootPath, "newone.log")
options.flags = "stdflags"
options.bufferLength = Cfg.Section("log").Key("BUFFER_LEN").MustInt64(10000)
generateNamedLogger("newone", options)
}
}
```
You should then add `newOneLogService` to `NewServices()` in
`modules/setting/setting.go`
## Using `logrotate` instead of built-in log rotation
Gitea includes built-in log rotation, which should be enough for most deployments. However, if you instead want to use the `logrotate` utility:
- Disable built-in log rotation by setting `LOG_ROTATE` to `false` in your `app.ini`.
- Install `logrotate`.
- Configure `logrotate` to match your deployment requirements, see `man 8 logrotate` for configuration syntax details. In the `postrotate/endscript` block send Gitea a `USR1` signal via `kill -USR1` or `kill -10` to the `gitea` process itself, or run `gitea manager logging release-and-reopen` (with the appropriate environment). Ensure that your configurations apply to all files emitted by Gitea loggers as described in the above sections.
- Always do `logrotate /etc/logrotate.conf --debug` to test your configurations.
- If you are using docker and are running from outside of the container you can use `docker exec -u $OS_USER $CONTAINER_NAME sh -c 'gitea manager logging release-and-reopen'` or `docker exec $CONTAINER_NAME sh -c '/bin/s6-svc -1 /etc/s6/gitea/'` or send `USR1` directly to the Gitea process itself.
The next `logrotate` jobs will include your configurations, so no restart is needed. You can also immediately reload `logrotate` with `logrotate /etc/logrotate.conf --force`.

@ -120,6 +120,8 @@ arguments - which can alternatively be run by running the subcommand web.`
if err != nil {
log.Fatal("Failed to run app with %s: %v", os.Args, err)
}
log.GetManager().Close()
}
func setFlagsAndBeforeOnSubcommands(command *cli.Command, defaultFlags []cli.Flag, before cli.BeforeFunc) {

@ -14,67 +14,62 @@ import (
// XORMLogBridge a logger bridge from Logger to xorm
type XORMLogBridge struct {
showSQLint *int32
showSQL atomic.Bool
logger log.Logger
}
// NewXORMLogger inits a log bridge for xorm
func NewXORMLogger(showSQL bool) xormlog.Logger {
showSQLint := int32(0)
if showSQL {
showSQLint = 1
}
return &XORMLogBridge{
showSQLint: &showSQLint,
logger: log.GetLogger("xorm"),
}
l := &XORMLogBridge{logger: log.GetLogger("xorm")}
l.showSQL.Store(showSQL)
return l
}
const stackLevel = 8
// Log a message with defined skip and at logging level
func (l *XORMLogBridge) Log(skip int, level log.Level, format string, v ...interface{}) error {
return l.logger.Log(skip+1, level, format, v...)
func (l *XORMLogBridge) Log(skip int, level log.Level, format string, v ...interface{}) {
l.logger.Log(skip+1, level, format, v...)
}
// Debug show debug log
func (l *XORMLogBridge) Debug(v ...interface{}) {
_ = l.Log(stackLevel, log.DEBUG, fmt.Sprint(v...))
l.Log(stackLevel, log.DEBUG, "%s", fmt.Sprint(v...))
}
// Debugf show debug log
func (l *XORMLogBridge) Debugf(format string, v ...interface{}) {
_ = l.Log(stackLevel, log.DEBUG, format, v...)
l.Log(stackLevel, log.DEBUG, format, v...)
}
// Error show error log
func (l *XORMLogBridge) Error(v ...interface{}) {
_ = l.Log(stackLevel, log.ERROR, fmt.Sprint(v...))
l.Log(stackLevel, log.ERROR, "%s", fmt.Sprint(v...))
}
// Errorf show error log
func (l *XORMLogBridge) Errorf(format string, v ...interface{}) {
_ = l.Log(stackLevel, log.ERROR, format, v...)
l.Log(stackLevel, log.ERROR, format, v...)
}
// Info show information level log
func (l *XORMLogBridge) Info(v ...interface{}) {
_ = l.Log(stackLevel, log.INFO, fmt.Sprint(v...))
l.Log(stackLevel, log.INFO, "%s", fmt.Sprint(v...))
}
// Infof show information level log
func (l *XORMLogBridge) Infof(format string, v ...interface{}) {
_ = l.Log(stackLevel, log.INFO, format, v...)
l.Log(stackLevel, log.INFO, format, v...)
}
// Warn show warning log
func (l *XORMLogBridge) Warn(v ...interface{}) {
_ = l.Log(stackLevel, log.WARN, fmt.Sprint(v...))
l.Log(stackLevel, log.WARN, "%s", fmt.Sprint(v...))
}
// Warnf show warnning log
func (l *XORMLogBridge) Warnf(format string, v ...interface{}) {
_ = l.Log(stackLevel, log.WARN, format, v...)
l.Log(stackLevel, log.WARN, format, v...)
}
// Level get logger level
@ -86,10 +81,12 @@ func (l *XORMLogBridge) Level() xormlog.LogLevel {
return xormlog.LOG_INFO
case log.WARN:
return xormlog.LOG_WARNING
case log.ERROR, log.CRITICAL:
case log.ERROR:
return xormlog.LOG_ERR
}
case log.NONE:
return xormlog.LOG_OFF
}
return xormlog.LOG_UNKNOWN
}
// SetLevel set the logger level
@ -98,16 +95,13 @@ func (l *XORMLogBridge) SetLevel(lvl xormlog.LogLevel) {
// ShowSQL set if record SQL
func (l *XORMLogBridge) ShowSQL(show ...bool) {
showSQL := int32(1)
if len(show) > 0 && !show[0] {
showSQL = 0
if len(show) == 0 {
show = []bool{true}
}
atomic.StoreInt32(l.showSQLint, showSQL)
l.showSQL.Store(show[0])
}
// IsShowSQL if record SQL
func (l *XORMLogBridge) IsShowSQL() bool {
showSQL := atomic.LoadInt32(l.showSQLint)
return showSQL == 1
return l.showSQL.Load()
}

@ -224,40 +224,27 @@ func DeletePullsByBaseRepoID(ctx context.Context, repoID int64) error {
return err
}
// ColorFormat writes a colored string to identify this struct
func (pr *PullRequest) ColorFormat(s fmt.State) {
func (pr *PullRequest) String() string {
if pr == nil {
log.ColorFprintf(s, "PR[%d]%s#%d[%s...%s:%s]",
log.NewColoredIDValue(0),
log.NewColoredValue("<nil>/<nil>"),
log.NewColoredIDValue(0),
log.NewColoredValue("<nil>"),
log.NewColoredValue("<nil>/<nil>"),
log.NewColoredValue("<nil>"),
)
return
}
log.ColorFprintf(s, "PR[%d]", log.NewColoredIDValue(pr.ID))
return "<PullRequest nil>"
}
s := new(strings.Builder)
fmt.Fprintf(s, "<PullRequest [%d]", pr.ID)
if pr.BaseRepo != nil {
log.ColorFprintf(s, "%s#%d[%s...", log.NewColoredValue(pr.BaseRepo.FullName()),
log.NewColoredIDValue(pr.Index), log.NewColoredValue(pr.BaseBranch))
fmt.Fprintf(s, "%s#%d[%s...", pr.BaseRepo.FullName(), pr.Index, pr.BaseBranch)
} else {
log.ColorFprintf(s, "Repo[%d]#%d[%s...", log.NewColoredIDValue(pr.BaseRepoID),
log.NewColoredIDValue(pr.Index), log.NewColoredValue(pr.BaseBranch))
fmt.Fprintf(s, "Repo[%d]#%d[%s...", pr.BaseRepoID, pr.Index, pr.BaseBranch)
}
if pr.HeadRepoID == pr.BaseRepoID {
log.ColorFprintf(s, "%s]", log.NewColoredValue(pr.HeadBranch))
fmt.Fprintf(s, "%s]", pr.HeadBranch)
} else if pr.HeadRepo != nil {
log.ColorFprintf(s, "%s:%s]", log.NewColoredValue(pr.HeadRepo.FullName()), log.NewColoredValue(pr.HeadBranch))
fmt.Fprintf(s, "%s:%s]", pr.HeadRepo.FullName(), pr.HeadBranch)
} else {
log.ColorFprintf(s, "Repo[%d]:%s]", log.NewColoredIDValue(pr.HeadRepoID), log.NewColoredValue(pr.HeadBranch))
fmt.Fprintf(s, "Repo[%d]:%s]", pr.HeadRepoID, pr.HeadBranch)
}
}
// String represents the pr as a simple string
func (pr *PullRequest) String() string {
return log.ColorFormatAsString(pr)
s.WriteByte('>')
return s.String()
}
// MustHeadUserName returns the HeadRepo's username if failed return blank

@ -24,6 +24,8 @@ import (
"xorm.io/xorm"
)
// FIXME: this file shouldn't be in a normal package, it should only be compiled for tests
// PrepareTestEnv prepares the test environment and reset the database. The skip parameter should usually be 0.
// Provide models to be sync'd with the database - in particular any models you expect fixtures to be loaded from.
//
@ -110,7 +112,7 @@ func PrepareTestEnv(t *testing.T, skip int, syncModels ...interface{}) (*xorm.En
}
func MainTest(m *testing.M) {
log.Register("test", testlogger.NewTestLogger)
log.RegisterEventWriter("test", testlogger.NewTestLoggerWriter)
giteaRoot := base.SetupGiteaRoot()
if giteaRoot == "" {
@ -154,7 +156,7 @@ func MainTest(m *testing.M) {
os.Exit(1)
}
setting.LoadDBSetting()
setting.InitLogs(true)
setting.InitLoggersForTest()
exitStatus := m.Run()

@ -94,21 +94,11 @@ func init() {
db.RegisterModel(new(TeamInvite))
}
// ColorFormat provides a basic color format for a Team
func (t *Team) ColorFormat(s fmt.State) {
func (t *Team) LogString() string {
if t == nil {
log.ColorFprintf(s, "%d:%s (OrgID: %d) %-v",
log.NewColoredIDValue(0),
"<nil>",
log.NewColoredIDValue(0),
0)
return
return "<Team nil>"
}
log.ColorFprintf(s, "%d:%s (OrgID: %d) %-v",
log.NewColoredIDValue(t.ID),
t.Name,
log.NewColoredIDValue(t.OrgID),
t.AccessMode)
return fmt.Sprintf("<Team %d:%s OrgID=%d AccessMode=%s>", t.ID, t.Name, t.OrgID, t.AccessMode.LogString())
}
// LoadUnits load a list of available units for a team

@ -102,17 +102,10 @@ func (p *Permission) CanWriteIssuesOrPulls(isPull bool) bool {
return p.CanWrite(unit.TypeIssues)
}
// ColorFormat writes a colored string for these Permissions
func (p *Permission) ColorFormat(s fmt.State) {
noColor := log.ColorBytes(log.Reset)
func (p *Permission) LogString() string {
format := "<Permission AccessMode=%s, %d Units, %d UnitsMode(s): [ "
args := []any{p.AccessMode.String(), len(p.Units), len(p.UnitsMode)}
format := "perm_model.AccessMode: %-v, %d Units, %d UnitsMode(s): [ "
args := []interface{}{
p.AccessMode,
log.NewColoredValueBytes(len(p.Units), &noColor),
log.NewColoredValueBytes(len(p.UnitsMode), &noColor),
}
if s.Flag('+') {
for i, unit := range p.Units {
config := ""
if unit.Config != nil {
@ -122,25 +115,15 @@ func (p *Permission) ColorFormat(s fmt.State) {
config = err.Error()
}
}
format += "\nUnits[%d]: ID: %d RepoID: %d Type: %-v Config: %s"
args = append(args,
log.NewColoredValueBytes(i, &noColor),
log.NewColoredIDValue(unit.ID),
log.NewColoredIDValue(unit.RepoID),
unit.Type,
config)
format += "\nUnits[%d]: ID: %d RepoID: %d Type: %s Config: %s"
args = append(args, i, unit.ID, unit.RepoID, unit.Type.LogString(), config)
}
for key, value := range p.UnitsMode {
format += "\nUnitMode[%-v]: %-v"
args = append(args,
key,
value)
}
} else {
format += "..."
args = append(args, key.LogString(), value.LogString())
}
format += " ]"
log.ColorFprintf(s, format, args...)
format += " ]>"
return fmt.Sprintf(format, args...)
}
// GetUserRepoPermission returns the user permissions to the repository

@ -5,8 +5,6 @@ package perm
import (
"fmt"
"code.gitea.io/gitea/modules/log"
)
// AccessMode specifies the users access mode
@ -40,11 +38,8 @@ func (mode AccessMode) String() string {
}
}
// ColorFormat provides a ColorFormatted version of this AccessMode
func (mode AccessMode) ColorFormat(s fmt.State) {
log.ColorFprintf(s, "%d:%s",
log.NewColoredIDValue(mode),
mode)
func (mode AccessMode) LogString() string {
return fmt.Sprintf("<AccessMode:%d:%s>", mode, mode.String())
}
// ParseAccessMode returns corresponding access mode to given permission string.

@ -196,19 +196,11 @@ func (repo *Repository) SanitizedOriginalURL() string {
return u.String()
}
// ColorFormat returns a colored string to represent this repo
func (repo *Repository) ColorFormat(s fmt.State) {
func (repo *Repository) LogString() string {
if repo == nil {
log.ColorFprintf(s, "%d:%s/%s",
log.NewColoredIDValue(0),
"<nil>",
"<nil>")
return
}
log.ColorFprintf(s, "%d:%s/%s",
log.NewColoredIDValue(repo.ID),
repo.OwnerName,
repo.Name)
return "<Repository nil>"
}
return fmt.Sprintf("<Repository %d:%s/%s>", repo.ID, repo.OwnerName, repo.Name)
}
// IsBeingMigrated indicates that repository is being migrated

@ -62,11 +62,8 @@ func (u Type) String() string {
return fmt.Sprintf("Unknown Type %d", u)
}
// ColorFormat provides a ColorFormatted version of this Type
func (u Type) ColorFormat(s fmt.State) {
log.ColorFprintf(s, "%d:%s",
log.NewColoredIDValue(u),
u)
func (u Type) LogString() string {
return fmt.Sprintf("<UnitType:%d:%s>", u, u.String())
}
var (

@ -151,17 +151,11 @@ type SearchOrganizationsOptions struct {
All bool
}
// ColorFormat writes a colored string to identify this struct
func (u *User) ColorFormat(s fmt.State) {
func (u *User) LogString() string {
if u == nil {
log.ColorFprintf(s, "%d:%s",
log.NewColoredIDValue(0),
log.NewColoredValue("<nil>"))
return
}
log.ColorFprintf(s, "%d:%s",
log.NewColoredIDValue(u.ID),
log.NewColoredValue(u.Name))
return "<User nil>"
}
return fmt.Sprintf("<User %d:%s>", u.ID, u.Name)
}
// BeforeUpdate is invoked from XORM before updating this object.

@ -95,10 +95,7 @@ func AccessLogger() func(http.Handler) http.Handler {
log.Error("Could not set up chi access logger: %v", err.Error())
}
err = logger.SendLog(log.INFO, "", "", 0, buf.String(), "")
if err != nil {
log.Error("Could not set up chi access logger: %v", err.Error())
}
logger.Info("%s", buf.String())
})
}
}

@ -6,6 +6,7 @@ package doctor
import (
"context"
"fmt"
"os"
"sort"
"strings"
@ -26,27 +27,9 @@ type Check struct {
Priority int
}
type wrappedLevelLogger struct {
log.LevelLogger
}
func (w *wrappedLevelLogger) Log(skip int, level log.Level, format string, v ...interface{}) error {
return w.LevelLogger.Log(
skip+1,
level,
" - %s "+format,
append(
[]interface{}{
log.NewColoredValueBytes(
fmt.Sprintf("[%s]", strings.ToUpper(level.String()[0:1])),
level.Color()),
}, v...)...)
}
func initDBDisableConsole(ctx context.Context, disableConsole bool) error {
func initDBSkipLogger(ctx context.Context) error {
setting.Init(&setting.Options{})
setting.LoadDBSetting()
setting.InitSQLLog(disableConsole)
if err := db.InitEngine(ctx); err != nil {
return fmt.Errorf("db.InitEngine: %w", err)
}
@ -57,30 +40,61 @@ func initDBDisableConsole(ctx context.Context, disableConsole bool) error {
return nil
}
type doctorCheckLogger struct {
colorize bool
}
var _ log.BaseLogger = (*doctorCheckLogger)(nil)
func (d *doctorCheckLogger) Log(skip int, level log.Level, format string, v ...any) {
_, _ = fmt.Fprintf(os.Stdout, format+"\n", v...)
}
func (d *doctorCheckLogger) GetLevel() log.Level {
return log.TRACE
}
type doctorCheckStepLogger struct {
colorize bool
}
var _ log.BaseLogger = (*doctorCheckStepLogger)(nil)
func (d *doctorCheckStepLogger) Log(skip int, level log.Level, format string, v ...any) {
levelChar := fmt.Sprintf("[%s]", strings.ToUpper(level.String()[0:1]))
var levelArg any = levelChar
if d.colorize {
levelArg = log.NewColoredValue(levelChar, level.ColorAttributes()...)
}
args := append([]any{levelArg}, v...)
_, _ = fmt.Fprintf(os.Stdout, " - %s "+format+"\n", args...)
}
func (d *doctorCheckStepLogger) GetLevel() log.Level {
return log.TRACE
}
// Checks is the list of available commands
var Checks []*Check
// RunChecks runs the doctor checks for the provided list
func RunChecks(ctx context.Context, logger log.Logger, autofix bool, checks []*Check) error {
wrappedLogger := log.LevelLoggerLogger{
LevelLogger: &wrappedLevelLogger{logger},
}
func RunChecks(ctx context.Context, colorize, autofix bool, checks []*Check) error {
// the checks output logs by a special logger, they do not use the default logger
logger := log.BaseLoggerToGeneralLogger(&doctorCheckLogger{colorize: colorize})
loggerStep := log.BaseLoggerToGeneralLogger(&doctorCheckStepLogger{colorize: colorize})
dbIsInit := false
for i, check := range checks {
if !dbIsInit && !check.SkipDatabaseInitialization {
// Only open database after the most basic configuration check
setting.Log.EnableXORMLog = false
if err := initDBDisableConsole(ctx, true); err != nil {
if err := initDBSkipLogger(ctx); err != nil {
logger.Error("Error whilst initializing the database: %v", err)
logger.Error("Check if you are using the right config file. You can use a --config directive to specify one.")
return nil
}
dbIsInit = true
}
logger.Info("[%d] %s", log.NewColoredIDValue(i+1), check.Title)
logger.Flush()
if err := check.Run(ctx, &wrappedLogger, autofix); err != nil {
logger.Info("\n[%d] %s", i+1, check.Title)
if err := check.Run(ctx, loggerStep, autofix); err != nil {
if check.AbortIfFailed {
logger.Critical("FAIL")
return err
@ -88,9 +102,9 @@ func RunChecks(ctx context.Context, logger log.Logger, autofix bool, checks []*C
logger.Error("ERROR")
} else {
logger.Info("OK")
logger.Flush()
}
}
logger.Info("\nAll done.")
return nil
}

@ -10,7 +10,6 @@ import (
"strings"
"testing"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
@ -18,8 +17,6 @@ import (
)
func testRun(m *testing.M) error {
_ = log.NewLogger(1000, "console", "console", `{"level":"trace","stacktracelevel":"NONE","stderr":true}`)
gitHomePath, err := os.MkdirTemp(os.TempDir(), "git-home")
if err != nil {
return fmt.Errorf("unable to create temp dir: %w", err)

@ -30,7 +30,7 @@ const (
// * HTTP redirection fallback
// * Builtin SSH listener
//
// If you add an additional place you must increment this number
// If you add a new place you must increment this number
// and add a function to call manager.InformCleanup if it's not going to be used
const numberOfServersToCreate = 4

@ -16,6 +16,7 @@ import (
"syscall"
"time"
"code.gitea.io/gitea/modules/graceful/releasereopen"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/setting"
@ -185,7 +186,7 @@ func (g *Manager) handleSignals(ctx context.Context) {
case syscall.SIGUSR1:
log.Warn("PID %d. Received SIGUSR1. Releasing and reopening logs", pid)
g.notify(statusMsg("Releasing and reopening logs"))
if err := log.ReleaseReopen(); err != nil {
if err := releasereopen.GetManager().ReleaseReopen(); err != nil {
log.Error("Error whilst releasing and reopening logs: %v", err)
}
case syscall.SIGUSR2:

@ -0,0 +1,61 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package releasereopen
import (
"errors"
"sync"
)
type ReleaseReopener interface {
ReleaseReopen() error
}
type Manager struct {
mu sync.Mutex
counter int64
releaseReopeners map[int64]ReleaseReopener
}
func (r *Manager) Register(rr ReleaseReopener) (cancel func()) {
r.mu.Lock()
defer r.mu.Unlock()
r.counter++
currentCounter := r.counter
r.releaseReopeners[r.counter] = rr
return func() {
r.mu.Lock()
defer r.mu.Unlock()
delete(r.releaseReopeners, currentCounter)
}
}
func (r *Manager) ReleaseReopen() error {
r.mu.Lock()
defer r.mu.Unlock()
var errs []error
for _, rr := range r.releaseReopeners {
if err := rr.ReleaseReopen(); err != nil {
errs = append(errs, err)
}
}
return errors.Join(errs...)
}
func GetManager() *Manager {
return manager
}
func NewManager() *Manager {
return &Manager{
releaseReopeners: make(map[int64]ReleaseReopener),
}
}
var manager = NewManager()

@ -0,0 +1,43 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package releasereopen
import (
"testing"
"github.com/stretchr/testify/assert"
)
type testReleaseReopener struct {
count int
}
func (t *testReleaseReopener) ReleaseReopen() error {
t.count++
return nil
}
func TestManager(t *testing.T) {
m := NewManager()
t1 := &testReleaseReopener{}
t2 := &testReleaseReopener{}
t3 := &testReleaseReopener{}
_ = m.Register(t1)
c2 := m.Register(t2)
_ = m.Register(t3)
assert.NoError(t, m.ReleaseReopen())
assert.EqualValues(t, 1, t1.count)
assert.EqualValues(t, 1, t2.count)
assert.EqualValues(t, 1, t3.count)
c2()
assert.NoError(t, m.ReleaseReopen())
assert.EqualValues(t, 2, t1.count)
assert.EqualValues(t, 1, t2.count)
assert.EqualValues(t, 2, t3.count)
}

@ -49,14 +49,6 @@ type ElasticSearchIndexer struct {
lock sync.RWMutex
}
type elasticLogger struct {
log.Logger
}
func (l elasticLogger) Printf(format string, args ...interface{}) {
_ = l.Logger.Log(2, l.Logger.GetLevel(), format, args...)
}
// NewElasticSearchIndexer creates a new elasticsearch indexer
func NewElasticSearchIndexer(url, indexerName string) (*ElasticSearchIndexer, bool, error) {
opts := []elastic.ClientOptionFunc{
@ -66,15 +58,11 @@ func NewElasticSearchIndexer(url, indexerName string) (*ElasticSearchIndexer, bo
elastic.SetGzip(false),
}
logger := elasticLogger{log.GetLogger(log.DEFAULT)}
logger := log.GetLogger(log.DEFAULT)
if logger.GetLevel() == log.TRACE || logger.GetLevel() == log.DEBUG {
opts = append(opts, elastic.SetTraceLog(logger))
} else if logger.GetLevel() == log.ERROR || logger.GetLevel() == log.CRITICAL || logger.GetLevel() == log.FATAL {
opts = append(opts, elastic.SetErrorLog(logger))
} else if logger.GetLevel() == log.INFO || logger.GetLevel() == log.WARN {
opts = append(opts, elastic.SetInfoLog(logger))
}
opts = append(opts, elastic.SetTraceLog(&log.PrintfLogger{Logf: logger.Trace}))
opts = append(opts, elastic.SetInfoLog(&log.PrintfLogger{Logf: logger.Info}))
opts = append(opts, elastic.SetErrorLog(&log.PrintfLogger{Logf: logger.Error}))
client, err := elastic.NewClient(opts...)
if err != nil {

@ -29,14 +29,6 @@ type ElasticSearchIndexer struct {
lock sync.RWMutex
}
type elasticLogger struct {
log.LevelLogger
}
func (l elasticLogger) Printf(format string, args ...interface{}) {
_ = l.Log(2, l.GetLevel(), format, args...)
}
// NewElasticSearchIndexer creates a new elasticsearch indexer
func NewElasticSearchIndexer(url, indexerName string) (*ElasticSearchIndexer, error) {
opts := []elastic.ClientOptionFunc{
@ -46,15 +38,10 @@ func NewElasticSearchIndexer(url, indexerName string) (*ElasticSearchIndexer, er
elastic.SetGzip(false),
}
logger := elasticLogger{log.GetLogger(log.DEFAULT)}
if logger.GetLevel() == log.TRACE || logger.GetLevel() == log.DEBUG {
opts = append(opts, elastic.SetTraceLog(logger))
} else if logger.GetLevel() == log.ERROR || logger.GetLevel() == log.CRITICAL || logger.GetLevel() == log.FATAL {
opts = append(opts, elastic.SetErrorLog(logger))
} else if logger.GetLevel() == log.INFO || logger.GetLevel() == log.WARN {
opts = append(opts, elastic.SetInfoLog(logger))
}
logger := log.GetLogger(log.DEFAULT)
opts = append(opts, elastic.SetTraceLog(&log.PrintfLogger{Logf: logger.Trace}))
opts = append(opts, elastic.SetInfoLog(&log.PrintfLogger{Logf: logger.Info}))
opts = append(opts, elastic.SetErrorLog(&log.PrintfLogger{Logf: logger.Error}))
client, err := elastic.NewClient(opts...)
if err != nil {

@ -13,8 +13,6 @@ import (
"strconv"
"strings"
"code.gitea.io/gitea/modules/log"
"github.com/minio/sha256-simd"
)
@ -113,15 +111,11 @@ func (p Pointer) RelativePath() string {
return path.Join(p.Oid[0:2], p.Oid[2:4], p.Oid[4:])
}
// ColorFormat provides a basic color format for a Team
func (p Pointer) ColorFormat(s fmt.State) {
func (p Pointer) LogString() string {
if p.Oid == "" && p.Size == 0 {
log.ColorFprintf(s, "<empty>")
return
return "<LFSPointer empty>"
}
log.ColorFprintf(s, "%s:%d",
log.NewColoredIDValue(p.Oid),
p.Size)
return fmt.Sprintf("<LFSPointer %s:%d>", p.Oid, p.Size)
}
// GeneratePointer generates a pointer for arbitrary content

@ -0,0 +1,115 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"fmt"
"strconv"
)
const escape = "\033"
// ColorAttribute defines a single SGR Code
type ColorAttribute int
// Base ColorAttributes
const (
Reset ColorAttribute = iota
Bold
Faint
Italic
Underline
BlinkSlow
BlinkRapid
ReverseVideo
Concealed
CrossedOut
)
// Foreground text colors
const (
FgBlack ColorAttribute = iota + 30
FgRed
FgGreen
FgYellow
FgBlue
FgMagenta
FgCyan
FgWhite
)
// Foreground Hi-Intensity text colors
const (
FgHiBlack ColorAttribute = iota + 90
FgHiRed
FgHiGreen
FgHiYellow
FgHiBlue
FgHiMagenta
FgHiCyan
FgHiWhite
)
// Background text colors
const (
BgBlack ColorAttribute = iota + 40
BgRed
BgGreen
BgYellow
BgBlue
BgMagenta
BgCyan
BgWhite
)
// Background Hi-Intensity text colors
const (
BgHiBlack ColorAttribute = iota + 100
BgHiRed
BgHiGreen
BgHiYellow
BgHiBlue
BgHiMagenta
BgHiCyan
BgHiWhite
)
var (
resetBytes = ColorBytes(Reset)
fgCyanBytes = ColorBytes(FgCyan)
fgGreenBytes = ColorBytes(FgGreen)
)
type ColoredValue struct {
v any
colors []ColorAttribute
}
func (c *ColoredValue) Format(f fmt.State, verb rune) {
_, _ = f.Write(ColorBytes(c.colors...))
s := fmt.Sprintf(fmt.FormatString(f, verb), c.v)
_, _ = f.Write([]byte(s))
_, _ = f.Write(resetBytes)
}
func NewColoredValue(v any, color ...ColorAttribute) *ColoredValue {
return &ColoredValue{v: v, colors: color}
}
// ColorBytes converts a list of ColorAttributes to a byte array
func ColorBytes(attrs ...ColorAttribute) []byte {
bytes := make([]byte, 0, 20)
bytes = append(bytes, escape[0], '[')
if len(attrs) > 0 {
bytes = append(bytes, strconv.Itoa(int(attrs[0]))...)
for _, a := range attrs[1:] {
bytes = append(bytes, ';')
bytes = append(bytes, strconv.Itoa(int(a))...)
}
} else {
bytes = append(bytes, strconv.Itoa(int(Bold))...)
}
bytes = append(bytes, 'm')
return bytes
}

@ -0,0 +1,14 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
// CanColorStdout reports if we can color the Stdout
// Although we could do terminal sniffing and the like - in reality
// most tools on *nix are happy to display ansi colors.
// We will terminal sniff on Windows in console_windows.go
var CanColorStdout = true
// CanColorStderr reports if we can color the Stderr
var CanColorStderr = true

@ -20,7 +20,7 @@ func enableVTMode(console windows.Handle) bool {
// EnableVirtualTerminalProcessing is the console mode to allow ANSI code
// interpretation on the console. See:
// https://docs.microsoft.com/en-us/windows/console/setconsolemode
// It only works on windows 10. Earlier terminals will fail with an err which we will
// It only works on Windows 10. Earlier terminals will fail with an err which we will
// handle to say don't color
mode |= windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING
err = windows.SetConsoleMode(console, mode)

@ -8,15 +8,15 @@ import (
"time"
)
var statusToColor = map[int][]byte{
100: ColorBytes(Bold),
200: ColorBytes(FgGreen),
300: ColorBytes(FgYellow),
304: ColorBytes(FgCyan),
400: ColorBytes(Bold, FgRed),
401: ColorBytes(Bold, FgMagenta),
403: ColorBytes(Bold, FgMagenta),
500: ColorBytes(Bold, BgRed),
var statusToColor = map[int][]ColorAttribute{
100: {Bold},
200: {FgGreen},
300: {FgYellow},
304: {FgCyan},
400: {Bold, FgRed},
401: {Bold, FgMagenta},
403: {Bold, FgMagenta},
500: {Bold, BgRed},
}
// ColoredStatus adds colors for HTTP status
@ -26,30 +26,30 @@ func ColoredStatus(status int, s ...string) *ColoredValue {
color, ok = statusToColor[(status/100)*100]
}
if !ok {
color = fgBoldBytes
color = []ColorAttribute{Bold}
}
if len(s) > 0 {
return NewColoredValueBytes(s[0], &color)
return NewColoredValue(s[0], color...)
}
return NewColoredValueBytes(status, &color)
return NewColoredValue(status, color...)
}
var methodToColor = map[string][]byte{
"GET": ColorBytes(FgBlue),
"POST": ColorBytes(FgGreen),
"DELETE": ColorBytes(FgRed),
"PATCH": ColorBytes(FgCyan),
"PUT": ColorBytes(FgYellow, Faint),
"HEAD": ColorBytes(FgBlue, Faint),
var methodToColor = map[string][]ColorAttribute{
"GET": {FgBlue},
"POST": {FgGreen},
"DELETE": {FgRed},
"PATCH": {FgCyan},
"PUT": {FgYellow, Faint},
"HEAD": {FgBlue, Faint},
}
// ColoredMethod adds colors for HTTP methods on log
func ColoredMethod(method string) *ColoredValue {
color, ok := methodToColor[method]
if !ok {
return NewColoredValueBytes(method, &fgBoldBytes)
return NewColoredValue(method, Bold)
}
return NewColoredValueBytes(method, &color)
return NewColoredValue(method, color...)
}
var (
@ -61,15 +61,15 @@ var (
10 * time.Second,
}
durationColors = [][]byte{
ColorBytes(FgGreen),
ColorBytes(Bold),
ColorBytes(FgYellow),
ColorBytes(FgRed, Bold),
ColorBytes(BgRed),
durationColors = [][]ColorAttribute{
{FgGreen},
{Bold},
{FgYellow},
{FgRed, Bold},
{BgRed},
}
wayTooLong = ColorBytes(BgMagenta)
wayTooLong = BgMagenta
)
// ColoredTime converts the provided time to a ColoredValue for logging. The duration is always formatted in milliseconds.
@ -80,8 +80,8 @@ func ColoredTime(duration time.Duration) *ColoredValue {
str := fmt.Sprintf("%.1fms", float64(duration.Microseconds())/1000)
for i, k := range durations {
if duration < k {
return NewColoredValueBytes(str, &durationColors[i])
return NewColoredValue(str, durationColors[i]...)
}
}
return NewColoredValueBytes(str, &wayTooLong)
return NewColoredValue(str, wayTooLong)
}

@ -1,435 +0,0 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"fmt"
"io"
"reflect"
"strconv"
"strings"
)
const escape = "\033"
// ColorAttribute defines a single SGR Code
type ColorAttribute int
// Base ColorAttributes
const (
Reset ColorAttribute = iota
Bold
Faint
Italic
Underline
BlinkSlow
BlinkRapid
ReverseVideo
Concealed
CrossedOut
)
// Foreground text colors
const (
FgBlack ColorAttribute = iota + 30
FgRed
FgGreen
FgYellow
FgBlue
FgMagenta
FgCyan
FgWhite
)
// Foreground Hi-Intensity text colors
const (
FgHiBlack ColorAttribute = iota + 90
FgHiRed
FgHiGreen
FgHiYellow
FgHiBlue
FgHiMagenta
FgHiCyan
FgHiWhite
)
// Background text colors
const (
BgBlack ColorAttribute = iota + 40
BgRed
BgGreen
BgYellow
BgBlue
BgMagenta
BgCyan
BgWhite
)
// Background Hi-Intensity text colors
const (
BgHiBlack ColorAttribute = iota + 100
BgHiRed
BgHiGreen
BgHiYellow
BgHiBlue
BgHiMagenta
BgHiCyan
BgHiWhite
)
var colorAttributeToString = map[ColorAttribute]string{
Reset: "Reset",
Bold: "Bold",
Faint: "Faint",
Italic: "Italic",
Underline: "Underline",
BlinkSlow: "BlinkSlow",
BlinkRapid: "BlinkRapid",
ReverseVideo: "ReverseVideo",
Concealed: "Concealed",
CrossedOut: "CrossedOut",
FgBlack: "FgBlack",
FgRed: "FgRed",
FgGreen: "FgGreen",
FgYellow: "FgYellow",
FgBlue: "FgBlue",
FgMagenta: "FgMagenta",
FgCyan: "FgCyan",
FgWhite: "FgWhite",
FgHiBlack: "FgHiBlack",
FgHiRed: "FgHiRed",
FgHiGreen: "FgHiGreen",
FgHiYellow: "FgHiYellow",
FgHiBlue: "FgHiBlue",
FgHiMagenta: "FgHiMagenta",
FgHiCyan: "FgHiCyan",
FgHiWhite: "FgHiWhite",
BgBlack: "BgBlack",
BgRed: "BgRed",
BgGreen: "BgGreen",
BgYellow: "BgYellow",
BgBlue: "BgBlue",
BgMagenta: "BgMagenta",
BgCyan: "BgCyan",
BgWhite: "BgWhite",
BgHiBlack: "BgHiBlack",
BgHiRed: "BgHiRed",
BgHiGreen: "BgHiGreen",
BgHiYellow: "BgHiYellow",
BgHiBlue: "BgHiBlue",
BgHiMagenta: "BgHiMagenta",
BgHiCyan: "BgHiCyan",
BgHiWhite: "BgHiWhite",
}
func (c *ColorAttribute) String() string {
return colorAttributeToString[*c]
}
var colorAttributeFromString = map[string]ColorAttribute{}
// ColorAttributeFromString will return a ColorAttribute given a string
func ColorAttributeFromString(from string) ColorAttribute {
lowerFrom := strings.TrimSpace(strings.ToLower(from))
return colorAttributeFromString[lowerFrom]
}
// ColorString converts a list of ColorAttributes to a color string
func ColorString(attrs ...ColorAttribute) string {
return string(ColorBytes(attrs...))
}
// ColorBytes converts a list of ColorAttributes to a byte array
func ColorBytes(attrs ...ColorAttribute) []byte {
bytes := make([]byte, 0, 20)
bytes = append(bytes, escape[0], '[')
if len(attrs) > 0 {
bytes = append(bytes, strconv.Itoa(int(attrs[0]))...)
for _, a := range attrs[1:] {
bytes = append(bytes, ';')
bytes = append(bytes, strconv.Itoa(int(a))...)
}
} else {
bytes = append(bytes, strconv.Itoa(int(Bold))...)
}
bytes = append(bytes, 'm')
return bytes
}
var levelToColor = map[Level][]byte{
TRACE: ColorBytes(Bold, FgCyan),
DEBUG: ColorBytes(Bold, FgBlue),
INFO: ColorBytes(Bold, FgGreen),
WARN: ColorBytes(Bold, FgYellow),
ERROR: ColorBytes(Bold, FgRed),
CRITICAL: ColorBytes(Bold, BgMagenta),
FATAL: ColorBytes(Bold, BgRed),
NONE: ColorBytes(Reset),
}
var (
resetBytes = ColorBytes(Reset)
fgCyanBytes = ColorBytes(FgCyan)
fgGreenBytes = ColorBytes(FgGreen)
fgBoldBytes = ColorBytes(Bold)
)
type protectedANSIWriterMode int
const (
escapeAll protectedANSIWriterMode = iota
allowColor
removeColor
)
type protectedANSIWriter struct {
w io.Writer
mode protectedANSIWriterMode
}
// Write will protect against unusual characters
func (c *protectedANSIWriter) Write(bytes []byte) (int, error) {
end := len(bytes)
totalWritten := 0
normalLoop:
for i := 0; i < end; {
lasti := i
if c.mode == escapeAll {
for i < end && (bytes[i] >= ' ' || bytes[i] == '\n' || bytes[i] == '\t') {
i++
}
} else {
// Allow tabs if we're not escaping everything
for i < end && (bytes[i] >= ' ' || bytes[i] == '\t') {
i++
}
}
if i > lasti {
written, err := c.w.Write(bytes[lasti:i])
totalWritten += written
if err != nil {
return totalWritten, err
}
}
if i >= end {
break
}
// If we're not just escaping all we should prefix all newlines with a \t
if c.mode != escapeAll {
if bytes[i] == '\n' {
written, err := c.w.Write([]byte{'\n', '\t'})
if written > 0 {
totalWritten++
}
if err != nil {
return totalWritten, err
}
i++
continue normalLoop
}
if bytes[i] == escape[0] && i+1 < end && bytes[i+1] == '[' {
for j := i + 2; j < end; j++ {
if bytes[j] >= '0' && bytes[j] <= '9' {
continue
}
if bytes[j] == ';' {
continue
}
if bytes[j] == 'm' {
if c.mode == allowColor {
written, err := c.w.Write(bytes[i : j+1])
totalWritten += written
if err != nil {
return totalWritten, err
}
} else {
totalWritten = j
}
i = j + 1
continue normalLoop
}
break
}
}
}
// Process naughty character
if _, err := fmt.Fprintf(c.w, `\%#03o`, bytes[i]); err != nil {
return totalWritten, err
}
i++
totalWritten++
}
return totalWritten, nil
}
// ColorSprintf returns a colored string from a format and arguments
// arguments will be wrapped in ColoredValues to protect against color spoofing
func ColorSprintf(format string, args ...interface{}) string {
if len(args) > 0 {
v := make([]interface{}, len(args))
for i := 0; i < len(v); i++ {
v[i] = NewColoredValuePointer(&args[i])
}
return fmt.Sprintf(format, v...)
}
return format
}
// ColorFprintf will write to the provided writer similar to ColorSprintf
func ColorFprintf(w io.Writer, format string, args ...interface{}) (int, error) {
if len(args) > 0 {
v := make([]interface{}, len(args))
for i := 0; i < len(v); i++ {
v[i] = NewColoredValuePointer(&args[i])
}
return fmt.Fprintf(w, format, v...)
}
return fmt.Fprint(w, format)
}
// ColorFormatted structs provide their own colored string when formatted with ColorSprintf
type ColorFormatted interface {
// ColorFormat provides the colored representation of the value
ColorFormat(s fmt.State)
}
var colorFormattedType = reflect.TypeOf((*ColorFormatted)(nil)).Elem()
// ColoredValue will Color the provided value
type ColoredValue struct {
colorBytes *[]byte
resetBytes *[]byte
Value *interface{}
}
// NewColoredValue is a helper function to create a ColoredValue from a Value
// If no color is provided it defaults to Bold with standard Reset
// If a ColoredValue is provided it is not changed
func NewColoredValue(value interface{}, color ...ColorAttribute) *ColoredValue {
return NewColoredValuePointer(&value, color...)
}
// NewColoredValuePointer is a helper function to create a ColoredValue from a Value Pointer
// If no color is provided it defaults to Bold with standard Reset
// If a ColoredValue is provided it is not changed
func NewColoredValuePointer(value *interface{}, color ...ColorAttribute) *ColoredValue {
if val, ok := (*value).(*ColoredValue); ok {
return val
}
if len(color) > 0 {
bytes := ColorBytes(color...)
return &ColoredValue{
colorBytes: &bytes,
resetBytes: &resetBytes,
Value: value,
}
}
return &ColoredValue{
colorBytes: &fgBoldBytes,
resetBytes: &resetBytes,
Value: value,
}
}
// NewColoredValueBytes creates a value from the provided value with color bytes
// If a ColoredValue is provided it is not changed
func NewColoredValueBytes(value interface{}, colorBytes *[]byte) *ColoredValue {
if val, ok := value.(*ColoredValue); ok {
return val
}
return &ColoredValue{
colorBytes: colorBytes,
resetBytes: &resetBytes,
Value: &value,
}
}
// NewColoredIDValue is a helper function to create a ColoredValue from a Value
// The Value will be colored with FgCyan
// If a ColoredValue is provided it is not changed
func NewColoredIDValue(value interface{}) *ColoredValue {
return NewColoredValueBytes(value, &fgCyanBytes)
}
// Format will format the provided value and protect against ANSI color spoofing within the value
// If the wrapped value is ColorFormatted and the format is "%-v" then its ColorString will
// be used. It is presumed that this ColorString is safe.
func (cv *ColoredValue) Format(s fmt.State, c rune) {
if c == 'v' && s.Flag('-') {
if val, ok := (*cv.Value).(ColorFormatted); ok {
val.ColorFormat(s)
return
}
v := reflect.ValueOf(*cv.Value)
t := v.Type()
if reflect.PtrTo(t).Implements(colorFormattedType) {
vp := reflect.New(t)
vp.Elem().Set(v)
val := vp.Interface().(ColorFormatted)
val.ColorFormat(s)
return
}
}
s.Write(*cv.colorBytes)
fmt.Fprintf(&protectedANSIWriter{w: s}, fmtString(s, c), *(cv.Value))
s.Write(*cv.resetBytes)
}
// ColorFormatAsString returns the result of the ColorFormat without the color
func ColorFormatAsString(colorVal ColorFormatted) string {
s := new(strings.Builder)
_, _ = ColorFprintf(&protectedANSIWriter{w: s, mode: removeColor}, "%-v", colorVal)
return s.String()
}
// SetColorBytes will allow a user to set the colorBytes of a colored value
func (cv *ColoredValue) SetColorBytes(colorBytes []byte) {
cv.colorBytes = &colorBytes
}
// SetColorBytesPointer will allow a user to set the colorBytes pointer of a colored value
func (cv *ColoredValue) SetColorBytesPointer(colorBytes *[]byte) {
cv.colorBytes = colorBytes
}
// SetResetBytes will allow a user to set the resetBytes pointer of a colored value
func (cv *ColoredValue) SetResetBytes(resetBytes []byte) {
cv.resetBytes = &resetBytes
}
// SetResetBytesPointer will allow a user to set the resetBytes pointer of a colored value
func (cv *ColoredValue) SetResetBytesPointer(resetBytes *[]byte) {
cv.resetBytes = resetBytes
}
func fmtString(s fmt.State, c rune) string {
var width, precision string
base := make([]byte, 0, 8)
base = append(base, '%')
for _, c := range []byte(" +-#0") {
if s.Flag(int(c)) {
base = append(base, c)
}
}
if w, ok := s.Width(); ok {
width = strconv.Itoa(w)
}
if p, ok := s.Precision(); ok {
precision = "." + strconv.Itoa(p)
}
return fmt.Sprintf("%s%s%s%c", base, width, precision, c)
}
func init() {
for attr, from := range colorAttributeToString {
colorAttributeFromString[strings.ToLower(from)] = attr
}
}

@ -1,137 +0,0 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"fmt"
"io"
"net"
"code.gitea.io/gitea/modules/json"
)
type connWriter struct {
innerWriter io.WriteCloser
ReconnectOnMsg bool `json:"reconnectOnMsg"`
Reconnect bool `json:"reconnect"`
Net string `json:"net"`
Addr string `json:"addr"`
}
// Close the inner writer
func (i *connWriter) Close() error {
if i.innerWriter != nil {
return i.innerWriter.Close()
}
return nil
}
// Write the data to the connection
func (i *connWriter) Write(p []byte) (int, error) {
if i.neededConnectOnMsg() {
if err := i.connect(); err != nil {
return 0, err
}
}
if i.ReconnectOnMsg {
defer i.innerWriter.Close()
}
return i.innerWriter.Write(p)
}
func (i *connWriter) neededConnectOnMsg() bool {
if i.Reconnect {
i.Reconnect = false
return true
}
if i.innerWriter == nil {
return true
}
return i.ReconnectOnMsg
}
func (i *connWriter) connect() error {
if i.innerWriter != nil {
i.innerWriter.Close()
i.innerWriter = nil
}
conn, err := net.Dial(i.Net, i.Addr)
if err != nil {
return err
}
if tcpConn, ok := conn.(*net.TCPConn); ok {
err = tcpConn.SetKeepAlive(true)
if err != nil {
return err
}
}
i.innerWriter = conn
return nil
}
func (i *connWriter) releaseReopen() error {
if i.innerWriter != nil {
return i.connect()
}
return nil
}
// ConnLogger implements LoggerProvider.
// it writes messages in keep-live tcp connection.
type ConnLogger struct {
WriterLogger
ReconnectOnMsg bool `json:"reconnectOnMsg"`
Reconnect bool `json:"reconnect"`
Net string `json:"net"`
Addr string `json:"addr"`
}
// NewConn creates new ConnLogger returning as LoggerProvider.
func NewConn() LoggerProvider {
conn := new(ConnLogger)
conn.Level = TRACE
return conn
}
// Init inits connection writer with json config.
// json config only need key "level".
func (log *ConnLogger) Init(jsonconfig string) error {
err := json.Unmarshal([]byte(jsonconfig), log)
if err != nil {
return fmt.Errorf("Unable to parse JSON: %w", err)
}
log.NewWriterLogger(&connWriter{
ReconnectOnMsg: log.ReconnectOnMsg,
Reconnect: log.Reconnect,
Net: log.Net,
Addr: log.Addr,
}, log.Level)
return nil
}
// Flush does nothing for this implementation
func (log *ConnLogger) Flush() {
}
// GetName returns the default name for this implementation
func (log *ConnLogger) GetName() string {
return "conn"
}
// ReleaseReopen causes the ConnLogger to reconnect to the server
func (log *ConnLogger) ReleaseReopen() error {
return log.out.(*connWriter).releaseReopen()
}
func init() {
Register("conn", NewConn)
}

@ -1,230 +0,0 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"fmt"
"io"
"net"
"strings"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func listenReadAndClose(t *testing.T, l net.Listener, expected string) {
conn, err := l.Accept()
assert.NoError(t, err)
defer conn.Close()
written, err := io.ReadAll(conn)
assert.NoError(t, err)
assert.Equal(t, expected, string(written))
}
func TestConnLogger(t *testing.T) {
protocol := "tcp"
address := ":3099"
l, err := net.Listen(protocol, address)
if err != nil {
t.Fatal(err)
}
defer l.Close()
prefix := "TestPrefix "
level := INFO
flags := LstdFlags | LUTC | Lfuncname
logger := NewConn()
connLogger := logger.(*ConnLogger)
logger.Init(fmt.Sprintf("{\"prefix\":\"%s\",\"level\":\"%s\",\"flags\":%d,\"reconnectOnMsg\":%t,\"reconnect\":%t,\"net\":\"%s\",\"addr\":\"%s\"}", prefix, level.String(), flags, true, true, protocol, address))
assert.Equal(t, flags, connLogger.Flags)
assert.Equal(t, level, connLogger.Level)
assert.Equal(t, level, logger.GetLevel())
location, _ := time.LoadLocation("EST")
date := time.Date(2019, time.January, 13, 22, 3, 30, 15, location)
dateString := date.UTC().Format("2006/01/02 15:04:05")
event := Event{
level: INFO,
msg: "TEST MSG",
caller: "CALLER",
filename: "FULL/FILENAME",
line: 1,
time: date,
}
expected := fmt.Sprintf("%s%s %s:%d:%s [%c] %s\n", prefix, dateString, event.filename, event.line, event.caller, strings.ToUpper(event.level.String())[0], event.msg)
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
listenReadAndClose(t, l, expected)
}()
go func() {
defer wg.Done()
err := logger.LogEvent(&event)
assert.NoError(t, err)
}()
wg.Wait()
event.level = WARN
expected = fmt.Sprintf("%s%s %s:%d:%s [%c] %s\n", prefix, dateString, event.filename, event.line, event.caller, strings.ToUpper(event.level.String())[0], event.msg)
wg.Add(2)
go func() {
defer wg.Done()
listenReadAndClose(t, l, expected)
}()
go func() {
defer wg.Done()
err := logger.LogEvent(&event)
assert.NoError(t, err)
}()
wg.Wait()
logger.Close()
}
func TestConnLoggerBadConfig(t *testing.T) {
logger := NewConn()
err := logger.Init("{")
assert.Error(t, err)
assert.Contains(t, err.Error(), "Unable to parse JSON")
logger.Close()
}
func TestConnLoggerCloseBeforeSend(t *testing.T) {
protocol := "tcp"
address := ":3099"
prefix := "TestPrefix "
level := INFO
flags := LstdFlags | LUTC | Lfuncname
logger := NewConn()
logger.Init(fmt.Sprintf("{\"prefix\":\"%s\",\"level\":\"%s\",\"flags\":%d,\"reconnectOnMsg\":%t,\"reconnect\":%t,\"net\":\"%s\",\"addr\":\"%s\"}", prefix, level.String(), flags, false, false, protocol, address))
logger.Close()
}
func TestConnLoggerFailConnect(t *testing.T) {
protocol := "tcp"
address := ":3099"
prefix := "TestPrefix "
level := INFO
flags := LstdFlags | LUTC | Lfuncname
logger := NewConn()
logger.Init(fmt.Sprintf("{\"prefix\":\"%s\",\"level\":\"%s\",\"flags\":%d,\"reconnectOnMsg\":%t,\"reconnect\":%t,\"net\":\"%s\",\"addr\":\"%s\"}", prefix, level.String(), flags, false, false, protocol, address))
assert.Equal(t, level, logger.GetLevel())
location, _ := time.LoadLocation("EST")
date := time.Date(2019, time.January, 13, 22, 3, 30, 15, location)
// dateString := date.UTC().Format("2006/01/02 15:04:05")
event := Event{
level: INFO,
msg: "TEST MSG",
caller: "CALLER",
filename: "FULL/FILENAME",
line: 1,
time: date,
}
err := logger.LogEvent(&event)
assert.Error(t, err)
logger.Close()
}
func TestConnLoggerClose(t *testing.T) {
protocol := "tcp"
address := ":3099"
l, err := net.Listen(protocol, address)
if err != nil {
t.Fatal(err)
}
defer l.Close()
prefix := "TestPrefix "
level := INFO
flags := LstdFlags | LUTC | Lfuncname
logger := NewConn()
connLogger := logger.(*ConnLogger)
logger.Init(fmt.Sprintf("{\"prefix\":\"%s\",\"level\":\"%s\",\"flags\":%d,\"reconnectOnMsg\":%t,\"reconnect\":%t,\"net\":\"%s\",\"addr\":\"%s\"}", prefix, level.String(), flags, false, false, protocol, address))
assert.Equal(t, flags, connLogger.Flags)
assert.Equal(t, level, connLogger.Level)
assert.Equal(t, level, logger.GetLevel())
location, _ := time.LoadLocation("EST")
date := time.Date(2019, time.January, 13, 22, 3, 30, 15, location)
dateString := date.UTC().Format("2006/01/02 15:04:05")
event := Event{
level: INFO,
msg: "TEST MSG",
caller: "CALLER",
filename: "FULL/FILENAME",
line: 1,
time: date,
}
expected := fmt.Sprintf("%s%s %s:%d:%s [%c] %s\n", prefix, dateString, event.filename, event.line, event.caller, strings.ToUpper(event.level.String())[0], event.msg)
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
err := logger.LogEvent(&event)
assert.NoError(t, err)
logger.Close()
}()
go func() {
defer wg.Done()
listenReadAndClose(t, l, expected)
}()
wg.Wait()
logger = NewConn()
connLogger = logger.(*ConnLogger)
logger.Init(fmt.Sprintf("{\"prefix\":\"%s\",\"level\":\"%s\",\"flags\":%d,\"reconnectOnMsg\":%t,\"reconnect\":%t,\"net\":\"%s\",\"addr\":\"%s\"}", prefix, level.String(), flags, false, true, protocol, address))
assert.Equal(t, flags, connLogger.Flags)
assert.Equal(t, level, connLogger.Level)
assert.Equal(t, level, logger.GetLevel())
event.level = WARN
expected = fmt.Sprintf("%s%s %s:%d:%s [%c] %s\n", prefix, dateString, event.filename, event.line, event.caller, strings.ToUpper(event.level.String())[0], event.msg)
wg.Add(2)
go func() {
defer wg.Done()
listenReadAndClose(t, l, expected)
}()
go func() {
defer wg.Done()
err := logger.LogEvent(&event)
assert.NoError(t, err)
logger.Close()
}()
wg.Wait()
logger.Flush()
logger.Close()
}

@ -1,93 +0,0 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"fmt"
"io"
"os"
"code.gitea.io/gitea/modules/json"
)
// CanColorStdout reports if we can color the Stdout
// Although we could do terminal sniffing and the like - in reality
// most tools on *nix are happy to display ansi colors.
// We will terminal sniff on Windows in console_windows.go
var CanColorStdout = true
// CanColorStderr reports if we can color the Stderr
var CanColorStderr = true
type nopWriteCloser struct {
w io.WriteCloser
}
func (n *nopWriteCloser) Write(p []byte) (int, error) {
return n.w.Write(p)
}
func (n *nopWriteCloser) Close() error {
return nil
}
// ConsoleLogger implements LoggerProvider and writes messages to terminal.
type ConsoleLogger struct {
WriterLogger
Stderr bool `json:"stderr"`
}
// NewConsoleLogger create ConsoleLogger returning as LoggerProvider.
func NewConsoleLogger() LoggerProvider {
log := &ConsoleLogger{}
log.NewWriterLogger(&nopWriteCloser{
w: os.Stdout,
})
return log
}
// Init inits connection writer with json config.
// json config only need key "level".
func (log *ConsoleLogger) Init(config string) error {
err := json.Unmarshal([]byte(config), log)
if err != nil {
return fmt.Errorf("Unable to parse JSON: %w", err)
}
if log.Stderr {
log.NewWriterLogger(&nopWriteCloser{
w: os.Stderr,
})
} else {
log.NewWriterLogger(log.out)
}
return nil
}
// Flush when log should be flushed
func (log *ConsoleLogger) Flush() {
}
// ReleaseReopen causes the console logger to reconnect to os.Stdout
func (log *ConsoleLogger) ReleaseReopen() error {
if log.Stderr {
log.NewWriterLogger(&nopWriteCloser{
w: os.Stderr,
})
} else {
log.NewWriterLogger(&nopWriteCloser{
w: os.Stdout,
})
}
return nil
}
// GetName returns the default name for this implementation
func (log *ConsoleLogger) GetName() string {
return "console"
}
func init() {
Register("console", NewConsoleLogger)
}

@ -1,137 +0,0 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"fmt"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestConsoleLoggerBadConfig(t *testing.T) {
logger := NewConsoleLogger()
err := logger.Init("{")
assert.Error(t, err)
assert.Contains(t, err.Error(), "Unable to parse JSON")
logger.Close()
}
func TestConsoleLoggerMinimalConfig(t *testing.T) {
for _, level := range Levels() {
var written []byte
var closed bool
c := CallbackWriteCloser{
callback: func(p []byte, close bool) {
written = p
closed = close
},
}
prefix := ""
flags := LstdFlags
cw := NewConsoleLogger()
realCW := cw.(*ConsoleLogger)
cw.Init(fmt.Sprintf("{\"level\":\"%s\"}", level))
nwc := realCW.out.(*nopWriteCloser)
nwc.w = c
assert.Equal(t, flags, realCW.Flags)
assert.Equal(t, FromString(level), realCW.Level)
assert.Equal(t, FromString(level), cw.GetLevel())
assert.Equal(t, prefix, realCW.Prefix)
assert.Equal(t, "", string(written))
cw.Close()
assert.False(t, closed)
}
}
func TestConsoleLogger(t *testing.T) {
var written []byte
var closed bool
c := CallbackWriteCloser{
callback: func(p []byte, close bool) {
written = p
closed = close
},
}
prefix := "TestPrefix "
level := INFO
flags := LstdFlags | LUTC | Lfuncname
cw := NewConsoleLogger()
realCW := cw.(*ConsoleLogger)
realCW.Colorize = false
nwc := realCW.out.(*nopWriteCloser)
nwc.w = c
cw.Init(fmt.Sprintf("{\"expression\":\"FILENAME\",\"prefix\":\"%s\",\"level\":\"%s\",\"flags\":%d}", prefix, level.String(), flags))
assert.Equal(t, flags, realCW.Flags)
assert.Equal(t, level, realCW.Level)
assert.Equal(t, level, cw.GetLevel())
location, _ := time.LoadLocation("EST")
date := time.Date(2019, time.January, 13, 22, 3, 30, 15, location)
dateString := date.UTC().Format("2006/01/02 15:04:05")
event := Event{
level: INFO,
msg: "TEST MSG",
caller: "CALLER",
filename: "FULL/FILENAME",
line: 1,
time: date,
}
expected := fmt.Sprintf("%s%s %s:%d:%s [%c] %s\n", prefix, dateString, event.filename, event.line, event.caller, strings.ToUpper(event.level.String())[0], event.msg)
cw.LogEvent(&event)
assert.Equal(t, expected, string(written))
assert.False(t, closed)
written = written[:0]
event.level = DEBUG
expected = ""
cw.LogEvent(&event)
assert.Equal(t, expected, string(written))
assert.False(t, closed)
event.level = TRACE
expected = ""
cw.LogEvent(&event)
assert.Equal(t, expected, string(written))
assert.False(t, closed)
nonMatchEvent := Event{
level: INFO,
msg: "TEST MSG",
caller: "CALLER",
filename: "FULL/FI_LENAME",
line: 1,
time: date,
}
event.level = INFO
expected = ""
cw.LogEvent(&nonMatchEvent)
assert.Equal(t, expected, string(written))
assert.False(t, closed)
event.level = WARN
expected = fmt.Sprintf("%s%s %s:%d:%s [%c] %s\n", prefix, dateString, event.filename, event.line, event.caller, strings.ToUpper(event.level.String())[0], event.msg)
cw.LogEvent(&event)
assert.Equal(t, expected, string(written))
assert.False(t, closed)
written = written[:0]
cw.Close()
assert.False(t, closed)
}

@ -1,61 +0,0 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import "fmt"
// ErrTimeout represents a "Timeout" kind of error.
type ErrTimeout struct {
Name string
Provider string
}
// IsErrTimeout checks if an error is a ErrTimeout.
func IsErrTimeout(err error) bool {
if err == nil {
return false
}
_, ok := err.(ErrTimeout)
return ok
}
func (err ErrTimeout) Error() string {
return fmt.Sprintf("Log Timeout for %s (%s)", err.Name, err.Provider)
}
// ErrUnknownProvider represents a "Unknown Provider" kind of error.
type ErrUnknownProvider struct {
Provider string
}
// IsErrUnknownProvider checks if an error is a ErrUnknownProvider.
func IsErrUnknownProvider(err error) bool {
if err == nil {
return false
}
_, ok := err.(ErrUnknownProvider)
return ok
}
func (err ErrUnknownProvider) Error() string {
return fmt.Sprintf("Unknown Log Provider \"%s\" (Was it registered?)", err.Provider)
}
// ErrDuplicateName represents a Duplicate Name error
type ErrDuplicateName struct {
Name string
}
// IsErrDuplicateName checks if an error is a ErrDuplicateName.
func IsErrDuplicateName(err error) bool {
if err == nil {
return false
}
_, ok := err.(ErrDuplicateName)
return ok
}
func (err ErrDuplicateName) Error() string {
return fmt.Sprintf("Duplicate named logger: %s", err.Name)
}

@ -1,460 +0,0 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"context"
"fmt"
"runtime/pprof"
"sync"
"time"
"code.gitea.io/gitea/modules/process"
)
// Event represents a logging event
type Event struct {
level Level
msg string
caller string
filename string
line int
time time.Time
stacktrace string
}
// EventLogger represents the behaviours of a logger
type EventLogger interface {
LogEvent(event *Event) error
Close()
Flush()
GetLevel() Level
GetStacktraceLevel() Level
GetName() string
ReleaseReopen() error
}
// ChannelledLog represents a cached channel to a LoggerProvider
type ChannelledLog struct {
ctx context.Context
finished context.CancelFunc
name string
provider string
queue chan *Event
loggerProvider LoggerProvider
flush chan bool
close chan bool
closed chan bool
}
// NewChannelledLog a new logger instance with given logger provider and config.
func NewChannelledLog(parent context.Context, name, provider, config string, bufferLength int64) (*ChannelledLog, error) {
if log, ok := providers[provider]; ok {
l := &ChannelledLog{
queue: make(chan *Event, bufferLength),
flush: make(chan bool),
close: make(chan bool),
closed: make(chan bool),
}
l.loggerProvider = log()
if err := l.loggerProvider.Init(config); err != nil {
return nil, err
}
l.name = name
l.provider = provider
l.ctx, _, l.finished = process.GetManager().AddTypedContext(parent, fmt.Sprintf("Logger: %s(%s)", l.name, l.provider), process.SystemProcessType, false)
go l.Start()
return l, nil
}
return nil, ErrUnknownProvider{provider}
}
// Start processing the ChannelledLog
func (l *ChannelledLog) Start() {
pprof.SetGoroutineLabels(l.ctx)
defer l.finished()
for {
select {
case event, ok := <-l.queue:
if !ok {
l.closeLogger()
return
}
l.loggerProvider.LogEvent(event) //nolint:errcheck
case _, ok := <-l.flush:
if !ok {
l.closeLogger()
return
}
l.emptyQueue()
l.loggerProvider.Flush()
case <-l.close:
l.emptyQueue()
l.closeLogger()
return
}
}
}
// LogEvent logs an event to this ChannelledLog
func (l *ChannelledLog) LogEvent(event *Event) error {
select {
case l.queue <- event:
return nil
case <-time.After(60 * time.Second):
// We're blocked!
return ErrTimeout{
Name: l.name,
Provider: l.provider,
}
}
}
func (l *ChannelledLog) emptyQueue() bool {
for {
select {
case event, ok := <-l.queue:
if !ok {
return false
}
l.loggerProvider.LogEvent(event) //nolint:errcheck
default:
return true
}
}
}
func (l *ChannelledLog) closeLogger() {
l.loggerProvider.Flush()
l.loggerProvider.Close()
l.closed <- true
}
// Close this ChannelledLog
func (l *ChannelledLog) Close() {
l.close <- true
<-l.closed
}
// Flush this ChannelledLog
func (l *ChannelledLog) Flush() {
l.flush <- true
}
// ReleaseReopen this ChannelledLog
func (l *ChannelledLog) ReleaseReopen() error {
return l.loggerProvider.ReleaseReopen()
}
// GetLevel gets the level of this ChannelledLog
func (l *ChannelledLog) GetLevel() Level {
return l.loggerProvider.GetLevel()
}
// GetStacktraceLevel gets the level of this ChannelledLog
func (l *ChannelledLog) GetStacktraceLevel() Level {
return l.loggerProvider.GetStacktraceLevel()
}
// GetName returns the name of this ChannelledLog
func (l *ChannelledLog) GetName() string {
return l.name
}
// MultiChannelledLog represents a cached channel to a LoggerProvider
type MultiChannelledLog struct {
ctx context.Context
finished context.CancelFunc
name string
bufferLength int64
queue chan *Event
rwmutex sync.RWMutex
loggers map[string]EventLogger
flush chan bool
close chan bool
started bool
level Level
stacktraceLevel Level
closed chan bool
paused chan bool
}
// NewMultiChannelledLog a new logger instance with given logger provider and config.
func NewMultiChannelledLog(name string, bufferLength int64) *MultiChannelledLog {
ctx, _, finished := process.GetManager().AddTypedContext(context.Background(), fmt.Sprintf("Logger: %s", name), process.SystemProcessType, false)
m := &MultiChannelledLog{
ctx: ctx,
finished: finished,
name: name,
queue: make(chan *Event, bufferLength),
flush: make(chan bool),
bufferLength: bufferLength,
loggers: make(map[string]EventLogger),
level: NONE,
stacktraceLevel: NONE,
close: make(chan bool),
closed: make(chan bool),
paused: make(chan bool),
}
return m
}
// AddLogger adds a logger to this MultiChannelledLog
func (m *MultiChannelledLog) AddLogger(logger EventLogger) error {
m.rwmutex.Lock()
name := logger.GetName()
if _, has := m.loggers[name]; has {
m.rwmutex.Unlock()
return ErrDuplicateName{name}
}
m.loggers[name] = logger
if logger.GetLevel() < m.level {
m.level = logger.GetLevel()
}
if logger.GetStacktraceLevel() < m.stacktraceLevel {
m.stacktraceLevel = logger.GetStacktraceLevel()
}
m.rwmutex.Unlock()
go m.Start()
return nil
}
// DelLogger removes a sub logger from this MultiChannelledLog
// NB: If you delete the last sublogger this logger will simply drop
// log events
func (m *MultiChannelledLog) DelLogger(name string) bool {
m.rwmutex.Lock()
logger, has := m.loggers[name]
if !has {
m.rwmutex.Unlock()
return false
}
delete(m.loggers, name)
m.internalResetLevel()
m.rwmutex.Unlock()
logger.Flush()
logger.Close()
return true
}
// GetEventLogger returns a sub logger from this MultiChannelledLog
func (m *MultiChannelledLog) GetEventLogger(name string) EventLogger {
m.rwmutex.RLock()
defer m.rwmutex.RUnlock()
return m.loggers[name]
}
// GetEventLoggerNames returns a list of names
func (m *MultiChannelledLog) GetEventLoggerNames() []string {
m.rwmutex.RLock()
defer m.rwmutex.RUnlock()
var keys []string
for k := range m.loggers {
keys = append(keys, k)
}
return keys
}
func (m *MultiChannelledLog) closeLoggers() {
m.rwmutex.Lock()
for _, logger := range m.loggers {
logger.Flush()
logger.Close()
}
m.rwmutex.Unlock()
m.closed <- true
}
// Pause pauses this Logger
func (m *MultiChannelledLog) Pause() {
m.paused <- true
}
// Resume resumes this Logger
func (m *MultiChannelledLog) Resume() {
m.paused <- false
}
// ReleaseReopen causes this logger to tell its subloggers to release and reopen
func (m *MultiChannelledLog) ReleaseReopen() error {
m.rwmutex.Lock()
defer m.rwmutex.Unlock()
var accumulatedErr error
for _, logger := range m.loggers {
if err := logger.ReleaseReopen(); err != nil {
if accumulatedErr == nil {
accumulatedErr = fmt.Errorf("Error whilst reopening: %s Error: %w", logger.GetName(), err)
} else {
accumulatedErr = fmt.Errorf("Error whilst reopening: %s Error: %v & %w", logger.GetName(), err, accumulatedErr)
}
}
}
return accumulatedErr
}
// Start processing the MultiChannelledLog
func (m *MultiChannelledLog) Start() {
m.rwmutex.Lock()
if m.started {
m.rwmutex.Unlock()
return
}
pprof.SetGoroutineLabels(m.ctx)
defer m.finished()
m.started = true
m.rwmutex.Unlock()
paused := false
for {
if paused {
select {
case paused = <-m.paused:
if !paused {
m.ResetLevel()
}
case _, ok := <-m.flush:
if !ok {
m.closeLoggers()
return
}
m.rwmutex.RLock()
for _, logger := range m.loggers {
logger.Flush()
}
m.rwmutex.RUnlock()
case <-m.close:
m.closeLoggers()
return
}
continue
}
select {
case paused = <-m.paused:
if paused && m.level < INFO {
m.level = INFO
}
case event, ok := <-m.queue:
if !ok {
m.closeLoggers()
return
}
m.rwmutex.RLock()
for _, logger := range m.loggers {
err := logger.LogEvent(event)
if err != nil {
fmt.Println(err) //nolint:forbidigo
}
}
m.rwmutex.RUnlock()
case _, ok := <-m.flush:
if !ok {
m.closeLoggers()
return
}
m.emptyQueue()
m.rwmutex.RLock()
for _, logger := range m.loggers {
logger.Flush()
}
m.rwmutex.RUnlock()
case <-m.close:
m.emptyQueue()
m.closeLoggers()
return
}
}
}
func (m *MultiChannelledLog) emptyQueue() bool {
for {
select {
case event, ok := <-m.queue:
if !ok {
return false
}
m.rwmutex.RLock()
for _, logger := range m.loggers {
err := logger.LogEvent(event)
if err != nil {
fmt.Println(err) //nolint:forbidigo
}
}
m.rwmutex.RUnlock()
default:
return true
}
}
}
// LogEvent logs an event to this MultiChannelledLog
func (m *MultiChannelledLog) LogEvent(event *Event) error {
select {
case m.queue <- event:
return nil
case <-time.After(100 * time.Millisecond):
// We're blocked!
return ErrTimeout{
Name: m.name,
Provider: "MultiChannelledLog",
}
}
}
// Close this MultiChannelledLog
func (m *MultiChannelledLog) Close() {
m.close <- true
<-m.closed
}
// Flush this ChannelledLog
func (m *MultiChannelledLog) Flush() {
m.flush <- true
}
// GetLevel gets the level of this MultiChannelledLog
func (m *MultiChannelledLog) GetLevel() Level {
m.rwmutex.RLock()
defer m.rwmutex.RUnlock()
return m.level
}
// GetStacktraceLevel gets the level of this MultiChannelledLog
func (m *MultiChannelledLog) GetStacktraceLevel() Level {
m.rwmutex.RLock()
defer m.rwmutex.RUnlock()
return m.stacktraceLevel
}
func (m *MultiChannelledLog) internalResetLevel() Level {
m.level = NONE
for _, logger := range m.loggers {
level := logger.GetLevel()
if level < m.level {
m.level = level
}
level = logger.GetStacktraceLevel()
if level < m.stacktraceLevel {
m.stacktraceLevel = level
}
}
return m.level
}
// ResetLevel will reset the level of this MultiChannelledLog
func (m *MultiChannelledLog) ResetLevel() Level {
m.rwmutex.Lock()
defer m.rwmutex.Unlock()
return m.internalResetLevel()
}
// GetName gets the name of this MultiChannelledLog
func (m *MultiChannelledLog) GetName() string {
return m.name
}
func (e *Event) GetMsg() string {
return e.msg
}

@ -0,0 +1,246 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"bytes"
"fmt"
"strings"
"time"
)
type Event struct {
Time time.Time
GoroutinePid string
Caller string
Filename string
Line int
Level Level
MsgSimpleText string
msgFormat string // the format and args is only valid in the caller's goroutine
msgArgs []any // they are discarded before the event is passed to the writer's channel
Stacktrace string
}
type EventFormatted struct {
Origin *Event
Msg any // the message formatted by the writer's formatter, the writer knows its type
}
type EventFormatter func(mode *WriterMode, event *Event, msgFormat string, msgArgs ...any) []byte
type logStringFormatter struct {
v LogStringer
}
var _ fmt.Formatter = logStringFormatter{}
func (l logStringFormatter) Format(f fmt.State, verb rune) {
if f.Flag('#') && verb == 'v' {
_, _ = fmt.Fprintf(f, "%#v", l.v)
return
}
_, _ = f.Write([]byte(l.v.LogString()))
}
// Copy of cheap integer to fixed-width decimal to ascii from logger.
// TODO: legacy bugs: doesn't support negative number, overflow if wid it too large.
func itoa(buf []byte, i, wid int) []byte {
var s [20]byte
bp := len(s) - 1
for i >= 10 || wid > 1 {
wid--
q := i / 10
s[bp] = byte('0' + i - q*10)
bp--
i = q
}
// i < 10
s[bp] = byte('0' + i)
return append(buf, s[bp:]...)
}
func colorSprintf(colorize bool, format string, args ...any) string {
hasColorValue := false
for _, v := range args {
if _, hasColorValue = v.(*ColoredValue); hasColorValue {
break
}
}
if colorize || !hasColorValue {
return fmt.Sprintf(format, args...)
}
noColors := make([]any, len(args))
copy(noColors, args)
for i, v := range args {
if cv, ok := v.(*ColoredValue); ok {
noColors[i] = cv.v
}
}
return fmt.Sprintf(format, noColors...)
}
// EventFormatTextMessage makes the log message for a writer with its mode. This function is a copy of the original package
func EventFormatTextMessage(mode *WriterMode, event *Event, msgFormat string, msgArgs ...any) []byte {
buf := make([]byte, 0, 1024)
buf = append(buf, mode.Prefix...)
t := event.Time
flags := mode.Flags.Bits()
if flags&(Ldate|Ltime|Lmicroseconds) != 0 {
if mode.Colorize {
buf = append(buf, fgCyanBytes...)
}
if flags&LUTC != 0 {
t = t.UTC()
}
if flags&Ldate != 0 {
year, month, day := t.Date()
buf = itoa(buf, year, 4)
buf = append(buf, '/')
buf = itoa(buf, int(month), 2)
buf = append(buf, '/')
buf = itoa(buf, day, 2)
buf = append(buf, ' ')
}
if flags&(Ltime|Lmicroseconds) != 0 {
hour, min, sec := t.Clock()
buf = itoa(buf, hour, 2)
buf = append(buf, ':')
buf = itoa(buf, min, 2)
buf = append(buf, ':')
buf = itoa(buf, sec, 2)
if flags&Lmicroseconds != 0 {
buf = append(buf, '.')
buf = itoa(buf, t.Nanosecond()/1e3, 6)
}
buf = append(buf, ' ')
}
if mode.Colorize {
buf = append(buf, resetBytes...)
}
}
if flags&(Lshortfile|Llongfile) != 0 {
if mode.Colorize {
buf = append(buf, fgGreenBytes...)
}
file := event.Filename
if flags&Lmedfile == Lmedfile {
startIndex := len(file) - 20
if startIndex > 0 {
file = "..." + file[startIndex:]
}
} else if flags&Lshortfile != 0 {
startIndex := strings.LastIndexByte(file, '/')
if startIndex > 0 && startIndex < len(file) {
file = file[startIndex+1:]
}
}
buf = append(buf, file...)
buf = append(buf, ':')
buf = itoa(buf, event.Line, -1)
if flags&(Lfuncname|Lshortfuncname) != 0 {
buf = append(buf, ':')
} else {
if mode.Colorize {
buf = append(buf, resetBytes...)
}
buf = append(buf, ' ')
}
}
if flags&(Lfuncname|Lshortfuncname) != 0 {
if mode.Colorize {
buf = append(buf, fgGreenBytes...)
}
funcname := event.Caller
if flags&Lshortfuncname != 0 {
lastIndex := strings.LastIndexByte(funcname, '.')
if lastIndex > 0 && len(funcname) > lastIndex+1 {
funcname = funcname[lastIndex+1:]
}
}
buf = append(buf, funcname...)
if mode.Colorize {
buf = append(buf, resetBytes...)
}
buf = append(buf, ' ')
}
if flags&(Llevel|Llevelinitial) != 0 {
level := strings.ToUpper(event.Level.String())
if mode.Colorize {
buf = append(buf, ColorBytes(levelToColor[event.Level]...)...)
}
buf = append(buf, '[')
if flags&Llevelinitial != 0 {
buf = append(buf, level[0])
} else {
buf = append(buf, level...)
}
buf = append(buf, ']')
if mode.Colorize {
buf = append(buf, resetBytes...)
}
buf = append(buf, ' ')
}
var msg []byte
// if the log needs colorizing, do it
if mode.Colorize && len(msgArgs) > 0 {
hasColorValue := false
for _, v := range msgArgs {
if _, hasColorValue = v.(*ColoredValue); hasColorValue {
break
}
}
if hasColorValue {
msg = []byte(fmt.Sprintf(msgFormat, msgArgs...))
}
}
// try to re-use the pre-formatted simple text message
if len(msg) == 0 {
msg = []byte(event.MsgSimpleText)
}
// if still no message, do the normal Sprintf for the message
if len(msg) == 0 {
msg = []byte(colorSprintf(mode.Colorize, msgFormat, msgArgs...))
}
// remove at most one trailing new line
if len(msg) > 0 && msg[len(msg)-1] == '\n' {
msg = msg[:len(msg)-1]
}
if flags&Lgopid == Lgopid {
if event.GoroutinePid != "" {
buf = append(buf, '[')
if mode.Colorize {
buf = append(buf, ColorBytes(FgHiYellow)...)
}
buf = append(buf, event.GoroutinePid...)
if mode.Colorize {
buf = append(buf, resetBytes...)
}
buf = append(buf, ']', ' ')
}
}
buf = append(buf, msg...)
if event.Stacktrace != "" && mode.StacktraceLevel <= event.Level {
lines := bytes.Split([]byte(event.Stacktrace), []byte("\n"))
for _, line := range lines {
buf = append(buf, "\n\t"...)
buf = append(buf, line...)
}
buf = append(buf, '\n')
}
buf = append(buf, '\n')
return buf
}

@ -0,0 +1,57 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestItoa(t *testing.T) {
b := itoa(nil, 0, 0)
assert.Equal(t, "0", string(b))
b = itoa(nil, 0, 1)
assert.Equal(t, "0", string(b))
b = itoa(nil, 0, 2)
assert.Equal(t, "00", string(b))
}
func TestEventFormatTextMessage(t *testing.T) {
res := EventFormatTextMessage(&WriterMode{Prefix: "[PREFIX] ", Colorize: false, Flags: Flags{defined: true, flags: 0xffffffff}},
&Event{
Time: time.Date(2020, 1, 2, 3, 4, 5, 6, time.UTC),
Caller: "caller",
Filename: "filename",
Line: 123,
GoroutinePid: "pid",
Level: ERROR,
Stacktrace: "stacktrace",
},
"msg format: %v %v", "arg0", NewColoredValue("arg1", FgBlue),
)
assert.Equal(t, `[PREFIX] 2020/01/02 03:04:05.000000 filename:123:caller [E] [pid] msg format: arg0 arg1
stacktrace
`, string(res))
res = EventFormatTextMessage(&WriterMode{Prefix: "[PREFIX] ", Colorize: true, Flags: Flags{defined: true, flags: 0xffffffff}},
&Event{
Time: time.Date(2020, 1, 2, 3, 4, 5, 6, time.UTC),
Caller: "caller",
Filename: "filename",
Line: 123,
GoroutinePid: "pid",
Level: ERROR,
Stacktrace: "stacktrace",
},
"msg format: %v %v", "arg0", NewColoredValue("arg1", FgBlue),
)
assert.Equal(t, "[PREFIX] \x1b[36m2020/01/02 03:04:05.000000 \x1b[0m\x1b[32mfilename:123:\x1b[32mcaller\x1b[0m \x1b[1;31m[E]\x1b[0m [\x1b[93mpid\x1b[0m] msg format: arg0 \x1b[34marg1\x1b[0m\n\tstacktrace\n\n", string(res))
}

@ -0,0 +1,54 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"fmt"
)
// EventWriter is the general interface for all event writers
// EventWriterBase is only used as its base interface
// A writer implementation could override the default EventWriterBase functions
// eg: a writer can override the Run to handle events in its own way with its own goroutine
type EventWriter interface {
EventWriterBase
}
// WriterMode is the mode for creating a new EventWriter, it contains common options for all writers
// Its WriterOption field is the specified options for a writer, it should be passed by value but not by pointer
type WriterMode struct {
BufferLen int
Level Level
Prefix string
Colorize bool
Flags Flags
Expression string
StacktraceLevel Level
WriterOption any
}
// EventWriterProvider is the function for creating a new EventWriter
type EventWriterProvider func(writerName string, writerMode WriterMode) EventWriter
var eventWriterProviders = map[string]EventWriterProvider{}
func RegisterEventWriter(writerType string, p EventWriterProvider) {
eventWriterProviders[writerType] = p
}
func HasEventWriter(writerType string) bool {
_, ok := eventWriterProviders[writerType]
return ok
}
func NewEventWriter(name, writerType string, mode WriterMode) (EventWriter, error) {
if p, ok := eventWriterProviders[writerType]; ok {
return p(name, mode), nil
}
return nil, fmt.Errorf("unknown event writer type %q for writer %q", writerType, name)
}

@ -0,0 +1,160 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"context"
"fmt"
"io"
"regexp"
"time"
)
// EventWriterBase is the base interface for most event writers
// It provides default implementations for most methods
type EventWriterBase interface {
Base() *EventWriterBaseImpl
GetWriterType() string
GetWriterName() string
GetLevel() Level
Run(ctx context.Context)
}
type EventWriterBaseImpl struct {
writerType string
Name string
Mode *WriterMode
Queue chan *EventFormatted
FormatMessage EventFormatter // format the Event to a message and write it to output
OutputWriteCloser io.WriteCloser // it will be closed when the event writer is stopped
GetPauseChan func() chan struct{}
shared bool
stopped chan struct{}
}
var _ EventWriterBase = (*EventWriterBaseImpl)(nil)
func (b *EventWriterBaseImpl) Base() *EventWriterBaseImpl {
return b
}
func (b *EventWriterBaseImpl) GetWriterType() string {
return b.writerType
}
func (b *EventWriterBaseImpl) GetWriterName() string {
return b.Name
}
func (b *EventWriterBaseImpl) GetLevel() Level {
return b.Mode.Level
}
// Run is the default implementation for EventWriter.Run
func (b *EventWriterBaseImpl) Run(ctx context.Context) {
defer b.OutputWriteCloser.Close()
var exprRegexp *regexp.Regexp
if b.Mode.Expression != "" {
var err error
if exprRegexp, err = regexp.Compile(b.Mode.Expression); err != nil {
FallbackErrorf("unable to compile expression %q for writer %q: %v", b.Mode.Expression, b.Name, err)
}
}
for {
if b.GetPauseChan != nil {
pause := b.GetPauseChan()
if pause != nil {
select {
case <-pause:
case <-ctx.Done():
return
}
}
}
select {
case <-ctx.Done():
return
case event, ok := <-b.Queue:
if !ok {
return
}
if exprRegexp != nil {
fileLineCaller := fmt.Sprintf("%s:%d:%s", event.Origin.Filename, event.Origin.Line, event.Origin.Caller)
matched := exprRegexp.Match([]byte(fileLineCaller)) || exprRegexp.Match([]byte(event.Origin.MsgSimpleText))
if !matched {
continue
}
}
var err error
switch msg := event.Msg.(type) {
case string:
_, err = b.OutputWriteCloser.Write([]byte(msg))
case []byte:
_, err = b.OutputWriteCloser.Write(msg)
case io.WriterTo:
_, err = msg.WriteTo(b.OutputWriteCloser)
default:
_, err = b.OutputWriteCloser.Write([]byte(fmt.Sprint(msg)))
}
if err != nil {
FallbackErrorf("unable to write log message of %q (%v): %v", b.Name, err, event.Msg)
}
}
}
}
func NewEventWriterBase(name, writerType string, mode WriterMode) *EventWriterBaseImpl {
if mode.BufferLen == 0 {
mode.BufferLen = 1000
}
if mode.Level == UNDEFINED {
mode.Level = INFO
}
if mode.StacktraceLevel == UNDEFINED {
mode.StacktraceLevel = NONE
}
b := &EventWriterBaseImpl{
writerType: writerType,
Name: name,
Mode: &mode,
Queue: make(chan *EventFormatted, mode.BufferLen),
GetPauseChan: GetManager().GetPauseChan, // by default, use the global pause channel
FormatMessage: EventFormatTextMessage,
}
return b
}
// eventWriterStartGo use "go" to start an event worker's Run method
func eventWriterStartGo(ctx context.Context, w EventWriter, shared bool) {
if w.Base().stopped != nil {
return // already started
}
w.Base().shared = shared
w.Base().stopped = make(chan struct{})
go func() {
defer close(w.Base().stopped)
w.Run(ctx)
}()
}
// eventWriterStopWait stops an event writer and waits for it to finish flushing (with a timeout)
func eventWriterStopWait(w EventWriter) {
close(w.Base().Queue)
select {
case <-w.Base().stopped:
case <-time.After(2 * time.Second):
FallbackErrorf("unable to stop log writer %q in time, skip", w.GetWriterName())
}
}

@ -0,0 +1,111 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"io"
"net"
)
type WriterConnOption struct {
Addr string
Protocol string
Reconnect bool
ReconnectOnMsg bool
}
type eventWriterConn struct {
*EventWriterBaseImpl
connWriter connWriter
}
var _ EventWriter = (*eventWriterConn)(nil)
func NewEventWriterConn(writerName string, writerMode WriterMode) EventWriter {
w := &eventWriterConn{EventWriterBaseImpl: NewEventWriterBase(writerName, "conn", writerMode)}
opt := writerMode.WriterOption.(WriterConnOption)
w.connWriter = connWriter{
ReconnectOnMsg: opt.ReconnectOnMsg,
Reconnect: opt.Reconnect,
Net: opt.Protocol,
Addr: opt.Addr,
}
w.OutputWriteCloser = &w.connWriter
return w
}
func init() {
RegisterEventWriter("conn", NewEventWriterConn)
}
// below is copied from old code
type connWriter struct {
innerWriter io.WriteCloser
ReconnectOnMsg bool
Reconnect bool
Net string `json:"net"`
Addr string `json:"addr"`
}
var _ io.WriteCloser = (*connWriter)(nil)
// Close the inner writer
func (i *connWriter) Close() error {
if i.innerWriter != nil {
return i.innerWriter.Close()
}
return nil
}
// Write the data to the connection
func (i *connWriter) Write(p []byte) (int, error) {
if i.neededConnectOnMsg() {
if err := i.connect(); err != nil {
return 0, err
}
}
if i.ReconnectOnMsg {
defer i.innerWriter.Close()
}
return i.innerWriter.Write(p)
}
func (i *connWriter) neededConnectOnMsg() bool {
if i.Reconnect {
i.Reconnect = false
return true
}
if i.innerWriter == nil {
return true
}
return i.ReconnectOnMsg
}
func (i *connWriter) connect() error {
if i.innerWriter != nil {
_ = i.innerWriter.Close()
i.innerWriter = nil
}
conn, err := net.Dial(i.Net, i.Addr)
if err != nil {
return err
}
if tcpConn, ok := conn.(*net.TCPConn); ok {
err = tcpConn.SetKeepAlive(true)
if err != nil {
return err
}
}
i.innerWriter = conn
return nil
}

@ -0,0 +1,75 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"context"
"fmt"
"io"
"net"
"strings"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func listenReadAndClose(t *testing.T, l net.Listener, expected string) {
conn, err := l.Accept()
assert.NoError(t, err)
defer conn.Close()
written, err := io.ReadAll(conn)
assert.NoError(t, err)
assert.Equal(t, expected, string(written))
}
func TestConnLogger(t *testing.T) {
protocol := "tcp"
address := ":3099"
l, err := net.Listen(protocol, address)
if err != nil {
t.Fatal(err)
}
defer l.Close()
prefix := "TestPrefix "
level := INFO
flags := LstdFlags | LUTC | Lfuncname
logger := NewLoggerWithWriters(context.Background(), NewEventWriterConn("test-conn", WriterMode{
Level: level,
Prefix: prefix,
Flags: FlagsFromBits(flags),
WriterOption: WriterConnOption{Addr: address, Protocol: protocol, Reconnect: true, ReconnectOnMsg: true},
}))
location, _ := time.LoadLocation("EST")
date := time.Date(2019, time.January, 13, 22, 3, 30, 15, location)
dateString := date.UTC().Format("2006/01/02 15:04:05")
event := Event{
Level: INFO,
MsgSimpleText: "TEST MSG",
Caller: "CALLER",
Filename: "FULL/FILENAME",
Line: 1,
Time: date,
}
expected := fmt.Sprintf("%s%s %s:%d:%s [%c] %s\n", prefix, dateString, event.Filename, event.Line, event.Caller, strings.ToUpper(event.Level.String())[0], event.MsgSimpleText)
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
listenReadAndClose(t, l, expected)
}()
logger.SendLogEvent(&event)
wg.Wait()
logger.Close()
}

@ -0,0 +1,40 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"io"
"os"
)
type WriterConsoleOption struct {
Stderr bool
}
type eventWriterConsole struct {
*EventWriterBaseImpl
}
var _ EventWriter = (*eventWriterConsole)(nil)
type nopCloser struct {
io.Writer
}
func (nopCloser) Close() error { return nil }
func NewEventWriterConsole(name string, mode WriterMode) EventWriter {
w := &eventWriterConsole{EventWriterBaseImpl: NewEventWriterBase(name, "console", mode)}
opt := mode.WriterOption.(WriterConsoleOption)
if opt.Stderr {
w.OutputWriteCloser = nopCloser{os.Stderr}
} else {
w.OutputWriteCloser = nopCloser{os.Stdout}
}
return w
}
func init() {
RegisterEventWriter("console", NewEventWriterConsole)
}

@ -0,0 +1,48 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"code.gitea.io/gitea/modules/util/rotatingfilewriter"
)
type WriterFileOption struct {
FileName string
MaxSize int64
LogRotate bool
DailyRotate bool
MaxDays int
Compress bool
CompressionLevel int
}
type eventWriterFile struct {
*EventWriterBaseImpl
fileWriter *rotatingfilewriter.RotatingFileWriter
}
var _ EventWriter = (*eventWriterFile)(nil)
func NewEventWriterFile(name string, mode WriterMode) EventWriter {
w := &eventWriterFile{EventWriterBaseImpl: NewEventWriterBase(name, "file", mode)}
opt := mode.WriterOption.(WriterFileOption)
var err error
w.fileWriter, err = rotatingfilewriter.Open(opt.FileName, &rotatingfilewriter.Options{
Rotate: opt.LogRotate,
MaximumSize: opt.MaxSize,
RotateDaily: opt.DailyRotate,
KeepDays: opt.MaxDays,
Compress: opt.Compress,
CompressionLevel: opt.CompressionLevel,
})
if err != nil {
FallbackErrorf("unable to open log file %q: %v", opt.FileName, err)
}
w.OutputWriteCloser = w.fileWriter
return w
}
func init() {
RegisterEventWriter("file", NewEventWriterFile)
}

@ -1,283 +0,0 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"bufio"
"compress/gzip"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"time"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/util"
)
// FileLogger implements LoggerProvider.
// It writes messages by lines limit, file size limit, or time frequency.
type FileLogger struct {
WriterLogger
mw *MuxWriter
// The opened file
Filename string `json:"filename"`
// Rotate at size
Maxsize int `json:"maxsize"`
maxsizeCursize int
// Rotate daily
Daily bool `json:"daily"`
Maxdays int64 `json:"maxdays"`
dailyOpenDate int
Rotate bool `json:"rotate"`
Compress bool `json:"compress"`
CompressionLevel int `json:"compressionLevel"`
startLock sync.Mutex // Only one log can write to the file
}
// MuxWriter an *os.File writer with locker.
type MuxWriter struct {
mu sync.Mutex
fd *os.File
owner *FileLogger
}
// Write writes to os.File.
func (mw *MuxWriter) Write(b []byte) (int, error) {
mw.mu.Lock()
defer mw.mu.Unlock()
mw.owner.docheck(len(b))
return mw.fd.Write(b)
}
// Close the internal writer
func (mw *MuxWriter) Close() error {
return mw.fd.Close()
}
// SetFd sets os.File in writer.
func (mw *MuxWriter) SetFd(fd *os.File) {
if mw.fd != nil {
mw.fd.Close()
}
mw.fd = fd
}
// NewFileLogger create a FileLogger returning as LoggerProvider.
func NewFileLogger() LoggerProvider {
log := &FileLogger{
Filename: "",
Maxsize: 1 << 28, // 256 MB
Daily: true,
Maxdays: 7,
Rotate: true,
Compress: true,
CompressionLevel: gzip.DefaultCompression,
}
log.Level = TRACE
// use MuxWriter instead direct use os.File for lock write when rotate
log.mw = new(MuxWriter)
log.mw.owner = log
return log
}
// Init file logger with json config.
// config like:
//
// {
// "filename":"log/gogs.log",
// "maxsize":1<<30,
// "daily":true,
// "maxdays":15,
// "rotate":true
// }
func (log *FileLogger) Init(config string) error {
if err := json.Unmarshal([]byte(config), log); err != nil {
return fmt.Errorf("Unable to parse JSON: %w", err)
}
if len(log.Filename) == 0 {
return errors.New("config must have filename")
}
// set MuxWriter as Logger's io.Writer
log.NewWriterLogger(log.mw)
return log.StartLogger()
}
// StartLogger start file logger. create log file and set to locker-inside file writer.
func (log *FileLogger) StartLogger() error {
fd, err := log.createLogFile()
if err != nil {
return err
}
log.mw.SetFd(fd)
return log.initFd()
}
func (log *FileLogger) docheck(size int) {
log.startLock.Lock()
defer log.startLock.Unlock()
if log.Rotate && ((log.Maxsize > 0 && log.maxsizeCursize >= log.Maxsize) ||
(log.Daily && time.Now().Day() != log.dailyOpenDate)) {
if err := log.DoRotate(); err != nil {
fmt.Fprintf(os.Stderr, "FileLogger(%q): %s\n", log.Filename, err)
return
}
}
log.maxsizeCursize += size
}
func (log *FileLogger) createLogFile() (*os.File, error) {
// Open the log file
return os.OpenFile(log.Filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0o660)
}
func (log *FileLogger) initFd() error {
fd := log.mw.fd
finfo, err := fd.Stat()
if err != nil {
return fmt.Errorf("get stat: %w", err)
}
log.maxsizeCursize = int(finfo.Size())
log.dailyOpenDate = time.Now().Day()
return nil
}
// DoRotate means it need to write file in new file.
// new file name like xx.log.2013-01-01.2
func (log *FileLogger) DoRotate() error {
_, err := os.Lstat(log.Filename)
if err == nil { // file exists
// Find the next available number
num := 1
fname := ""
for ; err == nil && num <= 999; num++ {
fname = log.Filename + fmt.Sprintf(".%s.%03d", time.Now().Format("2006-01-02"), num)
_, err = os.Lstat(fname)
if log.Compress && err != nil {
_, err = os.Lstat(fname + ".gz")
}
}
// return error if the last file checked still existed
if err == nil {
return fmt.Errorf("rotate: cannot find free log number to rename %s", log.Filename)
}
fd := log.mw.fd
fd.Close()
// close fd before rename
// Rename the file to its newfound home
if err = util.Rename(log.Filename, fname); err != nil {
return fmt.Errorf("Rotate: %w", err)
}
if log.Compress {
go compressOldLogFile(fname, log.CompressionLevel) //nolint:errcheck
}
// re-start logger
if err = log.StartLogger(); err != nil {
return fmt.Errorf("Rotate StartLogger: %w", err)
}
go log.deleteOldLog()
}
return nil
}
func compressOldLogFile(fname string, compressionLevel int) error {
reader, err := os.Open(fname)
if err != nil {
return err
}
defer reader.Close()
buffer := bufio.NewReader(reader)
fw, err := os.OpenFile(fname+".gz", os.O_WRONLY|os.O_CREATE, 0o660)
if err != nil {
return err
}
defer fw.Close()
zw, err := gzip.NewWriterLevel(fw, compressionLevel)
if err != nil {
return err
}
defer zw.Close()
_, err = buffer.WriteTo(zw)
if err != nil {
zw.Close()
fw.Close()
util.Remove(fname + ".gz") //nolint:errcheck
return err
}
reader.Close()
return util.Remove(fname)
}
func (log *FileLogger) deleteOldLog() {
dir := filepath.Dir(log.Filename)
_ = filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) (returnErr error) {
defer func() {
if r := recover(); r != nil {
returnErr = fmt.Errorf("Unable to delete old log '%s', error: %+v", path, r)
}
}()
if err != nil {
return err
}
if d.IsDir() {
return nil
}
info, err := d.Info()
if err != nil {
return err
}
if info.ModTime().Unix() < (time.Now().Unix() - 60*60*24*log.Maxdays) {
if strings.HasPrefix(filepath.Base(path), filepath.Base(log.Filename)) {
if err := util.Remove(path); err != nil {
returnErr = fmt.Errorf("Failed to remove %s: %w", path, err)
}
}
}
return returnErr
})
}
// Flush flush file logger.
// there are no buffering messages in file logger in memory.
// flush file means sync file from disk.
func (log *FileLogger) Flush() {
_ = log.mw.fd.Sync()
}
// ReleaseReopen releases and reopens log files
func (log *FileLogger) ReleaseReopen() error {
closingErr := log.mw.fd.Close()
startingErr := log.StartLogger()
if startingErr != nil {
if closingErr != nil {
return fmt.Errorf("Error during closing: %v Error during starting: %v", closingErr, startingErr)
}
return startingErr
}
return closingErr
}
// GetName returns the default name for this implementation
func (log *FileLogger) GetName() string {
return "file"
}
func init() {
Register("file", NewFileLogger)
}

@ -1,235 +0,0 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"compress/gzip"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestFileLoggerFails(t *testing.T) {
tmpDir := t.TempDir()
prefix := "TestPrefix "
level := INFO
flags := LstdFlags | LUTC | Lfuncname
// filename := filepath.Join(tmpDir, "test.log")
fileLogger := NewFileLogger()
// realFileLogger, ok := fileLogger.(*FileLogger)
// assert.True(t, ok)
// Fail if there is bad json
err := fileLogger.Init("{")
assert.Error(t, err)
// Fail if there is no filename
err = fileLogger.Init(fmt.Sprintf("{\"prefix\":\"%s\",\"level\":\"%s\",\"flags\":%d,\"filename\":\"%s\"}", prefix, level.String(), flags, ""))
assert.Error(t, err)
// Fail if the file isn't a filename
err = fileLogger.Init(fmt.Sprintf("{\"prefix\":\"%s\",\"level\":\"%s\",\"flags\":%d,\"filename\":\"%s\"}", prefix, level.String(), flags, filepath.ToSlash(tmpDir)))
assert.Error(t, err)
}
func TestFileLogger(t *testing.T) {
tmpDir := t.TempDir()
prefix := "TestPrefix "
level := INFO
flags := LstdFlags | LUTC | Lfuncname
filename := filepath.Join(tmpDir, "test.log")
fileLogger := NewFileLogger()
realFileLogger, ok := fileLogger.(*FileLogger)
assert.True(t, ok)
location, _ := time.LoadLocation("EST")
date := time.Date(2019, time.January, 13, 22, 3, 30, 15, location)
dateString := date.UTC().Format("2006/01/02 15:04:05")
event := Event{
level: INFO,
msg: "TEST MSG",
caller: "CALLER",
filename: "FULL/FILENAME",
line: 1,
time: date,
}
expected := fmt.Sprintf("%s%s %s:%d:%s [%c] %s\n", prefix, dateString, event.filename, event.line, event.caller, strings.ToUpper(event.level.String())[0], event.msg)
fileLogger.Init(fmt.Sprintf("{\"prefix\":\"%s\",\"level\":\"%s\",\"flags\":%d,\"filename\":\"%s\",\"maxsize\":%d,\"compress\":false}", prefix, level.String(), flags, filepath.ToSlash(filename), len(expected)*2))
assert.Equal(t, flags, realFileLogger.Flags)
assert.Equal(t, level, realFileLogger.Level)
assert.Equal(t, level, fileLogger.GetLevel())
fileLogger.LogEvent(&event)
fileLogger.Flush()
logData, err := os.ReadFile(filename)
assert.NoError(t, err)
assert.Equal(t, expected, string(logData))
event.level = DEBUG
fileLogger.LogEvent(&event)
fileLogger.Flush()
logData, err = os.ReadFile(filename)
assert.NoError(t, err)
assert.Equal(t, expected, string(logData))
event.level = TRACE
fileLogger.LogEvent(&event)
fileLogger.Flush()
logData, err = os.ReadFile(filename)
assert.NoError(t, err)
assert.Equal(t, expected, string(logData))
event.level = WARN
expected += fmt.Sprintf("%s%s %s:%d:%s [%c] %s\n", prefix, dateString, event.filename, event.line, event.caller, strings.ToUpper(event.level.String())[0], event.msg)
fileLogger.LogEvent(&event)
fileLogger.Flush()
logData, err = os.ReadFile(filename)
assert.NoError(t, err)
assert.Equal(t, expected, string(logData))
// Should rotate
fileLogger.LogEvent(&event)
fileLogger.Flush()
logData, err = os.ReadFile(filename + fmt.Sprintf(".%s.%03d", time.Now().Format("2006-01-02"), 1))
assert.NoError(t, err)
assert.Equal(t, expected, string(logData))
logData, err = os.ReadFile(filename)
assert.NoError(t, err)
expected = fmt.Sprintf("%s%s %s:%d:%s [%c] %s\n", prefix, dateString, event.filename, event.line, event.caller, strings.ToUpper(event.level.String())[0], event.msg)
assert.Equal(t, expected, string(logData))
for num := 2; num <= 999; num++ {
file, err := os.OpenFile(filename+fmt.Sprintf(".%s.%03d", time.Now().Format("2006-01-02"), num), os.O_RDONLY|os.O_CREATE, 0o666)
assert.NoError(t, err)
file.Close()
}
err = realFileLogger.DoRotate()
assert.Error(t, err)
expected += fmt.Sprintf("%s%s %s:%d:%s [%c] %s\n", prefix, dateString, event.filename, event.line, event.caller, strings.ToUpper(event.level.String())[0], event.msg)
fileLogger.LogEvent(&event)
fileLogger.Flush()
logData, err = os.ReadFile(filename)
assert.NoError(t, err)
assert.Equal(t, expected, string(logData))
// Should fail to rotate
expected += fmt.Sprintf("%s%s %s:%d:%s [%c] %s\n", prefix, dateString, event.filename, event.line, event.caller, strings.ToUpper(event.level.String())[0], event.msg)
fileLogger.LogEvent(&event)
fileLogger.Flush()
logData, err = os.ReadFile(filename)
assert.NoError(t, err)
assert.Equal(t, expected, string(logData))
fileLogger.Close()
}
func TestCompressFileLogger(t *testing.T) {
tmpDir := t.TempDir()
prefix := "TestPrefix "
level := INFO
flags := LstdFlags | LUTC | Lfuncname
filename := filepath.Join(tmpDir, "test.log")
fileLogger := NewFileLogger()
realFileLogger, ok := fileLogger.(*FileLogger)
assert.True(t, ok)
location, _ := time.LoadLocation("EST")
date := time.Date(2019, time.January, 13, 22, 3, 30, 15, location)
dateString := date.UTC().Format("2006/01/02 15:04:05")
event := Event{
level: INFO,
msg: "TEST MSG",
caller: "CALLER",
filename: "FULL/FILENAME",
line: 1,
time: date,
}
expected := fmt.Sprintf("%s%s %s:%d:%s [%c] %s\n", prefix, dateString, event.filename, event.line, event.caller, strings.ToUpper(event.level.String())[0], event.msg)
fileLogger.Init(fmt.Sprintf("{\"prefix\":\"%s\",\"level\":\"%s\",\"flags\":%d,\"filename\":\"%s\",\"maxsize\":%d,\"compress\":true}", prefix, level.String(), flags, filepath.ToSlash(filename), len(expected)*2))
fileLogger.LogEvent(&event)
fileLogger.Flush()
logData, err := os.ReadFile(filename)
assert.NoError(t, err)
assert.Equal(t, expected, string(logData))
event.level = WARN
expected += fmt.Sprintf("%s%s %s:%d:%s [%c] %s\n", prefix, dateString, event.filename, event.line, event.caller, strings.ToUpper(event.level.String())[0], event.msg)
fileLogger.LogEvent(&event)
fileLogger.Flush()
logData, err = os.ReadFile(filename)
assert.NoError(t, err)
assert.Equal(t, expected, string(logData))
// Should rotate
fileLogger.LogEvent(&event)
fileLogger.Flush()
for num := 2; num <= 999; num++ {
file, err := os.OpenFile(filename+fmt.Sprintf(".%s.%03d.gz", time.Now().Format("2006-01-02"), num), os.O_RDONLY|os.O_CREATE, 0o666)
assert.NoError(t, err)
file.Close()
}
err = realFileLogger.DoRotate()
assert.Error(t, err)
}
func TestCompressOldFile(t *testing.T) {
tmpDir := t.TempDir()
fname := filepath.Join(tmpDir, "test")
nonGzip := filepath.Join(tmpDir, "test-nonGzip")
f, err := os.OpenFile(fname, os.O_CREATE|os.O_WRONLY, 0o660)
assert.NoError(t, err)
ng, err := os.OpenFile(nonGzip, os.O_CREATE|os.O_WRONLY, 0o660)
assert.NoError(t, err)
for i := 0; i < 999; i++ {
f.WriteString("This is a test file\n")
ng.WriteString("This is a test file\n")
}
f.Close()
ng.Close()
err = compressOldLogFile(fname, -1)
assert.NoError(t, err)
_, err = os.Lstat(fname + ".gz")
assert.NoError(t, err)
f, err = os.Open(fname + ".gz")
assert.NoError(t, err)
zr, err := gzip.NewReader(f)
assert.NoError(t, err)
data, err := io.ReadAll(zr)
assert.NoError(t, err)
original, err := os.ReadFile(nonGzip)
assert.NoError(t, err)
assert.Equal(t, original, data)
}

@ -1,9 +1,14 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import "strings"
import (
"sort"
"strings"
"code.gitea.io/gitea/modules/json"
)
// These flags define which text to prefix to each log entry generated
// by the Logger. Bits are or'ed together to control what's printed.
@ -15,7 +20,7 @@ import "strings"
// The standard is:
// 2009/01/23 01:23:23 ...a/logger/c/d.go:23:runtime.Caller() [I]: message
const (
Ldate = 1 << iota // the date in the local time zone: 2009/01/23
Ldate uint32 = 1 << iota // the date in the local time zone: 2009/01/23
Ltime // the time in the local time zone: 01:23:23
Lmicroseconds // microsecond resolution: 01:23:23.123123. assumes Ltime.
Llongfile // full file name and line number: /a/logger/c/d.go:23
@ -23,18 +28,22 @@ const (
Lfuncname // function name of the caller: runtime.Caller()
Lshortfuncname // last part of the function name
LUTC // if Ldate or Ltime is set, use UTC rather than the local time zone
Llevelinitial // Initial character of the provided level in brackets eg. [I] for info
Llevelinitial // Initial character of the provided level in brackets, eg. [I] for info
Llevel // Provided level in brackets [INFO]
Lgopid // the Goroutine-PID of the context
// Last 20 characters of the filename
Lmedfile = Lshortfile | Llongfile
// LstdFlags is the initial value for the standard logger
LstdFlags = Ldate | Ltime | Lmedfile | Lshortfuncname | Llevelinitial
Lmedfile = Lshortfile | Llongfile // last 20 characters of the filename
LstdFlags = Ldate | Ltime | Lmedfile | Lshortfuncname | Llevelinitial // default
)
var flagFromString = map[string]int{
"none": 0,
const Ldefault = LstdFlags
type Flags struct {
defined bool
flags uint32
}
var flagFromString = map[string]uint32{
"date": Ldate,
"time": Ltime,
"microseconds": Lmicroseconds,
@ -45,22 +54,81 @@ var flagFromString = map[string]int{
"utc": LUTC,
"levelinitial": Llevelinitial,
"level": Llevel,
"gopid": Lgopid,
"medfile": Lmedfile,
"stdflags": LstdFlags,
}
// FlagsFromString takes a comma separated list of flags and returns
// the flags for this string
func FlagsFromString(from string) int {
flags := 0
for _, flag := range strings.Split(strings.ToLower(from), ",") {
f, ok := flagFromString[strings.TrimSpace(flag)]
if ok {
flags |= f
var flagComboToString = []struct {
flag uint32
name string
}{
// name with more bits comes first
{LstdFlags, "stdflags"},
{Lmedfile, "medfile"},
{Ldate, "date"},
{Ltime, "time"},
{Lmicroseconds, "microseconds"},
{Llongfile, "longfile"},
{Lshortfile, "shortfile"},
{Lfuncname, "funcname"},
{Lshortfuncname, "shortfuncname"},
{LUTC, "utc"},
{Llevelinitial, "levelinitial"},
{Llevel, "level"},
{Lgopid, "gopid"},
}
func (f Flags) Bits() uint32 {
if !f.defined {
return Ldefault
}
return f.flags
}
func (f Flags) String() string {
flags := f.Bits()
var flagNames []string
for _, it := range flagComboToString {
if flags&it.flag == it.flag {
flags &^= it.flag
flagNames = append(flagNames, it.name)
}
}
if len(flagNames) == 0 {
return "none"
}
if flags == 0 {
return -1
sort.Strings(flagNames)
return strings.Join(flagNames, ",")
}
func (f *Flags) UnmarshalJSON(bytes []byte) error {
var s string
if err := json.Unmarshal(bytes, &s); err != nil {
return err
}
return flags
*f = FlagsFromString(s)
return nil
}
func (f Flags) MarshalJSON() ([]byte, error) {
return []byte(`"` + f.String() + `"`), nil
}
func FlagsFromString(from string, def ...uint32) Flags {
from = strings.TrimSpace(from)
if from == "" && len(def) > 0 {
return Flags{defined: true, flags: def[0]}
}
flags := uint32(0)
for _, flag := range strings.Split(strings.ToLower(from), ",") {
flags |= flagFromString[strings.TrimSpace(flag)]
}
return Flags{defined: true, flags: flags}
}
func FlagsFromBits(flags uint32) Flags {
return Flags{defined: true, flags: flags}
}

@ -0,0 +1,30 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"testing"
"code.gitea.io/gitea/modules/json"
"github.com/stretchr/testify/assert"
)
func TestFlags(t *testing.T) {
assert.EqualValues(t, Ldefault, Flags{}.Bits())
assert.EqualValues(t, 0, FlagsFromString("").Bits())
assert.EqualValues(t, Lgopid, FlagsFromString("", Lgopid).Bits())
assert.EqualValues(t, 0, FlagsFromString("none", Lgopid).Bits())
assert.EqualValues(t, Ldate|Ltime, FlagsFromString("date,time", Lgopid).Bits())
assert.EqualValues(t, "stdflags", FlagsFromString("stdflags").String())
assert.EqualValues(t, "medfile", FlagsFromString("medfile").String())
bs, err := json.Marshal(FlagsFromString("utc,level"))
assert.NoError(t, err)
assert.EqualValues(t, `"level,utc"`, string(bs))
var flags Flags
assert.NoError(t, json.Unmarshal(bs, &flags))
assert.EqualValues(t, LUTC|Llevel, flags.Bits())
}

@ -0,0 +1,35 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"runtime"
"strings"
"code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/util/rotatingfilewriter"
)
var projectPackagePrefix string
func init() {
_, filename, _, _ := runtime.Caller(0)
projectPackagePrefix = strings.TrimSuffix(filename, "modules/log/init.go")
if projectPackagePrefix == filename {
// in case the source code file is moved, we can not trim the suffix, the code above should also be updated.
panic("unable to detect correct package prefix, please update file: " + filename)
}
rotatingfilewriter.ErrorPrintf = FallbackErrorf
process.Trace = func(start bool, pid process.IDType, description string, parentPID process.IDType, typ string) {
if start && parentPID != "" {
Log(1, TRACE, "Start %s: %s (from %s) (%s)", NewColoredValue(pid, FgHiYellow), description, NewColoredValue(parentPID, FgYellow), NewColoredValue(typ, Reset))
} else if start {
Log(1, TRACE, "Start %s: %s (%s)", NewColoredValue(pid, FgHiYellow), description, NewColoredValue(typ, Reset))
} else {
Log(1, TRACE, "Done %s: %s", NewColoredValue(pid, FgHiYellow), NewColoredValue(description, Reset))
}
}
}

@ -5,8 +5,6 @@ package log
import (
"bytes"
"fmt"
"os"
"strings"
"code.gitea.io/gitea/modules/json"
@ -16,53 +14,53 @@ import (
type Level int
const (
// TRACE represents the lowest log level
TRACE Level = iota
// DEBUG is for debug logging
UNDEFINED Level = iota
TRACE
DEBUG
// INFO is for information
INFO
// WARN is for warning information
WARN
// ERROR is for error reporting
ERROR
// CRITICAL is for critical errors
CRITICAL
// FATAL is for fatal errors
FATAL
// NONE is for no logging
NONE
)
const CRITICAL = ERROR // most logger frameworks doesn't support CRITICAL, and it doesn't seem useful
var toString = map[Level]string{
UNDEFINED: "undefined",
TRACE: "trace",
DEBUG: "debug",
INFO: "info",
WARN: "warn",
ERROR: "error",
CRITICAL: "critical",
FATAL: "fatal",
NONE: "none",
}
var toLevel = map[string]Level{
"undefined": UNDEFINED,
"trace": TRACE,
"debug": DEBUG,
"info": INFO,
"warn": WARN,
"warning": WARN,
"error": ERROR,
"critical": CRITICAL,
"fatal": FATAL,
"none": NONE,
}
// Levels returns all the possible logging levels
func Levels() []string {
keys := make([]string, 0)
for key := range toLevel {
keys = append(keys, key)
}
return keys
var levelToColor = map[Level][]ColorAttribute{
TRACE: {Bold, FgCyan},
DEBUG: {Bold, FgBlue},
INFO: {Bold, FgGreen},
WARN: {Bold, FgYellow},
ERROR: {Bold, FgRed},
FATAL: {Bold, BgRed},
NONE: {Reset},
}
func (l Level) String() string {
@ -73,14 +71,13 @@ func (l Level) String() string {
return "info"
}
// Color returns the color string for this Level
func (l Level) Color() *[]byte {
func (l Level) ColorAttributes() []ColorAttribute {
color, ok := levelToColor[l]
if ok {
return &(color)
return color
}
none := levelToColor[NONE]
return &none
return none
}
// MarshalJSON takes a Level and turns it into text
@ -91,31 +88,29 @@ func (l Level) MarshalJSON() ([]byte, error) {
return buffer.Bytes(), nil
}
// FromString takes a level string and returns a Level
func FromString(level string) Level {
temp, ok := toLevel[strings.ToLower(level)]
if !ok {
return INFO
}
return temp
}
// UnmarshalJSON takes text and turns it into a Level
func (l *Level) UnmarshalJSON(b []byte) error {
var tmp interface{}
var tmp any
err := json.Unmarshal(b, &tmp)
if err != nil {
fmt.Fprintf(os.Stderr, "Err: %v", err)
return err
}
switch v := tmp.(type) {
case string:
*l = FromString(v)
*l = LevelFromString(v)
case int:
*l = FromString(Level(v).String())
*l = LevelFromString(Level(v).String())
default:
*l = INFO
}
return nil
}
// LevelFromString takes a level string and returns a Level
func LevelFromString(level string) Level {
if l, ok := toLevel[strings.ToLower(level)]; ok {
return l
}
return INFO
}

@ -1,305 +0,0 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"fmt"
"os"
"runtime"
"strings"
"sync"
"code.gitea.io/gitea/modules/process"
)
type loggerMap struct {
sync.Map
}
func (m *loggerMap) Load(k string) (*MultiChannelledLogger, bool) {
v, ok := m.Map.Load(k)
if !ok {
return nil, false
}
l, ok := v.(*MultiChannelledLogger)
return l, ok
}
func (m *loggerMap) Store(k string, v *MultiChannelledLogger) {
m.Map.Store(k, v)
}
func (m *loggerMap) Delete(k string) {
m.Map.Delete(k)
}
var (
// DEFAULT is the name of the default logger
DEFAULT = "default"
// NamedLoggers map of named loggers
NamedLoggers loggerMap
prefix string
)
// NewLogger create a logger for the default logger
func NewLogger(bufLen int64, name, provider, config string) *MultiChannelledLogger {
err := NewNamedLogger(DEFAULT, bufLen, name, provider, config)
if err != nil {
CriticalWithSkip(1, "Unable to create default logger: %v", err)
panic(err)
}
l, _ := NamedLoggers.Load(DEFAULT)
return l
}
// NewNamedLogger creates a new named logger for a given configuration
func NewNamedLogger(name string, bufLen int64, subname, provider, config string) error {
logger, ok := NamedLoggers.Load(name)
if !ok {
logger = newLogger(name, bufLen)
NamedLoggers.Store(name, logger)
}
return logger.SetLogger(subname, provider, config)
}
// DelNamedLogger closes and deletes the named logger
func DelNamedLogger(name string) {
l, ok := NamedLoggers.Load(name)
if ok {
NamedLoggers.Delete(name)
l.Close()
}
}
// DelLogger removes the named sublogger from the default logger
func DelLogger(name string) error {
logger, _ := NamedLoggers.Load(DEFAULT)
found, err := logger.DelLogger(name)
if !found {
Trace("Log %s not found, no need to delete", name)
}
return err
}
// GetLogger returns either a named logger or the default logger
func GetLogger(name string) *MultiChannelledLogger {
logger, ok := NamedLoggers.Load(name)
if ok {
return logger
}
logger, _ = NamedLoggers.Load(DEFAULT)
return logger
}
// GetLevel returns the minimum logger level
func GetLevel() Level {
l, _ := NamedLoggers.Load(DEFAULT)
return l.GetLevel()
}
// GetStacktraceLevel returns the minimum logger level
func GetStacktraceLevel() Level {
l, _ := NamedLoggers.Load(DEFAULT)
return l.GetStacktraceLevel()
}
// Trace records trace log
func Trace(format string, v ...interface{}) {
Log(1, TRACE, format, v...)
}
// IsTrace returns true if at least one logger is TRACE
func IsTrace() bool {
return GetLevel() <= TRACE
}
// Debug records debug log
func Debug(format string, v ...interface{}) {
Log(1, DEBUG, format, v...)
}
// IsDebug returns true if at least one logger is DEBUG
func IsDebug() bool {
return GetLevel() <= DEBUG
}
// Info records info log
func Info(format string, v ...interface{}) {
Log(1, INFO, format, v...)
}
// IsInfo returns true if at least one logger is INFO
func IsInfo() bool {
return GetLevel() <= INFO
}
// Warn records warning log
func Warn(format string, v ...interface{}) {
Log(1, WARN, format, v...)
}
// IsWarn returns true if at least one logger is WARN
func IsWarn() bool {
return GetLevel() <= WARN
}
// Error records error log
func Error(format string, v ...interface{}) {
Log(1, ERROR, format, v...)
}
// ErrorWithSkip records error log from "skip" calls back from this function
func ErrorWithSkip(skip int, format string, v ...interface{}) {
Log(skip+1, ERROR, format, v...)
}
// IsError returns true if at least one logger is ERROR
func IsError() bool {
return GetLevel() <= ERROR
}
// Critical records critical log
func Critical(format string, v ...interface{}) {
Log(1, CRITICAL, format, v...)
}
// CriticalWithSkip records critical log from "skip" calls back from this function
func CriticalWithSkip(skip int, format string, v ...interface{}) {
Log(skip+1, CRITICAL, format, v...)
}
// IsCritical returns true if at least one logger is CRITICAL
func IsCritical() bool {
return GetLevel() <= CRITICAL
}
// Fatal records fatal log and exit process
func Fatal(format string, v ...interface{}) {
Log(1, FATAL, format, v...)
Close()
os.Exit(1)
}
// FatalWithSkip records fatal log from "skip" calls back from this function
func FatalWithSkip(skip int, format string, v ...interface{}) {
Log(skip+1, FATAL, format, v...)
Close()
os.Exit(1)
}
// IsFatal returns true if at least one logger is FATAL
func IsFatal() bool {
return GetLevel() <= FATAL
}
// Pause pauses all the loggers
func Pause() {
NamedLoggers.Range(func(key, value interface{}) bool {
logger := value.(*MultiChannelledLogger)
logger.Pause()
logger.Flush()
return true
})
}
// Resume resumes all the loggers
func Resume() {
NamedLoggers.Range(func(key, value interface{}) bool {
logger := value.(*MultiChannelledLogger)
logger.Resume()
return true
})
}
// ReleaseReopen releases and reopens logging files
func ReleaseReopen() error {
var accumulatedErr error
NamedLoggers.Range(func(key, value interface{}) bool {
logger := value.(*MultiChannelledLogger)
if err := logger.ReleaseReopen(); err != nil {
if accumulatedErr == nil {
accumulatedErr = fmt.Errorf("Error reopening %s: %w", key.(string), err)
} else {
accumulatedErr = fmt.Errorf("Error reopening %s: %v & %w", key.(string), err, accumulatedErr)
}
}
return true
})
return accumulatedErr
}
// Close closes all the loggers
func Close() {
l, ok := NamedLoggers.Load(DEFAULT)
if !ok {
return
}
NamedLoggers.Delete(DEFAULT)
l.Close()
}
// Log a message with defined skip and at logging level
// A skip of 0 refers to the caller of this command
func Log(skip int, level Level, format string, v ...interface{}) {
l, ok := NamedLoggers.Load(DEFAULT)
if ok {
l.Log(skip+1, level, format, v...) //nolint:errcheck
}
}
// LoggerAsWriter is a io.Writer shim around the gitea log
type LoggerAsWriter struct {
ourLoggers []*MultiChannelledLogger
level Level
}
// NewLoggerAsWriter creates a Writer representation of the logger with setable log level
func NewLoggerAsWriter(level string, ourLoggers ...*MultiChannelledLogger) *LoggerAsWriter {
if len(ourLoggers) == 0 {
l, _ := NamedLoggers.Load(DEFAULT)
ourLoggers = []*MultiChannelledLogger{l}
}
l := &LoggerAsWriter{
ourLoggers: ourLoggers,
level: FromString(level),
}
return l
}
// Write implements the io.Writer interface to allow spoofing of chi
func (l *LoggerAsWriter) Write(p []byte) (int, error) {
for _, logger := range l.ourLoggers {
// Skip = 3 because this presumes that we have been called by log.Println()
// If the caller has used log.Output or the like this will be wrong
logger.Log(3, l.level, string(p)) //nolint:errcheck
}
return len(p), nil
}
// Log takes a given string and logs it at the set log-level
func (l *LoggerAsWriter) Log(msg string) {
for _, logger := range l.ourLoggers {
// Set the skip to reference the call just above this
_ = logger.Log(1, l.level, msg)
}
}
func init() {
process.Trace = func(start bool, pid process.IDType, description string, parentPID process.IDType, typ string) {
if start && parentPID != "" {
Log(1, TRACE, "Start %s: %s (from %s) (%s)", NewColoredValue(pid, FgHiYellow), description, NewColoredValue(parentPID, FgYellow), NewColoredValue(typ, Reset))
} else if start {
Log(1, TRACE, "Start %s: %s (%s)", NewColoredValue(pid, FgHiYellow), description, NewColoredValue(typ, Reset))
} else {
Log(1, TRACE, "Done %s: %s", NewColoredValue(pid, FgHiYellow), NewColoredValue(description, Reset))
}
}
_, filename, _, _ := runtime.Caller(0)
prefix = strings.TrimSuffix(filename, "modules/log/log.go")
if prefix == filename {
// in case the source code file is moved, we can not trim the suffix, the code above should also be updated.
panic("unable to detect correct package prefix, please update file: " + filename)
}
}

@ -1,152 +0,0 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func baseConsoleTest(t *testing.T, logger *MultiChannelledLogger) (chan []byte, chan bool) {
written := make(chan []byte)
closed := make(chan bool)
c := CallbackWriteCloser{
callback: func(p []byte, close bool) {
written <- p
closed <- close
},
}
m := logger.MultiChannelledLog
channelledLog := m.GetEventLogger("console")
assert.NotEmpty(t, channelledLog)
realChanLog, ok := channelledLog.(*ChannelledLog)
assert.True(t, ok)
realCL, ok := realChanLog.loggerProvider.(*ConsoleLogger)
assert.True(t, ok)
assert.Equal(t, INFO, realCL.Level)
realCL.out = c
format := "test: %s"
args := []interface{}{"A"}
logger.Log(0, INFO, format, args...)
line := <-written
assert.Contains(t, string(line), fmt.Sprintf(format, args...))
assert.False(t, <-closed)
format = "test2: %s"
logger.Warn(format, args...)
line = <-written
assert.Contains(t, string(line), fmt.Sprintf(format, args...))
assert.False(t, <-closed)
format = "testerror: %s"
logger.Error(format, args...)
line = <-written
assert.Contains(t, string(line), fmt.Sprintf(format, args...))
assert.False(t, <-closed)
return written, closed
}
func TestNewLoggerUnexported(t *testing.T) {
level := INFO
logger := newLogger("UNEXPORTED", 0)
err := logger.SetLogger("console", "console", fmt.Sprintf(`{"level":"%s"}`, level.String()))
assert.NoError(t, err)
out := logger.MultiChannelledLog.GetEventLogger("console")
assert.NotEmpty(t, out)
chanlog, ok := out.(*ChannelledLog)
assert.True(t, ok)
assert.Equal(t, "console", chanlog.provider)
assert.Equal(t, INFO, logger.GetLevel())
baseConsoleTest(t, logger)
}
func TestNewLoggger(t *testing.T) {
level := INFO
logger := NewLogger(0, "console", "console", fmt.Sprintf(`{"level":"%s"}`, level.String()))
assert.Equal(t, INFO, GetLevel())
assert.False(t, IsTrace())
assert.False(t, IsDebug())
assert.True(t, IsInfo())
assert.True(t, IsWarn())
assert.True(t, IsError())
written, closed := baseConsoleTest(t, logger)
format := "test: %s"
args := []interface{}{"A"}
Log(0, INFO, format, args...)
line := <-written
assert.Contains(t, string(line), fmt.Sprintf(format, args...))
assert.False(t, <-closed)
Info(format, args...)
line = <-written
assert.Contains(t, string(line), fmt.Sprintf(format, args...))
assert.False(t, <-closed)
go DelLogger("console")
line = <-written
assert.Equal(t, "", string(line))
assert.True(t, <-closed)
}
func TestNewLogggerRecreate(t *testing.T) {
level := INFO
NewLogger(0, "console", "console", fmt.Sprintf(`{"level":"%s"}`, level.String()))
assert.Equal(t, INFO, GetLevel())
assert.False(t, IsTrace())
assert.False(t, IsDebug())
assert.True(t, IsInfo())
assert.True(t, IsWarn())
assert.True(t, IsError())
format := "test: %s"
args := []interface{}{"A"}
Log(0, INFO, format, args...)
NewLogger(0, "console", "console", fmt.Sprintf(`{"level":"%s"}`, level.String()))
assert.Equal(t, INFO, GetLevel())
assert.False(t, IsTrace())
assert.False(t, IsDebug())
assert.True(t, IsInfo())
assert.True(t, IsWarn())
assert.True(t, IsError())
Log(0, INFO, format, args...)
assert.Panics(t, func() {
NewLogger(0, "console", "console", fmt.Sprintf(`{"level":"%s"`, level.String()))
})
go DelLogger("console")
// We should be able to redelete without a problem
go DelLogger("console")
}
func TestNewNamedLogger(t *testing.T) {
level := INFO
err := NewNamedLogger("test", 0, "console", "console", fmt.Sprintf(`{"level":"%s"}`, level.String()))
assert.NoError(t, err)
logger, _ := NamedLoggers.Load("test")
assert.Equal(t, level, logger.GetLevel())
written, closed := baseConsoleTest(t, logger)
go DelNamedLogger("test")
line := <-written
assert.Equal(t, "", string(line))
assert.True(t, <-closed)
}

@ -1,141 +1,50 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
// Package log provides logging capabilities for Gitea.
// Concepts:
//
// * Logger: a Logger provides logging functions and dispatches log events to all its writers
//
// * EventWriter: written log Event to a destination (eg: file, console)
// - EventWriterBase: the base struct of a writer, it contains common fields and functions for all writers
// - WriterType: the type name of a writer, eg: console, file
// - WriterName: aka Mode Name in document, the name of a writer instance, it's usually defined by the config file.
// It is called "mode name" because old code use MODE as config key, to keep compatibility, keep this concept.
//
// * WriterMode: the common options for all writers, eg: log level.
// - WriterConsoleOption and others: the specified options for a writer, eg: file path, remote address.
//
// Call graph:
// -> log.Info()
// -> LoggerImpl.Log()
// -> LoggerImpl.SendLogEvent, then the event goes into writer's goroutines
// -> EventWriter.Run() handles the events
package log
import "os"
// Logger is the basic interface for logging
type Logger interface {
LevelLogger
Trace(format string, v ...interface{})
IsTrace() bool
Debug(format string, v ...interface{})
IsDebug() bool
Info(format string, v ...interface{})
IsInfo() bool
Warn(format string, v ...interface{})
IsWarn() bool
Error(format string, v ...interface{})
ErrorWithSkip(skip int, format string, v ...interface{})
IsError() bool
Critical(format string, v ...interface{})
CriticalWithSkip(skip int, format string, v ...interface{})
IsCritical() bool
Fatal(format string, v ...interface{})
FatalWithSkip(skip int, format string, v ...interface{})
IsFatal() bool
}
// LevelLogger is the simplest logging interface
type LevelLogger interface {
Flush()
Close()
// BaseLogger provides the basic logging functions
type BaseLogger interface {
Log(skip int, level Level, format string, v ...any)
GetLevel() Level
Log(skip int, level Level, format string, v ...interface{}) error
}
// SettableLogger is the interface of loggers which have subloggers
type SettableLogger interface {
SetLogger(name, provider, config string) error
DelLogger(name string) (bool, error)
}
// LevelLogger provides level-related logging functions
type LevelLogger interface {
LevelEnabled(level Level) bool
// StacktraceLogger is a logger that can log stacktraces
type StacktraceLogger interface {
GetStacktraceLevel() Level
Trace(format string, v ...any)
Debug(format string, v ...any)
Info(format string, v ...any)
Warn(format string, v ...any)
Error(format string, v ...any)
Critical(format string, v ...any)
}
// LevelLoggerLogger wraps a LevelLogger as a Logger
type LevelLoggerLogger struct {
type Logger interface {
BaseLogger
LevelLogger
}
// Trace records trace log
func (l *LevelLoggerLogger) Trace(format string, v ...interface{}) {
l.Log(1, TRACE, format, v...) //nolint:errcheck
}
// IsTrace returns true if the logger is TRACE
func (l *LevelLoggerLogger) IsTrace() bool {
return l.GetLevel() <= TRACE
}
// Debug records debug log
func (l *LevelLoggerLogger) Debug(format string, v ...interface{}) {
l.Log(1, DEBUG, format, v...) //nolint:errcheck
}
// IsDebug returns true if the logger is DEBUG
func (l *LevelLoggerLogger) IsDebug() bool {
return l.GetLevel() <= DEBUG
}
// Info records information log
func (l *LevelLoggerLogger) Info(format string, v ...interface{}) {
l.Log(1, INFO, format, v...) //nolint:errcheck
}
// IsInfo returns true if the logger is INFO
func (l *LevelLoggerLogger) IsInfo() bool {
return l.GetLevel() <= INFO
}
// Warn records warning log
func (l *LevelLoggerLogger) Warn(format string, v ...interface{}) {
l.Log(1, WARN, format, v...) //nolint:errcheck
}
// IsWarn returns true if the logger is WARN
func (l *LevelLoggerLogger) IsWarn() bool {
return l.GetLevel() <= WARN
}
// Error records error log
func (l *LevelLoggerLogger) Error(format string, v ...interface{}) {
l.Log(1, ERROR, format, v...) //nolint:errcheck
}
// ErrorWithSkip records error log from "skip" calls back from this function
func (l *LevelLoggerLogger) ErrorWithSkip(skip int, format string, v ...interface{}) {
l.Log(skip+1, ERROR, format, v...) //nolint:errcheck
}
// IsError returns true if the logger is ERROR
func (l *LevelLoggerLogger) IsError() bool {
return l.GetLevel() <= ERROR
}
// Critical records critical log
func (l *LevelLoggerLogger) Critical(format string, v ...interface{}) {
l.Log(1, CRITICAL, format, v...) //nolint:errcheck
}
// CriticalWithSkip records critical log from "skip" calls back from this function
func (l *LevelLoggerLogger) CriticalWithSkip(skip int, format string, v ...interface{}) {
l.Log(skip+1, CRITICAL, format, v...) //nolint:errcheck
}
// IsCritical returns true if the logger is CRITICAL
func (l *LevelLoggerLogger) IsCritical() bool {
return l.GetLevel() <= CRITICAL
}
// Fatal records fatal log and exit the process
func (l *LevelLoggerLogger) Fatal(format string, v ...interface{}) {
l.Log(1, FATAL, format, v...) //nolint:errcheck
l.Close()
os.Exit(1)
}
// FatalWithSkip records fatal log from "skip" calls back from this function and exits the process
func (l *LevelLoggerLogger) FatalWithSkip(skip int, format string, v ...interface{}) {
l.Log(skip+1, FATAL, format, v...) //nolint:errcheck
l.Close()
os.Exit(1)
}
// IsFatal returns true if the logger is FATAL
func (l *LevelLoggerLogger) IsFatal() bool {
return l.GetLevel() <= FATAL
type LogStringer interface { //nolint:revive
LogString() string
}

@ -0,0 +1,83 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"fmt"
"os"
)
// FallbackErrorf is the last chance to show an error if the logger has internal errors
func FallbackErrorf(format string, args ...any) {
_, _ = fmt.Fprintf(os.Stderr, format+"\n", args)
}
func GetLevel() Level {
return GetLogger(DEFAULT).GetLevel()
}
func Log(skip int, level Level, format string, v ...any) {
GetLogger(DEFAULT).Log(skip+1, level, format, v...)
}
func Trace(format string, v ...any) {
Log(1, TRACE, format, v...)
}
func IsTrace() bool {
return GetLevel() <= TRACE
}
func Debug(format string, v ...any) {
Log(1, DEBUG, format, v...)
}
func IsDebug() bool {
return GetLevel() <= DEBUG
}
func Info(format string, v ...any) {
Log(1, INFO, format, v...)
}
func Warn(format string, v ...any) {
Log(1, WARN, format, v...)
}
func Error(format string, v ...any) {
Log(1, ERROR, format, v...)
}
func ErrorWithSkip(skip int, format string, v ...any) {
Log(skip+1, ERROR, format, v...)
}
func Critical(format string, v ...any) {
Log(1, ERROR, format, v...)
}
// Fatal records fatal log and exit process
func Fatal(format string, v ...any) {
Log(1, FATAL, format, v...)
GetManager().Close()
os.Exit(1)
}
func GetLogger(name string) Logger {
return GetManager().GetLogger(name)
}
func IsLoggerEnabled(name string) bool {
return GetManager().GetLogger(name).IsEnabled()
}
func SetConsoleLogger(loggerName, writerName string, level Level) {
writer := NewEventWriterConsole(writerName, WriterMode{
Level: level,
Flags: FlagsFromBits(LstdFlags),
Colorize: CanColorStdout,
WriterOption: WriterConsoleOption{},
})
GetManager().GetLogger(loggerName).RemoveAllWriters().AddWriters(writer)
}

@ -0,0 +1,239 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"context"
"runtime"
"strings"
"sync"
"sync/atomic"
"time"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/util"
)
type LoggerImpl struct {
LevelLogger
ctx context.Context
ctxCancel context.CancelFunc
level atomic.Int32
stacktraceLevel atomic.Int32
eventWriterMu sync.RWMutex
eventWriters map[string]EventWriter
}
var (
_ BaseLogger = (*LoggerImpl)(nil)
_ LevelLogger = (*LoggerImpl)(nil)
)
// SendLogEvent sends a log event to all writers
func (l *LoggerImpl) SendLogEvent(event *Event) {
l.eventWriterMu.RLock()
defer l.eventWriterMu.RUnlock()
if len(l.eventWriters) == 0 {
FallbackErrorf("[no logger writer]: %s", event.MsgSimpleText)
return
}
// the writers have their own goroutines, the message arguments (with Stringer) shouldn't be used in other goroutines
// so the event message must be formatted here
msgFormat, msgArgs := event.msgFormat, event.msgArgs
event.msgFormat, event.msgArgs = "(already processed by formatters)", nil
for _, w := range l.eventWriters {
if event.Level < w.GetLevel() {
continue
}
formatted := &EventFormatted{
Origin: event,
Msg: w.Base().FormatMessage(w.Base().Mode, event, msgFormat, msgArgs...),
}
select {
case w.Base().Queue <- formatted:
default:
bs, _ := json.Marshal(event)
FallbackErrorf("log writer %q queue is full, event: %v", w.GetWriterName(), string(bs))
}
}
}
// syncLevelInternal syncs the level of the logger with the levels of the writers
func (l *LoggerImpl) syncLevelInternal() {
lowestLevel := NONE
for _, w := range l.eventWriters {
if w.GetLevel() < lowestLevel {
lowestLevel = w.GetLevel()
}
}
l.level.Store(int32(lowestLevel))
lowestLevel = NONE
for _, w := range l.eventWriters {
if w.Base().Mode.StacktraceLevel < lowestLevel {
lowestLevel = w.GetLevel()
}
}
l.stacktraceLevel.Store(int32(lowestLevel))
}
// removeWriterInternal removes a writer from the logger, and stops it if it's not shared
func (l *LoggerImpl) removeWriterInternal(w EventWriter) {
if !w.Base().shared {
eventWriterStopWait(w) // only stop non-shared writers, shared writers are managed by the manager
}
delete(l.eventWriters, w.GetWriterName())
}
// AddWriters adds writers to the logger, and starts them. Existing writers will be replaced by new ones.
func (l *LoggerImpl) AddWriters(writer ...EventWriter) {
l.eventWriterMu.Lock()
defer l.eventWriterMu.Unlock()
for _, w := range writer {
if old, ok := l.eventWriters[w.GetWriterName()]; ok {
l.removeWriterInternal(old)
}
}
for _, w := range writer {
l.eventWriters[w.GetWriterName()] = w
eventWriterStartGo(l.ctx, w, false)
}
l.syncLevelInternal()
}
// RemoveWriter removes a writer from the logger, and the writer is closed and flushed if it is not shared
func (l *LoggerImpl) RemoveWriter(modeName string) error {
l.eventWriterMu.Lock()
defer l.eventWriterMu.Unlock()
w, ok := l.eventWriters[modeName]
if !ok {
return util.ErrNotExist
}
l.removeWriterInternal(w)
l.syncLevelInternal()
return nil
}
// RemoveAllWriters removes all writers from the logger, non-shared writers are closed and flushed
func (l *LoggerImpl) RemoveAllWriters() *LoggerImpl {
l.eventWriterMu.Lock()
defer l.eventWriterMu.Unlock()
for _, w := range l.eventWriters {
l.removeWriterInternal(w)
}
l.eventWriters = map[string]EventWriter{}
l.syncLevelInternal()
return l
}
// DumpWriters dumps the writers as a JSON map, it's used for debugging and display purposes.
func (l *LoggerImpl) DumpWriters() map[string]any {
l.eventWriterMu.RLock()
defer l.eventWriterMu.RUnlock()
writers := make(map[string]any, len(l.eventWriters))
for k, w := range l.eventWriters {
bs, err := json.Marshal(w.Base().Mode)
if err != nil {
FallbackErrorf("marshal writer %q to dump failed: %v", k, err)
continue
}
m := map[string]any{}
_ = json.Unmarshal(bs, &m)
m["WriterType"] = w.GetWriterType()
writers[k] = m
}
return writers
}
// Close closes the logger, non-shared writers are closed and flushed
func (l *LoggerImpl) Close() {
l.RemoveAllWriters()
l.ctxCancel()
}
// IsEnabled returns true if the logger is enabled: it has a working level and has writers
// Fatal is not considered as enabled, because it's a special case and the process just exits
func (l *LoggerImpl) IsEnabled() bool {
l.eventWriterMu.RLock()
defer l.eventWriterMu.RUnlock()
return l.level.Load() < int32(FATAL) && len(l.eventWriters) > 0
}
// Log prepares the log event, if the level matches, the event will be sent to the writers
func (l *LoggerImpl) Log(skip int, level Level, format string, logArgs ...any) {
if Level(l.level.Load()) > level {
return
}
event := &Event{
Time: time.Now(),
Level: level,
Caller: "?()",
}
pc, filename, line, ok := runtime.Caller(skip + 1)
if ok {
fn := runtime.FuncForPC(pc)
if fn != nil {
event.Caller = fn.Name() + "()"
}
}
event.Filename, event.Line = strings.TrimPrefix(filename, projectPackagePrefix), line
if l.stacktraceLevel.Load() <= int32(level) {
event.Stacktrace = Stack(skip + 1)
}
labels := getGoroutineLabels()
if labels != nil {
event.GoroutinePid = labels["pid"]
}
// get a simple text message without color
msgArgs := make([]any, len(logArgs))
copy(msgArgs, logArgs)
// handle LogStringer values
for i, v := range msgArgs {
if cv, ok := v.(*ColoredValue); ok {
if s, ok := cv.v.(LogStringer); ok {
cv.v = logStringFormatter{v: s}
}
} else if s, ok := v.(LogStringer); ok {
msgArgs[i] = logStringFormatter{v: s}
}
}
event.MsgSimpleText = colorSprintf(false, format, msgArgs...)
event.msgFormat = format
event.msgArgs = msgArgs
l.SendLogEvent(event)
}
func (l *LoggerImpl) GetLevel() Level {
return Level(l.level.Load())
}
func NewLoggerWithWriters(ctx context.Context, writer ...EventWriter) *LoggerImpl {
l := &LoggerImpl{}
l.ctx, l.ctxCancel = context.WithCancel(ctx)
l.LevelLogger = BaseLoggerToGeneralLogger(l)
l.eventWriters = map[string]EventWriter{}
l.syncLevelInternal()
l.AddWriters(writer...)
return l
}

@ -0,0 +1,145 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"context"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
type dummyWriter struct {
*EventWriterBaseImpl
delay time.Duration
mu sync.Mutex
logs []string
}
func (d *dummyWriter) Write(p []byte) (n int, err error) {
if d.delay > 0 {
time.Sleep(d.delay)
}
d.mu.Lock()
defer d.mu.Unlock()
d.logs = append(d.logs, string(p))
return len(p), nil
}
func (d *dummyWriter) Close() error {
return nil
}
func (d *dummyWriter) GetLogs() []string {
d.mu.Lock()
defer d.mu.Unlock()
logs := make([]string, len(d.logs))
copy(logs, d.logs)
return logs
}
func newDummyWriter(name string, level Level, delay time.Duration) *dummyWriter {
w := &dummyWriter{
EventWriterBaseImpl: NewEventWriterBase(name, "dummy", WriterMode{Level: level, Flags: FlagsFromBits(0)}),
}
w.delay = delay
w.Base().OutputWriteCloser = w
return w
}
func TestLogger(t *testing.T) {
logger := NewLoggerWithWriters(context.Background())
dump := logger.DumpWriters()
assert.EqualValues(t, 0, len(dump))
assert.EqualValues(t, NONE, logger.GetLevel())
assert.False(t, logger.IsEnabled())
w1 := newDummyWriter("dummy-1", DEBUG, 0)
logger.AddWriters(w1)
assert.EqualValues(t, DEBUG, logger.GetLevel())
w2 := newDummyWriter("dummy-2", WARN, 200*time.Millisecond)
logger.AddWriters(w2)
assert.EqualValues(t, DEBUG, logger.GetLevel())
dump = logger.DumpWriters()
assert.EqualValues(t, 2, len(dump))
logger.Trace("trace-level") // this level is not logged
logger.Debug("debug-level")
logger.Error("error-level")
// w2 is slow, so only w1 has logs
time.Sleep(100 * time.Millisecond)
assert.Equal(t, []string{"debug-level\n", "error-level\n"}, w1.GetLogs())
assert.Equal(t, []string{}, w2.GetLogs())
logger.Close()
// after Close, all logs are flushed
assert.Equal(t, []string{"debug-level\n", "error-level\n"}, w1.GetLogs())
assert.Equal(t, []string{"error-level\n"}, w2.GetLogs())
}
func TestLoggerPause(t *testing.T) {
logger := NewLoggerWithWriters(context.Background())
w1 := newDummyWriter("dummy-1", DEBUG, 0)
logger.AddWriters(w1)
GetManager().PauseAll()
logger.Info("info-level")
time.Sleep(100 * time.Millisecond)
assert.Equal(t, []string{}, w1.GetLogs())
GetManager().ResumeAll()
time.Sleep(100 * time.Millisecond)
assert.Equal(t, []string{"info-level\n"}, w1.GetLogs())
logger.Close()
}
type testLogString struct {
Field string
}
func (t testLogString) LogString() string {
return "log-string"
}
func TestLoggerLogString(t *testing.T) {
logger := NewLoggerWithWriters(context.Background())
w1 := newDummyWriter("dummy-1", DEBUG, 0)
w1.Mode.Colorize = true
logger.AddWriters(w1)
logger.Info("%s %s %#v %v", testLogString{}, &testLogString{}, testLogString{Field: "detail"}, NewColoredValue(testLogString{}, FgRed))
logger.Close()
assert.Equal(t, []string{"log-string log-string log.testLogString{Field:\"detail\"} \x1b[31mlog-string\x1b[0m\n"}, w1.GetLogs())
}
func TestLoggerExpressionFilter(t *testing.T) {
logger := NewLoggerWithWriters(context.Background())
w1 := newDummyWriter("dummy-1", DEBUG, 0)
w1.Mode.Expression = "foo.*"
logger.AddWriters(w1)
logger.Info("foo")
logger.Info("bar")
logger.Info("foo bar")
logger.SendLogEvent(&Event{Level: INFO, Filename: "foo.go", MsgSimpleText: "by filename"})
logger.Close()
assert.Equal(t, []string{"foo\n", "foo bar\n", "by filename\n"}, w1.GetLogs())
}

@ -0,0 +1,142 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"context"
"fmt"
"sync"
"sync/atomic"
)
const DEFAULT = "default"
// LoggerManager manages loggers and shared event writers
type LoggerManager struct {
ctx context.Context
ctxCancel context.CancelFunc
mu sync.Mutex
writers map[string]EventWriter
loggers map[string]*LoggerImpl
defaultLogger atomic.Pointer[LoggerImpl]
pauseMu sync.RWMutex
pauseChan chan struct{}
}
// GetLogger returns a logger with the given name. If the logger doesn't exist, a new empty one will be created.
func (m *LoggerManager) GetLogger(name string) *LoggerImpl {
if name == DEFAULT {
if logger := m.defaultLogger.Load(); logger != nil {
return logger
}
}
m.mu.Lock()
defer m.mu.Unlock()
logger := m.loggers[name]
if logger == nil {
logger = NewLoggerWithWriters(m.ctx)
m.loggers[name] = logger
if name == DEFAULT {
m.defaultLogger.Store(logger)
}
}
return logger
}
// PauseAll pauses all event writers
func (m *LoggerManager) PauseAll() {
m.pauseMu.Lock()
m.pauseChan = make(chan struct{})
m.pauseMu.Unlock()
}
// ResumeAll resumes all event writers
func (m *LoggerManager) ResumeAll() {
m.pauseMu.Lock()
close(m.pauseChan)
m.pauseChan = nil
m.pauseMu.Unlock()
}
// GetPauseChan returns a channel for writer pausing
func (m *LoggerManager) GetPauseChan() chan struct{} {
m.pauseMu.RLock()
defer m.pauseMu.RUnlock()
return m.pauseChan
}
// Close closes the logger manager, all loggers and writers will be closed, the messages are flushed.
func (m *LoggerManager) Close() {
m.mu.Lock()
defer m.mu.Unlock()
for _, logger := range m.loggers {
logger.Close()
}
m.loggers = map[string]*LoggerImpl{}
for _, writer := range m.writers {
eventWriterStopWait(writer)
}
m.writers = map[string]EventWriter{}
m.ctxCancel()
}
// DumpLoggers returns a map of all loggers and their event writers, for debugging and display purposes.
func (m *LoggerManager) DumpLoggers() map[string]any {
m.mu.Lock()
defer m.mu.Unlock()
dump := map[string]any{}
for name, logger := range m.loggers {
loggerDump := map[string]any{
"IsEnabled": logger.IsEnabled(),
"EventWriters": logger.DumpWriters(),
}
dump[name] = loggerDump
}
return dump
}
// NewSharedWriter creates a new shared event writer, it can be used by multiple loggers, and a shared writer won't be closed if a logger is closed.
func (m *LoggerManager) NewSharedWriter(writerName, writerType string, mode WriterMode) (writer EventWriter, err error) {
m.mu.Lock()
defer m.mu.Unlock()
if _, ok := m.writers[writerName]; ok {
return nil, fmt.Errorf("log event writer %q has been added before", writerName)
}
if writer, err = NewEventWriter(writerName, writerType, mode); err != nil {
return nil, err
}
m.writers[writerName] = writer
eventWriterStartGo(m.ctx, writer, true)
return writer, nil
}
func (m *LoggerManager) GetSharedWriter(writerName string) EventWriter {
m.mu.Lock()
defer m.mu.Unlock()
return m.writers[writerName]
}
var loggerManager = NewManager()
func GetManager() *LoggerManager {
return loggerManager
}
func NewManager() *LoggerManager {
m := &LoggerManager{writers: map[string]EventWriter{}, loggers: map[string]*LoggerImpl{}}
m.ctx, m.ctxCancel = context.WithCancel(context.Background())
return m
}

@ -0,0 +1,42 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestSharedWorker(t *testing.T) {
RegisterEventWriter("dummy", func(writerName string, writerMode WriterMode) EventWriter {
return newDummyWriter(writerName, writerMode.Level, 0)
})
m := NewManager()
_, err := m.NewSharedWriter("dummy-1", "dummy", WriterMode{Level: DEBUG, Flags: FlagsFromBits(0)})
assert.NoError(t, err)
w := m.GetSharedWriter("dummy-1")
assert.NotNil(t, w)
loggerTest := m.GetLogger("test")
loggerTest.AddWriters(w)
loggerTest.Info("msg-1")
loggerTest.RemoveAllWriters() // the shared writer is not closed here
loggerTest.Info("never seen")
// the shared writer can still be used later
w = m.GetSharedWriter("dummy-1")
assert.NotNil(t, w)
loggerTest.AddWriters(w)
loggerTest.Info("msg-2")
m.GetLogger("test-another").AddWriters(w)
m.GetLogger("test-another").Info("msg-3")
m.Close()
logs := w.(*dummyWriter).GetLogs()
assert.Equal(t, []string{"msg-1\n", "msg-2\n", "msg-3\n"}, logs)
}

@ -0,0 +1,78 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"io"
)
type baseToLogger struct {
base BaseLogger
}
// BaseLoggerToGeneralLogger wraps a BaseLogger (which only has Log() function) to a Logger (which has Info() function)
func BaseLoggerToGeneralLogger(b BaseLogger) Logger {
l := &baseToLogger{base: b}
return l
}
var _ Logger = (*baseToLogger)(nil)
func (s *baseToLogger) Log(skip int, level Level, format string, v ...any) {
s.base.Log(skip+1, level, format, v...)
}
func (s *baseToLogger) GetLevel() Level {
return s.base.GetLevel()
}
func (s *baseToLogger) LevelEnabled(level Level) bool {
return s.base.GetLevel() <= level
}
func (s *baseToLogger) Trace(format string, v ...any) {
s.base.Log(1, TRACE, format, v...)
}
func (s *baseToLogger) Debug(format string, v ...any) {
s.base.Log(1, DEBUG, format, v...)
}
func (s *baseToLogger) Info(format string, v ...any) {
s.base.Log(1, INFO, format, v...)
}
func (s *baseToLogger) Warn(format string, v ...any) {
s.base.Log(1, WARN, format, v...)
}
func (s *baseToLogger) Error(format string, v ...any) {
s.base.Log(1, ERROR, format, v...)
}
func (s *baseToLogger) Critical(format string, v ...any) {
s.base.Log(1, CRITICAL, format, v...)
}
type PrintfLogger struct {
Logf func(format string, args ...any)
}
func (p *PrintfLogger) Printf(format string, args ...any) {
p.Logf(format, args...)
}
type loggerToWriter struct {
logf func(format string, args ...any)
}
func (p *loggerToWriter) Write(bs []byte) (int, error) {
p.logf("%s", string(bs))
return len(bs), nil
}
// LoggerToWriter wraps a log function to an io.Writer
func LoggerToWriter(logf func(format string, args ...any)) io.Writer {
return &loggerToWriter{logf: logf}
}

@ -1,104 +0,0 @@
// Copyright 2020 The Gogs Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"fmt"
"runtime"
"strings"
"time"
)
// MultiChannelledLogger is default logger in the Gitea application.
// it can contain several providers and log message into all providers.
type MultiChannelledLogger struct {
LevelLoggerLogger
*MultiChannelledLog
bufferLength int64
}
// newLogger initializes and returns a new logger.
func newLogger(name string, buffer int64) *MultiChannelledLogger {
l := &MultiChannelledLogger{
MultiChannelledLog: NewMultiChannelledLog(name, buffer),
bufferLength: buffer,
}
l.LevelLogger = l
return l
}
// SetLogger sets new logger instance with given logger provider and config.
func (l *MultiChannelledLogger) SetLogger(name, provider, config string) error {
eventLogger, err := NewChannelledLog(l.ctx, name, provider, config, l.bufferLength)
if err != nil {
return fmt.Errorf("failed to create sublogger (%s): %w", name, err)
}
l.MultiChannelledLog.DelLogger(name)
err = l.MultiChannelledLog.AddLogger(eventLogger)
if err != nil {
if IsErrDuplicateName(err) {
return fmt.Errorf("%w other names: %v", err, l.MultiChannelledLog.GetEventLoggerNames())
}
return fmt.Errorf("failed to add sublogger (%s): %w", name, err)
}
return nil
}
// DelLogger deletes a sublogger from this logger.
func (l *MultiChannelledLogger) DelLogger(name string) (bool, error) {
return l.MultiChannelledLog.DelLogger(name), nil
}
// Log msg at the provided level with the provided caller defined by skip (0 being the function that calls this function)
func (l *MultiChannelledLogger) Log(skip int, level Level, format string, v ...interface{}) error {
if l.GetLevel() > level {
return nil
}
caller := "?()"
pc, filename, line, ok := runtime.Caller(skip + 1)
if ok {
// Get caller function name.
fn := runtime.FuncForPC(pc)
if fn != nil {
caller = fn.Name() + "()"
}
}
msg := format
if len(v) > 0 {
msg = ColorSprintf(format, v...)
}
labels := getGoroutineLabels()
if labels != nil {
pid, ok := labels["pid"]
if ok {
msg = "[" + ColorString(FgHiYellow) + pid + ColorString(Reset) + "] " + msg
}
}
stack := ""
if l.GetStacktraceLevel() <= level {
stack = Stack(skip + 1)
}
return l.SendLog(level, caller, strings.TrimPrefix(filename, prefix), line, msg, stack)
}
// SendLog sends a log event at the provided level with the information given
func (l *MultiChannelledLogger) SendLog(level Level, caller, filename string, line int, msg, stack string) error {
if l.GetLevel() > level {
return nil
}
event := &Event{
level: level,
caller: caller,
filename: filename,
line: line,
msg: msg,
time: time.Now(),
stacktrace: stack,
}
l.LogEvent(event) //nolint:errcheck
return nil
}

@ -1,25 +0,0 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
// LoggerProvider represents behaviors of a logger provider.
type LoggerProvider interface {
Init(config string) error
EventLogger
}
type loggerProvider func() LoggerProvider
var providers = make(map[string]loggerProvider)
// Register registers given logger provider to providers.
func Register(name string, log loggerProvider) {
if log == nil {
panic("log: register provider is nil")
}
if _, dup := providers[name]; dup {
panic("log: register called twice for provider \"" + name + "\"")
}
providers[name] = log
}

@ -1,114 +0,0 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"fmt"
"net/smtp"
"strings"
"code.gitea.io/gitea/modules/json"
)
type smtpWriter struct {
owner *SMTPLogger
}
// Write sends the message as an email
func (s *smtpWriter) Write(p []byte) (int, error) {
return s.owner.sendMail(p)
}
// Close does nothing
func (s *smtpWriter) Close() error {
return nil
}
// SMTPLogger implements LoggerProvider and is used to send emails via given SMTP-server.
type SMTPLogger struct {
WriterLogger
Username string `json:"Username"`
Password string `json:"password"`
Host string `json:"host"`
Subject string `json:"subject"`
RecipientAddresses []string `json:"sendTos"`
sendMailFn func(string, smtp.Auth, string, []string, []byte) error
}
// NewSMTPLogger creates smtp writer.
func NewSMTPLogger() LoggerProvider {
s := &SMTPLogger{}
s.Level = TRACE
s.sendMailFn = smtp.SendMail
return s
}
// Init smtp writer with json config.
// config like:
//
// {
// "Username":"example@gmail.com",
// "password:"password",
// "host":"smtp.gmail.com:465",
// "subject":"email title",
// "sendTos":["email1","email2"],
// "level":LevelError
// }
func (log *SMTPLogger) Init(jsonconfig string) error {
err := json.Unmarshal([]byte(jsonconfig), log)
if err != nil {
return fmt.Errorf("Unable to parse JSON: %w", err)
}
log.NewWriterLogger(&smtpWriter{
owner: log,
})
log.sendMailFn = smtp.SendMail
return nil
}
// WriteMsg writes message in smtp writer.
// it will send an email with subject and only this message.
func (log *SMTPLogger) sendMail(p []byte) (int, error) {
hp := strings.Split(log.Host, ":")
// Set up authentication information.
auth := smtp.PlainAuth(
"",
log.Username,
log.Password,
hp[0],
)
// Connect to the server, authenticate, set the sender and recipient,
// and send the email all in one step.
contentType := "Content-Type: text/plain" + "; charset=UTF-8"
mailmsg := []byte("To: " + strings.Join(log.RecipientAddresses, ";") + "\r\nFrom: " + log.Username + "<" + log.Username +
">\r\nSubject: " + log.Subject + "\r\n" + contentType + "\r\n\r\n")
mailmsg = append(mailmsg, p...)
return len(p), log.sendMailFn(
log.Host,
auth,
log.Username,
log.RecipientAddresses,
mailmsg,
)
}
// Flush when log should be flushed
func (log *SMTPLogger) Flush() {
}
// ReleaseReopen does nothing
func (log *SMTPLogger) ReleaseReopen() error {
return nil
}
// GetName returns the default name for this implementation
func (log *SMTPLogger) GetName() string {
return "smtp"
}
func init() {
Register("smtp", NewSMTPLogger)
}

@ -1,85 +0,0 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"fmt"
"net/smtp"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestSMTPLogger(t *testing.T) {
prefix := "TestPrefix "
level := INFO
flags := LstdFlags | LUTC | Lfuncname
username := "testuser"
password := "testpassword"
host := "testhost"
subject := "testsubject"
sendTos := []string{"testto1", "testto2"}
logger := NewSMTPLogger()
smtpLogger, ok := logger.(*SMTPLogger)
assert.True(t, ok)
err := logger.Init(fmt.Sprintf("{\"prefix\":\"%s\",\"level\":\"%s\",\"flags\":%d,\"username\":\"%s\",\"password\":\"%s\",\"host\":\"%s\",\"subject\":\"%s\",\"sendTos\":[\"%s\",\"%s\"]}", prefix, level.String(), flags, username, password, host, subject, sendTos[0], sendTos[1]))
assert.NoError(t, err)
assert.Equal(t, flags, smtpLogger.Flags)
assert.Equal(t, level, smtpLogger.Level)
assert.Equal(t, level, logger.GetLevel())
location, _ := time.LoadLocation("EST")
date := time.Date(2019, time.January, 13, 22, 3, 30, 15, location)
dateString := date.UTC().Format("2006/01/02 15:04:05")
event := Event{
level: INFO,
msg: "TEST MSG",
caller: "CALLER",
filename: "FULL/FILENAME",
line: 1,
time: date,
}
expected := fmt.Sprintf("%s%s %s:%d:%s [%c] %s\n", prefix, dateString, event.filename, event.line, event.caller, strings.ToUpper(event.level.String())[0], event.msg)
var envToHost string
var envFrom string
var envTo []string
var envMsg []byte
smtpLogger.sendMailFn = func(addr string, a smtp.Auth, from string, to []string, msg []byte) error {
envToHost = addr
envFrom = from
envTo = to
envMsg = msg
return nil
}
err = logger.LogEvent(&event)
assert.NoError(t, err)
assert.Equal(t, host, envToHost)
assert.Equal(t, username, envFrom)
assert.Equal(t, sendTos, envTo)
assert.Contains(t, string(envMsg), expected)
logger.Flush()
event.level = WARN
expected = fmt.Sprintf("%s%s %s:%d:%s [%c] %s\n", prefix, dateString, event.filename, event.line, event.caller, strings.ToUpper(event.level.String())[0], event.msg)
err = logger.LogEvent(&event)
assert.NoError(t, err)
assert.Equal(t, host, envToHost)
assert.Equal(t, username, envFrom)
assert.Equal(t, sendTos, envTo)
assert.Contains(t, string(envMsg), expected)
logger.Close()
}

@ -32,19 +32,19 @@ func Stack(skip int) string {
}
// Print equivalent of debug.Stack()
fmt.Fprintf(buf, "%s:%d (0x%x)\n", filename, lineNumber, programCounter)
_, _ = fmt.Fprintf(buf, "%s:%d (0x%x)\n", filename, lineNumber, programCounter)
// Now try to print the offending line
if filename != lastFilename {
data, err := os.ReadFile(filename)
if err != nil {
// can't read this sourcefile
// can't read this source file
// likely we don't have the sourcecode available
continue
}
lines = bytes.Split(data, []byte{'\n'})
lastFilename = filename
}
fmt.Fprintf(buf, "\t%s: %s\n", functionName(programCounter), source(lines, lineNumber))
_, _ = fmt.Fprintf(buf, "\t%s: %s\n", functionName(programCounter), source(lines, lineNumber))
}
return buf.String()
}

@ -1,269 +0,0 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"bytes"
"fmt"
"io"
"regexp"
"strings"
"sync"
)
type byteArrayWriter []byte
func (b *byteArrayWriter) Write(p []byte) (int, error) {
*b = append(*b, p...)
return len(p), nil
}
// WriterLogger represent a basic logger for Gitea
type WriterLogger struct {
out io.WriteCloser
mu sync.Mutex
Level Level `json:"level"`
StacktraceLevel Level `json:"stacktraceLevel"`
Flags int `json:"flags"`
Prefix string `json:"prefix"`
Colorize bool `json:"colorize"`
Expression string `json:"expression"`
regexp *regexp.Regexp
}
// NewWriterLogger creates a new WriterLogger from the provided WriteCloser.
// Optionally the level can be changed at the same time.
func (logger *WriterLogger) NewWriterLogger(out io.WriteCloser, level ...Level) {
logger.mu.Lock()
defer logger.mu.Unlock()
logger.out = out
switch logger.Flags {
case 0:
logger.Flags = LstdFlags
case -1:
logger.Flags = 0
}
if len(level) > 0 {
logger.Level = level[0]
}
logger.createExpression()
}
func (logger *WriterLogger) createExpression() {
if len(logger.Expression) > 0 {
var err error
logger.regexp, err = regexp.Compile(logger.Expression)
if err != nil {
logger.regexp = nil
}
}
}
// GetLevel returns the logging level for this logger
func (logger *WriterLogger) GetLevel() Level {
return logger.Level
}
// GetStacktraceLevel returns the stacktrace logging level for this logger
func (logger *WriterLogger) GetStacktraceLevel() Level {
return logger.StacktraceLevel
}
// Copy of cheap integer to fixed-width decimal to ascii from logger.
func itoa(buf *[]byte, i, wid int) {
var logger [20]byte
bp := len(logger) - 1
for i >= 10 || wid > 1 {
wid--
q := i / 10
logger[bp] = byte('0' + i - q*10)
bp--
i = q
}
// i < 10
logger[bp] = byte('0' + i)
*buf = append(*buf, logger[bp:]...)
}
func (logger *WriterLogger) createMsg(buf *[]byte, event *Event) {
*buf = append(*buf, logger.Prefix...)
t := event.time
if logger.Flags&(Ldate|Ltime|Lmicroseconds) != 0 {
if logger.Colorize {
*buf = append(*buf, fgCyanBytes...)
}
if logger.Flags&LUTC != 0 {
t = t.UTC()
}
if logger.Flags&Ldate != 0 {
year, month, day := t.Date()
itoa(buf, year, 4)
*buf = append(*buf, '/')
itoa(buf, int(month), 2)
*buf = append(*buf, '/')
itoa(buf, day, 2)
*buf = append(*buf, ' ')
}
if logger.Flags&(Ltime|Lmicroseconds) != 0 {
hour, min, sec := t.Clock()
itoa(buf, hour, 2)
*buf = append(*buf, ':')
itoa(buf, min, 2)
*buf = append(*buf, ':')
itoa(buf, sec, 2)
if logger.Flags&Lmicroseconds != 0 {
*buf = append(*buf, '.')
itoa(buf, t.Nanosecond()/1e3, 6)
}
*buf = append(*buf, ' ')
}
if logger.Colorize {
*buf = append(*buf, resetBytes...)
}
}
if logger.Flags&(Lshortfile|Llongfile) != 0 {
if logger.Colorize {
*buf = append(*buf, fgGreenBytes...)
}
file := event.filename
if logger.Flags&Lmedfile == Lmedfile {
startIndex := len(file) - 20
if startIndex > 0 {
file = "..." + file[startIndex:]
}
} else if logger.Flags&Lshortfile != 0 {
startIndex := strings.LastIndexByte(file, '/')
if startIndex > 0 && startIndex < len(file) {
file = file[startIndex+1:]
}
}
*buf = append(*buf, file...)
*buf = append(*buf, ':')
itoa(buf, event.line, -1)
if logger.Flags&(Lfuncname|Lshortfuncname) != 0 {
*buf = append(*buf, ':')
} else {
if logger.Colorize {
*buf = append(*buf, resetBytes...)
}
*buf = append(*buf, ' ')
}
}
if logger.Flags&(Lfuncname|Lshortfuncname) != 0 {
if logger.Colorize {
*buf = append(*buf, fgGreenBytes...)
}
funcname := event.caller
if logger.Flags&Lshortfuncname != 0 {
lastIndex := strings.LastIndexByte(funcname, '.')
if lastIndex > 0 && len(funcname) > lastIndex+1 {
funcname = funcname[lastIndex+1:]
}
}
*buf = append(*buf, funcname...)
if logger.Colorize {
*buf = append(*buf, resetBytes...)
}
*buf = append(*buf, ' ')
}
if logger.Flags&(Llevel|Llevelinitial) != 0 {
level := strings.ToUpper(event.level.String())
if logger.Colorize {
*buf = append(*buf, levelToColor[event.level]...)
}
*buf = append(*buf, '[')
if logger.Flags&Llevelinitial != 0 {
*buf = append(*buf, level[0])
} else {
*buf = append(*buf, level...)
}
*buf = append(*buf, ']')
if logger.Colorize {
*buf = append(*buf, resetBytes...)
}
*buf = append(*buf, ' ')
}
msg := []byte(event.msg)
if len(msg) > 0 && msg[len(msg)-1] == '\n' {
msg = msg[:len(msg)-1]
}
pawMode := allowColor
if !logger.Colorize {
pawMode = removeColor
}
baw := byteArrayWriter(*buf)
(&protectedANSIWriter{
w: &baw,
mode: pawMode,
}).Write(msg) //nolint:errcheck
*buf = baw
if event.stacktrace != "" && logger.StacktraceLevel <= event.level {
lines := bytes.Split([]byte(event.stacktrace), []byte("\n"))
if len(lines) > 1 {
for _, line := range lines {
*buf = append(*buf, "\n\t"...)
*buf = append(*buf, line...)
}
}
*buf = append(*buf, '\n')
}
*buf = append(*buf, '\n')
}
// LogEvent logs the event to the internal writer
func (logger *WriterLogger) LogEvent(event *Event) error {
if logger.Level > event.level {
return nil
}
logger.mu.Lock()
defer logger.mu.Unlock()
if !logger.Match(event) {
return nil
}
var buf []byte
logger.createMsg(&buf, event)
_, err := logger.out.Write(buf)
return err
}
// Match checks if the given event matches the logger's regexp expression
func (logger *WriterLogger) Match(event *Event) bool {
if logger.regexp == nil {
return true
}
if logger.regexp.Match([]byte(fmt.Sprintf("%s:%d:%s", event.filename, event.line, event.caller))) {
return true
}
// Match on the non-colored msg - therefore strip out colors
var msg []byte
baw := byteArrayWriter(msg)
(&protectedANSIWriter{
w: &baw,
mode: removeColor,
}).Write([]byte(event.msg)) //nolint:errcheck
msg = baw
return logger.regexp.Match(msg)
}
// Close the base logger
func (logger *WriterLogger) Close() {
logger.mu.Lock()
defer logger.mu.Unlock()
if logger.out != nil {
logger.out.Close()
}
}
// GetName returns empty for these provider loggers
func (logger *WriterLogger) GetName() string {
return ""
}

@ -1,275 +0,0 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"fmt"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
type CallbackWriteCloser struct {
callback func([]byte, bool)
}
func (c CallbackWriteCloser) Write(p []byte) (int, error) {
c.callback(p, false)
return len(p), nil
}
func (c CallbackWriteCloser) Close() error {
c.callback(nil, true)
return nil
}
func TestBaseLogger(t *testing.T) {
var written []byte
var closed bool
c := CallbackWriteCloser{
callback: func(p []byte, close bool) {
written = p
closed = close
},
}
prefix := "TestPrefix "
b := WriterLogger{
out: c,
Level: INFO,
Flags: LstdFlags | LUTC,
Prefix: prefix,
}
location, _ := time.LoadLocation("EST")
date := time.Date(2019, time.January, 13, 22, 3, 30, 15, location)
dateString := date.UTC().Format("2006/01/02 15:04:05")
event := Event{
level: INFO,
msg: "TEST MSG",
caller: "CALLER",
filename: "FULL/FILENAME",
line: 1,
time: date,
}
assert.Equal(t, INFO, b.GetLevel())
expected := fmt.Sprintf("%s%s %s:%d:%s [%c] %s\n", prefix, dateString, event.filename, event.line, event.caller, strings.ToUpper(event.level.String())[0], event.msg)
b.LogEvent(&event)
assert.Equal(t, expected, string(written))
assert.False(t, closed)
written = written[:0]
event.level = DEBUG
expected = ""
b.LogEvent(&event)
assert.Equal(t, expected, string(written))
assert.False(t, closed)
event.level = TRACE
expected = ""
b.LogEvent(&event)
assert.Equal(t, expected, string(written))
assert.False(t, closed)
event.level = WARN
expected = fmt.Sprintf("%s%s %s:%d:%s [%c] %s\n", prefix, dateString, event.filename, event.line, event.caller, strings.ToUpper(event.level.String())[0], event.msg)
b.LogEvent(&event)
assert.Equal(t, expected, string(written))
assert.False(t, closed)
written = written[:0]
event.level = ERROR
expected = fmt.Sprintf("%s%s %s:%d:%s [%c] %s\n", prefix, dateString, event.filename, event.line, event.caller, strings.ToUpper(event.level.String())[0], event.msg)
b.LogEvent(&event)
assert.Equal(t, expected, string(written))
assert.False(t, closed)
written = written[:0]
event.level = CRITICAL
expected = fmt.Sprintf("%s%s %s:%d:%s [%c] %s\n", prefix, dateString, event.filename, event.line, event.caller, strings.ToUpper(event.level.String())[0], event.msg)
b.LogEvent(&event)
assert.Equal(t, expected, string(written))
assert.False(t, closed)
written = written[:0]
b.Close()
assert.True(t, closed)
}
func TestBaseLoggerDated(t *testing.T) {
var written []byte
var closed bool
c := CallbackWriteCloser{
callback: func(p []byte, close bool) {
written = p
closed = close
},
}
prefix := ""
b := WriterLogger{
out: c,
Level: WARN,
Flags: Ldate | Ltime | Lmicroseconds | Lshortfile | Llevel,
Prefix: prefix,
}
location, _ := time.LoadLocation("EST")
date := time.Date(2019, time.January, 13, 22, 3, 30, 115, location)
dateString := date.Format("2006/01/02 15:04:05.000000")
event := Event{
level: WARN,
msg: "TEST MESSAGE TEST\n",
caller: "CALLER",
filename: "FULL/FILENAME",
line: 1,
time: date,
}
assert.Equal(t, WARN, b.GetLevel())
expected := fmt.Sprintf("%s%s %s:%d [%s] %s", prefix, dateString, "FILENAME", event.line, strings.ToUpper(event.level.String()), event.msg)
b.LogEvent(&event)
assert.Equal(t, expected, string(written))
assert.False(t, closed)
written = written[:0]
event.level = INFO
expected = ""
b.LogEvent(&event)
assert.Equal(t, expected, string(written))
assert.False(t, closed)
written = written[:0]
event.level = ERROR
expected = fmt.Sprintf("%s%s %s:%d [%s] %s", prefix, dateString, "FILENAME", event.line, strings.ToUpper(event.level.String()), event.msg)
b.LogEvent(&event)
assert.Equal(t, expected, string(written))
assert.False(t, closed)
written = written[:0]
event.level = DEBUG
expected = ""
b.LogEvent(&event)
assert.Equal(t, expected, string(written))
assert.False(t, closed)
written = written[:0]
event.level = CRITICAL
expected = fmt.Sprintf("%s%s %s:%d [%s] %s", prefix, dateString, "FILENAME", event.line, strings.ToUpper(event.level.String()), event.msg)
b.LogEvent(&event)
assert.Equal(t, expected, string(written))
assert.False(t, closed)
written = written[:0]
event.level = TRACE
expected = ""
b.LogEvent(&event)
assert.Equal(t, expected, string(written))
assert.False(t, closed)
written = written[:0]
b.Close()
assert.True(t, closed)
}
func TestBaseLoggerMultiLineNoFlagsRegexp(t *testing.T) {
var written []byte
var closed bool
c := CallbackWriteCloser{
callback: func(p []byte, close bool) {
written = p
closed = close
},
}
prefix := ""
b := WriterLogger{
Level: DEBUG,
StacktraceLevel: ERROR,
Flags: -1,
Prefix: prefix,
Expression: "FILENAME",
}
b.NewWriterLogger(c)
location, _ := time.LoadLocation("EST")
date := time.Date(2019, time.January, 13, 22, 3, 30, 115, location)
event := Event{
level: DEBUG,
msg: "TEST\nMESSAGE\nTEST",
caller: "CALLER",
filename: "FULL/FILENAME",
line: 1,
time: date,
}
assert.Equal(t, DEBUG, b.GetLevel())
expected := "TEST\n\tMESSAGE\n\tTEST\n"
b.LogEvent(&event)
assert.Equal(t, expected, string(written))
assert.False(t, closed)
written = written[:0]
event.filename = "ELSEWHERE"
b.LogEvent(&event)
assert.Equal(t, "", string(written))
assert.False(t, closed)
written = written[:0]
event.caller = "FILENAME"
b.LogEvent(&event)
assert.Equal(t, expected, string(written))
assert.False(t, closed)
written = written[:0]
event = Event{
level: DEBUG,
msg: "TEST\nFILENAME\nTEST",
caller: "CALLER",
filename: "FULL/ELSEWHERE",
line: 1,
time: date,
}
expected = "TEST\n\tFILENAME\n\tTEST\n"
b.LogEvent(&event)
assert.Equal(t, expected, string(written))
assert.False(t, closed)
written = written[:0]
}
func TestBrokenRegexp(t *testing.T) {
var closed bool
c := CallbackWriteCloser{
callback: func(p []byte, close bool) {
closed = close
},
}
b := WriterLogger{
Level: DEBUG,
StacktraceLevel: ERROR,
Flags: -1,
Prefix: prefix,
Expression: "\\",
}
b.NewWriterLogger(c)
assert.Empty(t, b.regexp)
b.Close()
assert.True(t, closed)
}

@ -75,18 +75,18 @@ func SetLogSQL(ctx context.Context, on bool) ResponseExtra {
// LoggerOptions represents the options for the add logger call
type LoggerOptions struct {
Group string
Name string
Logger string
Writer string
Mode string
Config map[string]interface{}
}
// AddLogger adds a logger
func AddLogger(ctx context.Context, group, name, mode string, config map[string]interface{}) ResponseExtra {
func AddLogger(ctx context.Context, logger, writer, mode string, config map[string]interface{}) ResponseExtra {
reqURL := setting.LocalURL + "api/internal/manager/add-logger"
req := newInternalRequest(ctx, reqURL, "POST", LoggerOptions{
Group: group,
Name: name,
Logger: logger,
Writer: writer,
Mode: mode,
Config: config,
})
@ -94,8 +94,8 @@ func AddLogger(ctx context.Context, group, name, mode string, config map[string]
}
// RemoveLogger removes a logger
func RemoveLogger(ctx context.Context, group, name string) ResponseExtra {
reqURL := setting.LocalURL + fmt.Sprintf("api/internal/manager/remove-logger/%s/%s", url.PathEscape(group), url.PathEscape(name))
func RemoveLogger(ctx context.Context, logger, writer string) ResponseExtra {
reqURL := setting.LocalURL + fmt.Sprintf("api/internal/manager/remove-logger/%s/%s", url.PathEscape(logger), url.PathEscape(writer))
req := newInternalRequest(ctx, reqURL, "POST")
return requestJSONUserMsg(req, "Removed")
}

@ -33,6 +33,58 @@ type ConfigProvider interface {
Save() error
}
// ConfigSectionKey only searches the keys in the given section, but it is O(n).
// ini package has a special behavior: with "[sec] a=1" and an empty "[sec.sub]",
// then in "[sec.sub]", Key()/HasKey() can always see "a=1" because it always tries parent sections.
// It returns nil if the key doesn't exist.
func ConfigSectionKey(sec ConfigSection, key string) *ini.Key {
if sec == nil {
return nil
}
for _, k := range sec.Keys() {
if k.Name() == key {
return k
}
}
return nil
}
func ConfigSectionKeyString(sec ConfigSection, key string, def ...string) string {
k := ConfigSectionKey(sec, key)
if k != nil && k.String() != "" {
return k.String()
}
if len(def) > 0 {
return def[0]
}
return ""
}
// ConfigInheritedKey works like ini.Section.Key(), but it always returns a new key instance, it is O(n) because NewKey is O(n)
// and the returned key is safe to be used with "MustXxx", it doesn't change the parent's values.
// Otherwise, ini.Section.Key().MustXxx would pollute the parent section's keys.
// It never returns nil.
func ConfigInheritedKey(sec ConfigSection, key string) *ini.Key {
k := sec.Key(key)
if k != nil && k.String() != "" {
newKey, _ := sec.NewKey(k.Name(), k.String())
return newKey
}
newKey, _ := sec.NewKey(key, "")
return newKey
}
func ConfigInheritedKeyString(sec ConfigSection, key string, def ...string) string {
k := sec.Key(key)
if k != nil && k.String() != "" {
return k.String()
}
if len(def) > 0 {
return def[0]
}
return ""
}
type iniFileConfigProvider struct {
opts *Options
*ini.File

@ -0,0 +1,66 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package setting
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestConfigProviderBehaviors(t *testing.T) {
t.Run("BuggyKeyOverwritten", func(t *testing.T) {
cfg, _ := NewConfigProviderFromData(`
[foo]
key =
`)
sec := cfg.Section("foo")
secSub := cfg.Section("foo.bar")
secSub.Key("key").MustString("1") // try to read a key from subsection
assert.Equal(t, "1", sec.Key("key").String()) // TODO: BUGGY! the key in [foo] is overwritten
})
t.Run("SubsectionSeeParentKeys", func(t *testing.T) {
cfg, _ := NewConfigProviderFromData(`
[foo]
key = 123
`)
secSub := cfg.Section("foo.bar.xxx")
assert.Equal(t, "123", secSub.Key("key").String())
})
}
func TestConfigProviderHelper(t *testing.T) {
cfg, _ := NewConfigProviderFromData(`
[foo]
empty =
key = 123
`)
sec := cfg.Section("foo")
secSub := cfg.Section("foo.bar")
// test empty key
assert.Equal(t, "def", ConfigSectionKeyString(sec, "empty", "def"))
assert.Equal(t, "xyz", ConfigSectionKeyString(secSub, "empty", "xyz"))
// test non-inherited key, only see the keys in current section
assert.NotNil(t, ConfigSectionKey(sec, "key"))
assert.Nil(t, ConfigSectionKey(secSub, "key"))
// test default behavior
assert.Equal(t, "123", ConfigSectionKeyString(sec, "key"))
assert.Equal(t, "", ConfigSectionKeyString(secSub, "key"))
assert.Equal(t, "def", ConfigSectionKeyString(secSub, "key", "def"))
assert.Equal(t, "123", ConfigInheritedKeyString(secSub, "key"))
// Workaround for ini package's BuggyKeyOverwritten behavior
assert.Equal(t, "", ConfigSectionKeyString(sec, "empty"))
assert.Equal(t, "", ConfigSectionKeyString(secSub, "empty"))
assert.Equal(t, "def", ConfigInheritedKey(secSub, "empty").MustString("def"))
assert.Equal(t, "def", ConfigInheritedKey(secSub, "empty").MustString("xyz"))
assert.Equal(t, "", ConfigSectionKeyString(sec, "empty"))
assert.Equal(t, "def", ConfigSectionKeyString(secSub, "empty"))
}

@ -92,7 +92,7 @@ func loadDBSetting(rootCfg ConfigProvider) {
Database.MaxOpenConns = sec.Key("MAX_OPEN_CONNS").MustInt(0)
Database.IterateBufferSize = sec.Key("ITERATE_BUFFER_SIZE").MustInt(50)
Database.LogSQL = sec.Key("LOG_SQL").MustBool(true)
Database.LogSQL = sec.Key("LOG_SQL").MustBool(false)
Database.DBConnectRetries = sec.Key("DB_RETRIES").MustInt(10)
Database.DBConnectBackoff = sec.Key("DB_RETRY_BACKOFF").MustDuration(3 * time.Second)
Database.AutoMigration = sec.Key("AUTO_MIGRATION").MustBool(true)

@ -10,384 +10,251 @@ import (
"path"
"path/filepath"
"strings"
"sync"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util"
)
var (
filenameSuffix = ""
descriptionLock = sync.RWMutex{}
logDescriptions = make(map[string]*LogDescription)
)
type LogGlobalConfig struct {
RootPath string
// Log settings
var Log struct {
Mode string
Level log.Level
StacktraceLogLevel string
RootPath string
EnableSSHLog bool
EnableXORMLog bool
StacktraceLogLevel log.Level
BufferLen int
DisableRouterLog bool
EnableSSHLog bool
EnableAccessLog bool
AccessLogTemplate string
BufferLength int64
RequestIDHeaders []string
}
// GetLogDescriptions returns a race safe set of descriptions
func GetLogDescriptions() map[string]*LogDescription {
descriptionLock.RLock()
defer descriptionLock.RUnlock()
descs := make(map[string]*LogDescription, len(logDescriptions))
for k, v := range logDescriptions {
subLogDescriptions := make([]SubLogDescription, len(v.SubLogDescriptions))
copy(subLogDescriptions, v.SubLogDescriptions)
descs[k] = &LogDescription{
Name: v.Name,
SubLogDescriptions: subLogDescriptions,
}
var Log LogGlobalConfig
const accessLogTemplateDefault = `{{.Ctx.RemoteHost}} - {{.Identity}} {{.Start.Format "[02/Jan/2006:15:04:05 -0700]" }} "{{.Ctx.Req.Method}} {{.Ctx.Req.URL.RequestURI}} {{.Ctx.Req.Proto}}" {{.ResponseWriter.Status}} {{.ResponseWriter.Size}} "{{.Ctx.Req.Referer}}" "{{.Ctx.Req.UserAgent}}"`
func loadLogGlobalFrom(rootCfg ConfigProvider) {
sec := rootCfg.Section("log")
Log.Level = log.LevelFromString(sec.Key("LEVEL").MustString(log.INFO.String()))
Log.StacktraceLogLevel = log.LevelFromString(sec.Key("STACKTRACE_LEVEL").MustString(log.NONE.String()))
Log.BufferLen = sec.Key("BUFFER_LEN").MustInt(10000)
Log.Mode = sec.Key("MODE").MustString("console")
Log.RootPath = sec.Key("ROOT_PATH").MustString(path.Join(AppWorkPath, "log"))
if !filepath.IsAbs(Log.RootPath) {
Log.RootPath = filepath.Join(AppWorkPath, Log.RootPath)
}
return descs
}
Log.RootPath = util.FilePathJoinAbs(Log.RootPath)
Log.EnableSSHLog = sec.Key("ENABLE_SSH_LOG").MustBool(false)
// AddLogDescription adds a set of descriptions to the complete description
func AddLogDescription(key string, description *LogDescription) {
descriptionLock.Lock()
defer descriptionLock.Unlock()
logDescriptions[key] = description
Log.AccessLogTemplate = sec.Key("ACCESS_LOG_TEMPLATE").MustString(accessLogTemplateDefault)
Log.RequestIDHeaders = sec.Key("REQUEST_ID_HEADERS").Strings(",")
}
// AddSubLogDescription adds a sub log description
func AddSubLogDescription(key string, subLogDescription SubLogDescription) bool {
descriptionLock.Lock()
defer descriptionLock.Unlock()
desc, ok := logDescriptions[key]
if !ok {
return false
func prepareLoggerConfig(rootCfg ConfigProvider) {
sec := rootCfg.Section("log")
if !sec.HasKey("logger.default.MODE") {
sec.Key("logger.default.MODE").MustString(",")
}
for i, sub := range desc.SubLogDescriptions {
if sub.Name == subLogDescription.Name {
desc.SubLogDescriptions[i] = subLogDescription
return true
deprecatedSetting(rootCfg, "log", "ACCESS", "log", "logger.access.MODE", "1.21")
deprecatedSetting(rootCfg, "log", "ENABLE_ACCESS_LOG", "log", "logger.access.MODE", "1.21")
if val := sec.Key("ACCESS").String(); val != "" {
sec.Key("logger.access.MODE").MustString(val)
}
if sec.HasKey("ENABLE_ACCESS_LOG") && !sec.Key("ENABLE_ACCESS_LOG").MustBool() {
sec.Key("logger.access.MODE").SetValue("")
}
desc.SubLogDescriptions = append(desc.SubLogDescriptions, subLogDescription)
return true
}
// RemoveSubLogDescription removes a sub log description
func RemoveSubLogDescription(key, name string) bool {
descriptionLock.Lock()
defer descriptionLock.Unlock()
desc, ok := logDescriptions[key]
if !ok {
return false
deprecatedSetting(rootCfg, "log", "ROUTER", "log", "logger.router.MODE", "1.21")
deprecatedSetting(rootCfg, "log", "DISABLE_ROUTER_LOG", "log", "logger.router.MODE", "1.21")
if val := sec.Key("ROUTER").String(); val != "" {
sec.Key("logger.router.MODE").MustString(val)
}
for i, sub := range desc.SubLogDescriptions {
if sub.Name == name {
desc.SubLogDescriptions = append(desc.SubLogDescriptions[:i], desc.SubLogDescriptions[i+1:]...)
return true
if !sec.HasKey("logger.router.MODE") {
sec.Key("logger.router.MODE").MustString(",") // use default logger
}
if sec.HasKey("DISABLE_ROUTER_LOG") && sec.Key("DISABLE_ROUTER_LOG").MustBool() {
sec.Key("logger.router.MODE").SetValue("")
}
return false
}
type defaultLogOptions struct {
levelName string // LogLevel
flags string
filename string // path.Join(LogRootPath, "gitea.log")
bufferLength int64
disableConsole bool
}
func newDefaultLogOptions() defaultLogOptions {
return defaultLogOptions{
levelName: Log.Level.String(),
flags: "stdflags",
filename: filepath.Join(Log.RootPath, "gitea.log"),
bufferLength: 10000,
disableConsole: false,
deprecatedSetting(rootCfg, "log", "XORM", "log", "logger.xorm.MODE", "1.21")
deprecatedSetting(rootCfg, "log", "ENABLE_XORM_LOG", "log", "logger.xorm.MODE", "1.21")
if val := sec.Key("XORM").String(); val != "" {
sec.Key("logger.xorm.MODE").MustString(val)
}
if !sec.HasKey("logger.xorm.MODE") {
sec.Key("logger.xorm.MODE").MustString(",") // use default logger
}
if sec.HasKey("ENABLE_XORM_LOG") && !sec.Key("ENABLE_XORM_LOG").MustBool() {
sec.Key("logger.xorm.MODE").SetValue("")
}
}
// SubLogDescription describes a sublogger
type SubLogDescription struct {
Name string
Provider string
Config string
}
// LogDescription describes a named logger
type LogDescription struct {
Name string
SubLogDescriptions []SubLogDescription
}
func getLogLevel(section ConfigSection, key string, defaultValue log.Level) log.Level {
value := section.Key(key).MustString(defaultValue.String())
return log.FromString(value)
}
func getStacktraceLogLevel(section ConfigSection, key, defaultValue string) string {
value := section.Key(key).MustString(defaultValue)
return log.FromString(value).String()
func LogPrepareFilenameForWriter(fileName, defaultFileName string) string {
if fileName == "" {
fileName = defaultFileName
}
if !filepath.IsAbs(fileName) {
fileName = filepath.Join(Log.RootPath, fileName)
} else {
fileName = filepath.Clean(fileName)
}
if err := os.MkdirAll(filepath.Dir(fileName), os.ModePerm); err != nil {
panic(fmt.Sprintf("unable to create directory for log %q: %v", fileName, err.Error()))
}
return fileName
}
func loadLogFrom(rootCfg ConfigProvider) {
sec := rootCfg.Section("log")
Log.Level = getLogLevel(sec, "LEVEL", log.INFO)
Log.StacktraceLogLevel = getStacktraceLogLevel(sec, "STACKTRACE_LEVEL", "None")
Log.RootPath = sec.Key("ROOT_PATH").MustString(path.Join(AppWorkPath, "log"))
forcePathSeparator(Log.RootPath)
Log.BufferLength = sec.Key("BUFFER_LEN").MustInt64(10000)
Log.EnableSSHLog = sec.Key("ENABLE_SSH_LOG").MustBool(false)
Log.EnableAccessLog = sec.Key("ENABLE_ACCESS_LOG").MustBool(false)
Log.AccessLogTemplate = sec.Key("ACCESS_LOG_TEMPLATE").MustString(
`{{.Ctx.RemoteHost}} - {{.Identity}} {{.Start.Format "[02/Jan/2006:15:04:05 -0700]" }} "{{.Ctx.Req.Method}} {{.Ctx.Req.URL.RequestURI}} {{.Ctx.Req.Proto}}" {{.ResponseWriter.Status}} {{.ResponseWriter.Size}} "{{.Ctx.Req.Referer}}" "{{.Ctx.Req.UserAgent}}"`,
)
Log.RequestIDHeaders = sec.Key("REQUEST_ID_HEADERS").Strings(",")
// the `MustString` updates the default value, and `log.ACCESS` is used by `generateNamedLogger("access")` later
_ = rootCfg.Section("log").Key("ACCESS").MustString("file")
sec.Key("ROUTER").MustString("console")
// Allow [log] DISABLE_ROUTER_LOG to override [server] DISABLE_ROUTER_LOG
Log.DisableRouterLog = sec.Key("DISABLE_ROUTER_LOG").MustBool(Log.DisableRouterLog)
Log.EnableXORMLog = rootCfg.Section("log").Key("ENABLE_XORM_LOG").MustBool(true)
}
func loadLogModeByName(rootCfg ConfigProvider, loggerName, modeName string) (writerName, writerType string, writerMode log.WriterMode, err error) {
sec := rootCfg.Section("log." + modeName)
func generateLogConfig(sec ConfigSection, name string, defaults defaultLogOptions) (mode, jsonConfig, levelName string) {
level := getLogLevel(sec, "LEVEL", Log.Level)
levelName = level.String()
stacktraceLevelName := getStacktraceLogLevel(sec, "STACKTRACE_LEVEL", Log.StacktraceLogLevel)
stacktraceLevel := log.FromString(stacktraceLevelName)
mode = name
keys := sec.Keys()
logPath := defaults.filename
flags := log.FlagsFromString(defaults.flags)
expression := ""
prefix := ""
for _, key := range keys {
switch key.Name() {
case "MODE":
mode = key.MustString(name)
case "FILE_NAME":
logPath = key.MustString(defaults.filename)
forcePathSeparator(logPath)
if !filepath.IsAbs(logPath) {
logPath = path.Join(Log.RootPath, logPath)
}
case "FLAGS":
flags = log.FlagsFromString(key.MustString(defaults.flags))
case "EXPRESSION":
expression = key.MustString("")
case "PREFIX":
prefix = key.MustString("")
}
writerMode = log.WriterMode{}
writerType = ConfigSectionKeyString(sec, "MODE")
if writerType == "" {
writerType = modeName
}
logConfig := map[string]interface{}{
"level": level.String(),
"expression": expression,
"prefix": prefix,
"flags": flags,
"stacktraceLevel": stacktraceLevel.String(),
writerName = modeName
defaultFlags := "stdflags"
defaultFilaName := "gitea.log"
if loggerName == "access" {
// "access" logger is special, by default it doesn't have output flags, so it also needs a new writer name to avoid conflicting with other writers.
// so "access" logger's writer name is usually "file.access" or "console.access"
writerName += ".access"
defaultFlags = "none"
defaultFilaName = "access.log"
}
// Generate log configuration.
switch mode {
writerMode.Level = log.LevelFromString(ConfigInheritedKeyString(sec, "LEVEL", Log.Level.String()))
writerMode.StacktraceLevel = log.LevelFromString(ConfigInheritedKeyString(sec, "STACKTRACE_LEVEL", Log.StacktraceLogLevel.String()))
writerMode.Prefix = ConfigInheritedKeyString(sec, "PREFIX")
writerMode.Expression = ConfigInheritedKeyString(sec, "EXPRESSION")
writerMode.Flags = log.FlagsFromString(ConfigInheritedKeyString(sec, "FLAGS", defaultFlags))
switch writerType {
case "console":
useStderr := sec.Key("STDERR").MustBool(false)
logConfig["stderr"] = useStderr
useStderr := ConfigInheritedKey(sec, "STDERR").MustBool(false)
defaultCanColor := log.CanColorStdout
if useStderr {
logConfig["colorize"] = sec.Key("COLORIZE").MustBool(log.CanColorStderr)
} else {
logConfig["colorize"] = sec.Key("COLORIZE").MustBool(log.CanColorStdout)
defaultCanColor = log.CanColorStderr
}
writerOption := log.WriterConsoleOption{Stderr: useStderr}
writerMode.Colorize = ConfigInheritedKey(sec, "COLORIZE").MustBool(defaultCanColor)
writerMode.WriterOption = writerOption
case "file":
if err := os.MkdirAll(path.Dir(logPath), os.ModePerm); err != nil {
panic(err.Error())
}
logConfig["filename"] = logPath + filenameSuffix
logConfig["rotate"] = sec.Key("LOG_ROTATE").MustBool(true)
logConfig["maxsize"] = 1 << uint(sec.Key("MAX_SIZE_SHIFT").MustInt(28))
logConfig["daily"] = sec.Key("DAILY_ROTATE").MustBool(true)
logConfig["maxdays"] = sec.Key("MAX_DAYS").MustInt(7)
logConfig["compress"] = sec.Key("COMPRESS").MustBool(true)
logConfig["compressionLevel"] = sec.Key("COMPRESSION_LEVEL").MustInt(-1)
fileName := LogPrepareFilenameForWriter(ConfigInheritedKey(sec, "FILE_NAME").String(), defaultFilaName)
writerOption := log.WriterFileOption{}
writerOption.FileName = fileName + filenameSuffix // FIXME: the suffix doesn't seem right, see its related comments
writerOption.LogRotate = ConfigInheritedKey(sec, "LOG_ROTATE").MustBool(true)
writerOption.MaxSize = 1 << uint(ConfigInheritedKey(sec, "MAX_SIZE_SHIFT").MustInt(28))
writerOption.DailyRotate = ConfigInheritedKey(sec, "DAILY_ROTATE").MustBool(true)
writerOption.MaxDays = ConfigInheritedKey(sec, "MAX_DAYS").MustInt(7)
writerOption.Compress = ConfigInheritedKey(sec, "COMPRESS").MustBool(true)
writerOption.CompressionLevel = ConfigInheritedKey(sec, "COMPRESSION_LEVEL").MustInt(-1)
writerMode.WriterOption = writerOption
case "conn":
logConfig["reconnectOnMsg"] = sec.Key("RECONNECT_ON_MSG").MustBool()
logConfig["reconnect"] = sec.Key("RECONNECT").MustBool()
logConfig["net"] = sec.Key("PROTOCOL").In("tcp", []string{"tcp", "unix", "udp"})
logConfig["addr"] = sec.Key("ADDR").MustString(":7020")
case "smtp":
logConfig["username"] = sec.Key("USER").MustString("example@example.com")
logConfig["password"] = sec.Key("PASSWD").MustString("******")
logConfig["host"] = sec.Key("HOST").MustString("127.0.0.1:25")
sendTos := strings.Split(sec.Key("RECEIVERS").MustString(""), ",")
for i, address := range sendTos {
sendTos[i] = strings.TrimSpace(address)
writerOption := log.WriterConnOption{}
writerOption.ReconnectOnMsg = ConfigInheritedKey(sec, "RECONNECT_ON_MSG").MustBool()
writerOption.Reconnect = ConfigInheritedKey(sec, "RECONNECT").MustBool()
writerOption.Protocol = ConfigInheritedKey(sec, "PROTOCOL").In("tcp", []string{"tcp", "unix", "udp"})
writerOption.Addr = ConfigInheritedKey(sec, "ADDR").MustString(":7020")
writerMode.WriterOption = writerOption
default:
if !log.HasEventWriter(writerType) {
return "", "", writerMode, fmt.Errorf("invalid log writer type (mode): %s", writerType)
}
logConfig["sendTos"] = sendTos
logConfig["subject"] = sec.Key("SUBJECT").MustString("Diagnostic message from Gitea")
}
logConfig["colorize"] = sec.Key("COLORIZE").MustBool(false)
byteConfig, err := json.Marshal(logConfig)
if err != nil {
log.Error("Failed to marshal log configuration: %v %v", logConfig, err)
return
}
jsonConfig = string(byteConfig)
return mode, jsonConfig, levelName
return writerName, writerType, writerMode, nil
}
func generateNamedLogger(rootCfg ConfigProvider, key string, options defaultLogOptions) *LogDescription {
description := LogDescription{
Name: key,
}
sections := strings.Split(rootCfg.Section("log").Key(strings.ToUpper(key)).MustString(""), ",")
for i := 0; i < len(sections); i++ {
sections[i] = strings.TrimSpace(sections[i])
}
var filenameSuffix = ""
for _, name := range sections {
if len(name) == 0 || (name == "console" && options.disableConsole) {
continue
}
sec, err := rootCfg.GetSection("log." + name + "." + key)
if err != nil {
sec, _ = rootCfg.NewSection("log." + name + "." + key)
}
// RestartLogsWithPIDSuffix restarts the logs with a PID suffix on files
// FIXME: it seems not right, it breaks log rotating or log collectors
func RestartLogsWithPIDSuffix() {
filenameSuffix = fmt.Sprintf(".%d", os.Getpid())
initAllLoggers() // when forking, before restarting, rename logger file and re-init all loggers
}
provider, config, levelName := generateLogConfig(sec, name, options)
func InitLoggersForTest() {
initAllLoggers()
}
if err := log.NewNamedLogger(key, options.bufferLength, name, provider, config); err != nil {
// Maybe panic here?
log.Error("Could not create new named logger: %v", err.Error())
}
// initAllLoggers creates all the log services
func initAllLoggers() {
initManagedLoggers(log.GetManager(), CfgProvider)
description.SubLogDescriptions = append(description.SubLogDescriptions, SubLogDescription{
Name: name,
Provider: provider,
Config: config,
})
log.Info("%s Log: %s(%s:%s)", util.ToTitleCase(key), util.ToTitleCase(name), provider, levelName)
}
golog.SetFlags(0)
golog.SetPrefix("")
golog.SetOutput(log.LoggerToWriter(log.GetLogger(log.DEFAULT).Info))
}
AddLogDescription(key, &description)
func initManagedLoggers(manager *log.LoggerManager, cfg ConfigProvider) {
loadLogGlobalFrom(cfg)
prepareLoggerConfig(cfg)
return &description
initLoggerByName(manager, cfg, log.DEFAULT) // default
initLoggerByName(manager, cfg, "access")
initLoggerByName(manager, cfg, "router")
initLoggerByName(manager, cfg, "xorm")
}
// initLogFrom initializes logging with settings from configuration provider
func initLogFrom(rootCfg ConfigProvider) {
func initLoggerByName(manager *log.LoggerManager, rootCfg ConfigProvider, loggerName string) {
sec := rootCfg.Section("log")
options := newDefaultLogOptions()
options.bufferLength = Log.BufferLength
keyPrefix := "logger." + loggerName
description := LogDescription{
Name: log.DEFAULT,
disabled := sec.HasKey(keyPrefix+".MODE") && sec.Key(keyPrefix+".MODE").String() == ""
if disabled {
return
}
sections := strings.Split(sec.Key("MODE").MustString("console"), ",")
modeVal := sec.Key(keyPrefix + ".MODE").String()
if modeVal == "," {
modeVal = Log.Mode
}
useConsole := false
for _, name := range sections {
name = strings.TrimSpace(name)
if name == "" {
var eventWriters []log.EventWriter
modes := strings.Split(modeVal, ",")
for _, modeName := range modes {
modeName = strings.TrimSpace(modeName)
if modeName == "" {
continue
}
if name == "console" {
useConsole = true
}
sec, err := rootCfg.GetSection("log." + name + ".default")
writerName, writerType, writerMode, err := loadLogModeByName(rootCfg, loggerName, modeName)
if err != nil {
sec, err = rootCfg.GetSection("log." + name)
if err != nil {
sec, _ = rootCfg.NewSection("log." + name)
log.FallbackErrorf("Failed to load writer mode %q for logger %s: %v", modeName, loggerName, err)
continue
}
if writerMode.BufferLen == 0 {
writerMode.BufferLen = Log.BufferLen
}
provider, config, levelName := generateLogConfig(sec, name, options)
log.NewLogger(options.bufferLength, name, provider, config)
description.SubLogDescriptions = append(description.SubLogDescriptions, SubLogDescription{
Name: name,
Provider: provider,
Config: config,
})
log.Info("Gitea Log Mode: %s(%s:%s)", util.ToTitleCase(name), util.ToTitleCase(provider), levelName)
eventWriter := manager.GetSharedWriter(writerName)
if eventWriter == nil {
eventWriter, err = manager.NewSharedWriter(writerName, writerType, writerMode)
if err != nil {
log.FallbackErrorf("Failed to create event writer for logger %s: %v", loggerName, err)
continue
}
AddLogDescription(log.DEFAULT, &description)
if !useConsole {
log.Info("According to the configuration, subsequent logs will not be printed to the console")
if err := log.DelLogger("console"); err != nil {
log.Fatal("Cannot delete console logger: %v", err)
}
eventWriters = append(eventWriters, eventWriter)
}
// Finally redirect the default golog to here
golog.SetFlags(0)
golog.SetPrefix("")
golog.SetOutput(log.NewLoggerAsWriter("INFO", log.GetLogger(log.DEFAULT)))
}
// RestartLogsWithPIDSuffix restarts the logs with a PID suffix on files
func RestartLogsWithPIDSuffix() {
filenameSuffix = fmt.Sprintf(".%d", os.Getpid())
InitLogs(false)
manager.GetLogger(loggerName).RemoveAllWriters().AddWriters(eventWriters...)
}
// InitLogs creates all the log services
func InitLogs(disableConsole bool) {
initLogFrom(CfgProvider)
if !Log.DisableRouterLog {
options := newDefaultLogOptions()
options.filename = filepath.Join(Log.RootPath, "router.log")
options.flags = "date,time" // For the router we don't want any prefixed flags
options.bufferLength = Log.BufferLength
generateNamedLogger(CfgProvider, "router", options)
}
if Log.EnableAccessLog {
options := newDefaultLogOptions()
options.filename = filepath.Join(Log.RootPath, "access.log")
options.flags = "" // For the router we don't want any prefixed flags
options.bufferLength = Log.BufferLength
generateNamedLogger(CfgProvider, "access", options)
}
initSQLLogFrom(CfgProvider, disableConsole)
func InitSQLLoggersForCli(level log.Level) {
log.SetConsoleLogger("xorm", "console", level)
}
// InitSQLLog initializes xorm logger setting
func InitSQLLog(disableConsole bool) {
initSQLLogFrom(CfgProvider, disableConsole)
func IsAccessLogEnabled() bool {
return log.IsLoggerEnabled("access")
}
func initSQLLogFrom(rootCfg ConfigProvider, disableConsole bool) {
if Log.EnableXORMLog {
options := newDefaultLogOptions()
options.filename = filepath.Join(Log.RootPath, "xorm.log")
options.bufferLength = Log.BufferLength
options.disableConsole = disableConsole
rootCfg.Section("log").Key("XORM").MustString(",")
generateNamedLogger(rootCfg, "xorm", options)
}
func IsRouteLogEnabled() bool {
return log.IsLoggerEnabled("router")
}

@ -0,0 +1,387 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package setting
import (
"path/filepath"
"strings"
"testing"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func initLoggersByConfig(t *testing.T, config string) (*log.LoggerManager, func()) {
oldLogConfig := Log
Log = LogGlobalConfig{}
defer func() {
Log = oldLogConfig
}()
cfg, err := NewConfigProviderFromData(config)
assert.NoError(t, err)
manager := log.NewManager()
initManagedLoggers(manager, cfg)
return manager, manager.Close
}
func toJSON(v interface{}) string {
b, _ := json.MarshalIndent(v, "", "\t")
return string(b)
}
func TestLogConfigDefault(t *testing.T) {
manager, managerClose := initLoggersByConfig(t, ``)
defer managerClose()
writerDump := `
{
"console": {
"BufferLen": 10000,
"Colorize": false,
"Expression": "",
"Flags": "stdflags",
"Level": "info",
"Prefix": "",
"StacktraceLevel": "none",
"WriterOption": {
"Stderr": false
},
"WriterType": "console"
}
}
`
dump := manager.GetLogger(log.DEFAULT).DumpWriters()
require.JSONEq(t, writerDump, toJSON(dump))
dump = manager.GetLogger("access").DumpWriters()
require.JSONEq(t, "{}", toJSON(dump))
dump = manager.GetLogger("router").DumpWriters()
require.JSONEq(t, writerDump, toJSON(dump))
dump = manager.GetLogger("xorm").DumpWriters()
require.JSONEq(t, writerDump, toJSON(dump))
}
func TestLogConfigDisable(t *testing.T) {
manager, managerClose := initLoggersByConfig(t, `
[log]
logger.router.MODE =
logger.xorm.MODE =
`)
defer managerClose()
writerDump := `
{
"console": {
"BufferLen": 10000,
"Colorize": false,
"Expression": "",
"Flags": "stdflags",
"Level": "info",
"Prefix": "",
"StacktraceLevel": "none",
"WriterOption": {
"Stderr": false
},
"WriterType": "console"
}
}
`
dump := manager.GetLogger(log.DEFAULT).DumpWriters()
require.JSONEq(t, writerDump, toJSON(dump))
dump = manager.GetLogger("access").DumpWriters()
require.JSONEq(t, "{}", toJSON(dump))
dump = manager.GetLogger("router").DumpWriters()
require.JSONEq(t, "{}", toJSON(dump))
dump = manager.GetLogger("xorm").DumpWriters()
require.JSONEq(t, "{}", toJSON(dump))
}
func TestLogConfigLegacyDefault(t *testing.T) {
manager, managerClose := initLoggersByConfig(t, `
[log]
MODE = console
`)
defer managerClose()
writerDump := `
{
"console": {
"BufferLen": 10000,
"Colorize": false,
"Expression": "",
"Flags": "stdflags",
"Level": "info",
"Prefix": "",
"StacktraceLevel": "none",
"WriterOption": {
"Stderr": false
},
"WriterType": "console"
}
}
`
dump := manager.GetLogger(log.DEFAULT).DumpWriters()
require.JSONEq(t, writerDump, toJSON(dump))
dump = manager.GetLogger("access").DumpWriters()
require.JSONEq(t, "{}", toJSON(dump))
dump = manager.GetLogger("router").DumpWriters()
require.JSONEq(t, writerDump, toJSON(dump))
dump = manager.GetLogger("xorm").DumpWriters()
require.JSONEq(t, writerDump, toJSON(dump))
}
func TestLogConfigLegacyMode(t *testing.T) {
tempDir := t.TempDir()
tempPath := func(file string) string {
return filepath.Join(tempDir, file)
}
manager, managerClose := initLoggersByConfig(t, `
[log]
ROOT_PATH = `+tempDir+`
MODE = file
ROUTER = file
ACCESS = file
`)
defer managerClose()
writerDump := `
{
"file": {
"BufferLen": 10000,
"Colorize": false,
"Expression": "",
"Flags": "stdflags",
"Level": "info",
"Prefix": "",
"StacktraceLevel": "none",
"WriterOption": {
"Compress": true,
"CompressionLevel": -1,
"DailyRotate": true,
"FileName": "$FILENAME",
"LogRotate": true,
"MaxDays": 7,
"MaxSize": 268435456
},
"WriterType": "file"
}
}
`
writerDumpAccess := `
{
"file.access": {
"BufferLen": 10000,
"Colorize": false,
"Expression": "",
"Flags": "none",
"Level": "info",
"Prefix": "",
"StacktraceLevel": "none",
"WriterOption": {
"Compress": true,
"CompressionLevel": -1,
"DailyRotate": true,
"FileName": "$FILENAME",
"LogRotate": true,
"MaxDays": 7,
"MaxSize": 268435456
},
"WriterType": "file"
}
}
`
dump := manager.GetLogger(log.DEFAULT).DumpWriters()
require.JSONEq(t, strings.ReplaceAll(writerDump, "$FILENAME", tempPath("gitea.log")), toJSON(dump))
dump = manager.GetLogger("access").DumpWriters()
require.JSONEq(t, strings.ReplaceAll(writerDumpAccess, "$FILENAME", tempPath("access.log")), toJSON(dump))
dump = manager.GetLogger("router").DumpWriters()
require.JSONEq(t, strings.ReplaceAll(writerDump, "$FILENAME", tempPath("gitea.log")), toJSON(dump))
}
func TestLogConfigLegacyModeDisable(t *testing.T) {
manager, managerClose := initLoggersByConfig(t, `
[log]
ROUTER = file
ACCESS = file
DISABLE_ROUTER_LOG = true
ENABLE_ACCESS_LOG = false
`)
defer managerClose()
dump := manager.GetLogger("access").DumpWriters()
require.JSONEq(t, "{}", toJSON(dump))
dump = manager.GetLogger("router").DumpWriters()
require.JSONEq(t, "{}", toJSON(dump))
}
func TestLogConfigNewConfig(t *testing.T) {
manager, managerClose := initLoggersByConfig(t, `
[log]
logger.access.MODE = console
logger.xorm.MODE = console, console-1
[log.console]
LEVEL = warn
[log.console-1]
MODE = console
LEVEL = error
STDERR = true
`)
defer managerClose()
writerDump := `
{
"console": {
"BufferLen": 10000,
"Colorize": false,
"Expression": "",
"Flags": "stdflags",
"Level": "warn",
"Prefix": "",
"StacktraceLevel": "none",
"WriterOption": {
"Stderr": false
},
"WriterType": "console"
},
"console-1": {
"BufferLen": 10000,
"Colorize": false,
"Expression": "",
"Flags": "stdflags",
"Level": "error",
"Prefix": "",
"StacktraceLevel": "none",
"WriterOption": {
"Stderr": true
},
"WriterType": "console"
}
}
`
writerDumpAccess := `
{
"console.access": {
"BufferLen": 10000,
"Colorize": false,
"Expression": "",
"Flags": "none",
"Level": "warn",
"Prefix": "",
"StacktraceLevel": "none",
"WriterOption": {
"Stderr": false
},
"WriterType": "console"
}
}
`
dump := manager.GetLogger("xorm").DumpWriters()
require.JSONEq(t, writerDump, toJSON(dump))
dump = manager.GetLogger("access").DumpWriters()
require.JSONEq(t, writerDumpAccess, toJSON(dump))
}
func TestLogConfigModeFile(t *testing.T) {
tempDir := t.TempDir()
tempPath := func(file string) string {
return filepath.Join(tempDir, file)
}
manager, managerClose := initLoggersByConfig(t, `
[log]
ROOT_PATH = `+tempDir+`
BUFFER_LEN = 10
MODE = file, file1
[log.file1]
MODE = file
LEVEL = error
STACKTRACE_LEVEL = fatal
EXPRESSION = filter
FLAGS = medfile
PREFIX = "[Prefix] "
FILE_NAME = file-xxx.log
LOG_ROTATE = false
MAX_SIZE_SHIFT = 1
DAILY_ROTATE = false
MAX_DAYS = 90
COMPRESS = false
COMPRESSION_LEVEL = 4
`)
defer managerClose()
writerDump := `
{
"file": {
"BufferLen": 10,
"Colorize": false,
"Expression": "",
"Flags": "stdflags",
"Level": "info",
"Prefix": "",
"StacktraceLevel": "none",
"WriterOption": {
"Compress": true,
"CompressionLevel": -1,
"DailyRotate": true,
"FileName": "$FILENAME-0",
"LogRotate": true,
"MaxDays": 7,
"MaxSize": 268435456
},
"WriterType": "file"
},
"file1": {
"BufferLen": 10,
"Colorize": false,
"Expression": "filter",
"Flags": "medfile",
"Level": "error",
"Prefix": "[Prefix] ",
"StacktraceLevel": "fatal",
"WriterOption": {
"Compress": false,
"CompressionLevel": 4,
"DailyRotate": false,
"FileName": "$FILENAME-1",
"LogRotate": false,
"MaxDays": 90,
"MaxSize": 2
},
"WriterType": "file"
}
}
`
dump := manager.GetLogger(log.DEFAULT).DumpWriters()
expected := writerDump
expected = strings.ReplaceAll(expected, "$FILENAME-0", tempPath("gitea.log"))
expected = strings.ReplaceAll(expected, "$FILENAME-1", tempPath("file-xxx.log"))
require.JSONEq(t, expected, toJSON(dump))
}

@ -278,7 +278,6 @@ func loadRepositoryFrom(rootCfg ConfigProvider) {
Repository.MaxCreationLimit = sec.Key("MAX_CREATION_LIMIT").MustInt(-1)
Repository.DefaultBranch = sec.Key("DEFAULT_BRANCH").MustString(Repository.DefaultBranch)
RepoRootPath = sec.Key("ROOT").MustString(path.Join(AppDataPath, "gitea-repositories"))
forcePathSeparator(RepoRootPath)
if !filepath.IsAbs(RepoRootPath) {
RepoRootPath = filepath.Join(AppWorkPath, RepoRootPath)
} else {

@ -317,7 +317,6 @@ func loadServerFrom(rootCfg ConfigProvider) {
PortToRedirect = sec.Key("PORT_TO_REDIRECT").MustString("80")
RedirectorUseProxyProtocol = sec.Key("REDIRECTOR_USE_PROXY_PROTOCOL").MustBool(UseProxyProtocol)
OfflineMode = sec.Key("OFFLINE_MODE").MustBool()
Log.DisableRouterLog = sec.Key("DISABLE_ROUTER_LOG").MustBool()
if len(StaticRootPath) == 0 {
StaticRootPath = AppWorkPath
}

@ -115,7 +115,7 @@ func init() {
// We can rely on log.CanColorStdout being set properly because modules/log/console_windows.go comes before modules/setting/setting.go lexicographically
// By default set this logger at Info - we'll change it later, but we need to start with something.
log.NewLogger(0, "console", "console", fmt.Sprintf(`{"level": "info", "colorize": %t, "stacktraceLevel": "none"}`, log.CanColorStdout))
log.SetConsoleLogger(log.DEFAULT, "console", log.INFO)
var err error
if AppPath, err = getAppPath(); err != nil {
@ -124,12 +124,6 @@ func init() {
AppWorkPath = getWorkPath(AppPath)
}
func forcePathSeparator(path string) {
if strings.Contains(path, "\\") {
log.Fatal("Do not use '\\' or '\\\\' in paths, instead, please use '/' in all places")
}
}
// IsRunUserMatchCurrentUser returns false if configured run user does not match
// actual user that runs the app. The first return value is the actual user name.
// This check is ignored under Windows since SSH remote login is not the main
@ -218,9 +212,9 @@ func Init(opts *Options) {
// loadCommonSettingsFrom loads common configurations from a configuration provider.
func loadCommonSettingsFrom(cfg ConfigProvider) {
// WARNNING: don't change the sequence except you know what you are doing.
// WARNING: don't change the sequence except you know what you are doing.
loadRunModeFrom(cfg)
loadLogFrom(cfg)
loadLogGlobalFrom(cfg)
loadServerFrom(cfg)
loadSSHFrom(cfg)
@ -282,10 +276,11 @@ func mustCurrentRunUserMatch(rootCfg ConfigProvider) {
// LoadSettings initializes the settings for normal start up
func LoadSettings() {
initAllLoggers()
loadDBSetting(CfgProvider)
loadServiceFrom(CfgProvider)
loadOAuth2ClientFrom(CfgProvider)
InitLogs(false)
loadCacheFrom(CfgProvider)
loadSessionFrom(CfgProvider)
loadCorsFrom(CfgProvider)

@ -223,9 +223,7 @@ func publicKeyHandler(ctx ssh.Context, key ssh.PublicKey) bool {
// validate the cert for this principal
if err := c.CheckCert(principal, cert); err != nil {
// User is presenting an invalid certificate - STOP any further processing
if log.IsError() {
log.Error("Invalid Certificate KeyID %s with Signature Fingerprint %s presented for Principal: %s from %s", cert.KeyId, gossh.FingerprintSHA256(cert.SignatureKey), principal, ctx.RemoteAddr())
}
log.Warn("Failed authentication attempt from %s", ctx.RemoteAddr())
return false
@ -239,10 +237,8 @@ func publicKeyHandler(ctx ssh.Context, key ssh.PublicKey) bool {
return true
}
if log.IsWarn() {
log.Warn("From %s Fingerprint: %s is a certificate, but no valid principals found", ctx.RemoteAddr(), gossh.FingerprintSHA256(key))
log.Warn("Failed authentication attempt from %s", ctx.RemoteAddr())
}
return false
}
@ -253,10 +249,8 @@ func publicKeyHandler(ctx ssh.Context, key ssh.PublicKey) bool {
pkey, err := asymkey_model.SearchPublicKeyByContent(ctx, strings.TrimSpace(string(gossh.MarshalAuthorizedKey(key))))
if err != nil {
if asymkey_model.IsErrKeyNotExist(err) {
if log.IsWarn() {
log.Warn("Unknown public key: %s from %s", gossh.FingerprintSHA256(key), ctx.RemoteAddr())
log.Warn("Failed authentication attempt from %s", ctx.RemoteAddr())
}
return false
}
log.Error("SearchPublicKeyByContent: %v", err)

@ -126,7 +126,7 @@ func wrapFatal(msg string) {
if msg == "" {
return
}
log.FatalWithSkip(1, "Unable to compile templates, %s", msg)
log.Fatal("Unable to compile templates, %s", msg)
}
type templateErrorPrettier struct {

@ -4,7 +4,8 @@
package test
import (
"strconv"
"context"
"fmt"
"strings"
"sync"
"sync/atomic"
@ -14,9 +15,7 @@ import (
)
type LogChecker struct {
logger *log.MultiChannelledLogger
loggerName string
eventLoggerName string
*log.EventWriterBaseImpl
filterMessages []string
filtered []bool
@ -27,54 +26,44 @@ type LogChecker struct {
mu sync.Mutex
}
func (lc *LogChecker) LogEvent(event *log.Event) error {
func (lc *LogChecker) Run(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
case event, ok := <-lc.Queue:
if !ok {
return
}
lc.checkLogEvent(event)
}
}
}
func (lc *LogChecker) checkLogEvent(event *log.EventFormatted) {
lc.mu.Lock()
defer lc.mu.Unlock()
for i, msg := range lc.filterMessages {
if strings.Contains(event.GetMsg(), msg) {
if strings.Contains(event.Origin.MsgSimpleText, msg) {
lc.filtered[i] = true
}
}
if strings.Contains(event.GetMsg(), lc.stopMark) {
if strings.Contains(event.Origin.MsgSimpleText, lc.stopMark) {
lc.stopped = true
}
return nil
}
func (lc *LogChecker) Close() {}
func (lc *LogChecker) Flush() {}
func (lc *LogChecker) GetLevel() log.Level {
return log.TRACE
}
func (lc *LogChecker) GetStacktraceLevel() log.Level {
return log.NONE
}
func (lc *LogChecker) GetName() string {
return lc.eventLoggerName
}
func (lc *LogChecker) ReleaseReopen() error {
return nil
}
var checkerIndex int64
func NewLogChecker(loggerName string) (logChecker *LogChecker, cancel func()) {
logger := log.GetLogger(loggerName)
func NewLogChecker(namePrefix string) (logChecker *LogChecker, cancel func()) {
logger := log.GetManager().GetLogger(namePrefix)
newCheckerIndex := atomic.AddInt64(&checkerIndex, 1)
lc := &LogChecker{
logger: logger,
loggerName: loggerName,
eventLoggerName: "TestLogChecker-" + strconv.FormatInt(newCheckerIndex, 10),
}
if err := logger.AddLogger(lc); err != nil {
panic(err) // it's impossible
}
return lc, func() { _, _ = logger.DelLogger(lc.GetName()) }
writerName := namePrefix + "-" + fmt.Sprint(newCheckerIndex)
lc := &LogChecker{}
lc.EventWriterBaseImpl = log.NewEventWriterBase(writerName, "test-log-checker", log.WriterMode{})
logger.AddWriters(lc)
return lc, func() { _ = logger.RemoveWriter(writerName) }
}
// Filter will make the `Check` function to check if these logs are outputted.

@ -13,8 +13,6 @@ import (
)
func TestLogChecker(t *testing.T) {
_ = log.NewLogger(1000, "console", "console", `{"level":"info","stacktracelevel":"NONE","stderr":true}`)
lc, cleanup := NewLogChecker(log.DEFAULT)
defer cleanup()

@ -13,7 +13,6 @@ import (
"testing"
"time"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/queue"
)
@ -24,19 +23,14 @@ var (
SlowFlush = 5 * time.Second
)
// TestLogger is a logger which will write to the testing log
type TestLogger struct {
log.WriterLogger
}
var WriterCloser = &testLoggerWriterCloser{}
type testLoggerWriterCloser struct {
sync.RWMutex
t []*testing.TB
t []testing.TB
}
func (w *testLoggerWriterCloser) pushT(t *testing.TB) {
func (w *testLoggerWriterCloser) pushT(t testing.TB) {
w.Lock()
w.t = append(w.t, t)
w.Unlock()
@ -48,7 +42,7 @@ func (w *testLoggerWriterCloser) Write(p []byte) (int, error) {
w.RLock()
defer w.RUnlock()
var t *testing.TB
var t testing.TB
if len(w.t) > 0 {
t = w.t[len(w.t)-1]
}
@ -57,33 +51,13 @@ func (w *testLoggerWriterCloser) Write(p []byte) (int, error) {
p = p[:len(p)-1]
}
if t == nil || *t == nil {
if t == nil {
// if there is no running test, the log message should be outputted to console, to avoid losing important information.
// the "???" prefix is used to match the "===" and "+++" in PrintCurrentTest
return fmt.Fprintf(os.Stdout, "??? [TestLogger] %s\n", p)
}
defer func() {
err := recover()
if err == nil {
return
}
var errString string
errErr, ok := err.(error)
if ok {
errString = errErr.Error()
} else {
errString, ok = err.(string)
}
if !ok {
panic(err)
}
if !strings.HasPrefix(errString, "Log in goroutine after ") {
panic(err)
}
}()
(*t).Log(string(p))
t.Log(string(p))
return len(p), nil
}
@ -106,8 +80,8 @@ func (w *testLoggerWriterCloser) Reset() {
if t == nil {
continue
}
fmt.Fprintf(os.Stdout, "Unclosed logger writer in test: %s", (*t).Name())
(*t).Errorf("Unclosed logger writer in test: %s", (*t).Name())
_, _ = fmt.Fprintf(os.Stdout, "Unclosed logger writer in test: %s", t.Name())
t.Errorf("Unclosed logger writer in test: %s", t.Name())
}
w.t = nil
}
@ -124,25 +98,25 @@ func PrintCurrentTest(t testing.TB, skip ...int) func() {
_, filename, line, _ := runtime.Caller(actualSkip)
if log.CanColorStdout {
fmt.Fprintf(os.Stdout, "=== %s (%s:%d)\n", fmt.Formatter(log.NewColoredValue(t.Name())), strings.TrimPrefix(filename, prefix), line)
_, _ = fmt.Fprintf(os.Stdout, "=== %s (%s:%d)\n", fmt.Formatter(log.NewColoredValue(t.Name())), strings.TrimPrefix(filename, prefix), line)
} else {
fmt.Fprintf(os.Stdout, "=== %s (%s:%d)\n", t.Name(), strings.TrimPrefix(filename, prefix), line)
_, _ = fmt.Fprintf(os.Stdout, "=== %s (%s:%d)\n", t.Name(), strings.TrimPrefix(filename, prefix), line)
}
WriterCloser.pushT(&t)
WriterCloser.pushT(t)
return func() {
took := time.Since(start)
if took > SlowTest {
if log.CanColorStdout {
fmt.Fprintf(os.Stdout, "+++ %s is a slow test (took %v)\n", fmt.Formatter(log.NewColoredValue(t.Name(), log.Bold, log.FgYellow)), fmt.Formatter(log.NewColoredValue(took, log.Bold, log.FgYellow)))
_, _ = fmt.Fprintf(os.Stdout, "+++ %s is a slow test (took %v)\n", fmt.Formatter(log.NewColoredValue(t.Name(), log.Bold, log.FgYellow)), fmt.Formatter(log.NewColoredValue(took, log.Bold, log.FgYellow)))
} else {
fmt.Fprintf(os.Stdout, "+++ %s is a slow test (took %v)\n", t.Name(), took)
_, _ = fmt.Fprintf(os.Stdout, "+++ %s is a slow test (took %v)\n", t.Name(), took)
}
}
timer := time.AfterFunc(SlowFlush, func() {
if log.CanColorStdout {
fmt.Fprintf(os.Stdout, "+++ %s ... still flushing after %v ...\n", fmt.Formatter(log.NewColoredValue(t.Name(), log.Bold, log.FgRed)), SlowFlush)
_, _ = fmt.Fprintf(os.Stdout, "+++ %s ... still flushing after %v ...\n", fmt.Formatter(log.NewColoredValue(t.Name(), log.Bold, log.FgRed)), SlowFlush)
} else {
fmt.Fprintf(os.Stdout, "+++ %s ... still flushing after %v ...\n", t.Name(), SlowFlush)
_, _ = fmt.Fprintf(os.Stdout, "+++ %s ... still flushing after %v ...\n", t.Name(), SlowFlush)
}
})
if err := queue.GetManager().FlushAll(context.Background(), time.Minute); err != nil {
@ -152,9 +126,9 @@ func PrintCurrentTest(t testing.TB, skip ...int) func() {
flushTook := time.Since(start) - took
if flushTook > SlowFlush {
if log.CanColorStdout {
fmt.Fprintf(os.Stdout, "+++ %s had a slow clean-up flush (took %v)\n", fmt.Formatter(log.NewColoredValue(t.Name(), log.Bold, log.FgRed)), fmt.Formatter(log.NewColoredValue(flushTook, log.Bold, log.FgRed)))
_, _ = fmt.Fprintf(os.Stdout, "+++ %s had a slow clean-up flush (took %v)\n", fmt.Formatter(log.NewColoredValue(t.Name(), log.Bold, log.FgRed)), fmt.Formatter(log.NewColoredValue(flushTook, log.Bold, log.FgRed)))
} else {
fmt.Fprintf(os.Stdout, "+++ %s had a slow clean-up flush (took %v)\n", t.Name(), flushTook)
_, _ = fmt.Fprintf(os.Stdout, "+++ %s had a slow clean-up flush (took %v)\n", t.Name(), flushTook)
}
}
WriterCloser.popT()
@ -168,40 +142,20 @@ func Printf(format string, args ...interface{}) {
args[i] = log.NewColoredValue(args[i])
}
}
fmt.Fprintf(os.Stdout, "\t"+format, args...)
_, _ = fmt.Fprintf(os.Stdout, "\t"+format, args...)
}
// NewTestLogger creates a TestLogger as a log.LoggerProvider
func NewTestLogger() log.LoggerProvider {
logger := &TestLogger{}
logger.Colorize = log.CanColorStdout
logger.Level = log.TRACE
return logger
}
// Init inits connection writer with json config.
// json config only need key "level".
func (log *TestLogger) Init(config string) error {
err := json.Unmarshal([]byte(config), log)
if err != nil {
return err
}
log.NewWriterLogger(WriterCloser)
return nil
}
// Flush when log should be flushed
func (log *TestLogger) Flush() {
}
// ReleaseReopen does nothing
func (log *TestLogger) ReleaseReopen() error {
return nil
// TestLogEventWriter is a logger which will write to the testing log
type TestLogEventWriter struct {
*log.EventWriterBaseImpl
}
// GetName returns the default name for this implementation
func (log *TestLogger) GetName() string {
return "test"
// NewTestLoggerWriter creates a TestLogEventWriter as a log.LoggerProvider
func NewTestLoggerWriter(name string, mode log.WriterMode) log.EventWriter {
w := &TestLogEventWriter{}
w.EventWriterBaseImpl = log.NewEventWriterBase(name, "test-log-writer", mode)
w.OutputWriteCloser = WriterCloser
return w
}
func init() {

@ -0,0 +1,246 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package rotatingfilewriter
import (
"bufio"
"compress/gzip"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"time"
"code.gitea.io/gitea/modules/graceful/releasereopen"
"code.gitea.io/gitea/modules/util"
)
type Options struct {
Rotate bool
MaximumSize int64
RotateDaily bool
KeepDays int
Compress bool
CompressionLevel int
}
type RotatingFileWriter struct {
mu sync.Mutex
fd *os.File
currentSize int64
openDate int
options Options
cancelReleaseReopen func()
}
var ErrorPrintf func(format string, args ...interface{})
// errorf tries to print error messages. Since this writer could be used by a logger system, this is the last chance to show the error in some cases
func errorf(format string, args ...interface{}) {
if ErrorPrintf != nil {
ErrorPrintf("rotatingfilewriter: "+format+"\n", args...)
}
}
// Open creates a new rotating file writer.
// Notice: if a file is opened by two rotators, there will be conflicts when rotating.
// In the future, there should be "rotating file manager"
func Open(filename string, options *Options) (*RotatingFileWriter, error) {
if options == nil {
options = &Options{}
}
rfw := &RotatingFileWriter{
options: *options,
}
if err := rfw.open(filename); err != nil {
return nil, err
}
rfw.cancelReleaseReopen = releasereopen.GetManager().Register(rfw)
return rfw, nil
}
func (rfw *RotatingFileWriter) Write(b []byte) (int, error) {
if rfw.options.Rotate && ((rfw.options.MaximumSize > 0 && rfw.currentSize >= rfw.options.MaximumSize) || (rfw.options.RotateDaily && time.Now().Day() != rfw.openDate)) {
if err := rfw.DoRotate(); err != nil {
// if this writer is used by a logger system, it's the logger system's responsibility to handle/show the error
return 0, err
}
}
n, err := rfw.fd.Write(b)
if err == nil {
rfw.currentSize += int64(n)
}
return n, err
}
func (rfw *RotatingFileWriter) Flush() error {
return rfw.fd.Sync()
}
func (rfw *RotatingFileWriter) Close() error {
rfw.mu.Lock()
if rfw.cancelReleaseReopen != nil {
rfw.cancelReleaseReopen()
rfw.cancelReleaseReopen = nil
}
rfw.mu.Unlock()
return rfw.fd.Close()
}
func (rfw *RotatingFileWriter) open(filename string) error {
fd, err := os.OpenFile(filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0o660)
if err != nil {
return err
}
rfw.fd = fd
finfo, err := fd.Stat()
if err != nil {
return err
}
rfw.currentSize = finfo.Size()
rfw.openDate = finfo.ModTime().Day()
return nil
}
func (rfw *RotatingFileWriter) ReleaseReopen() error {
return errors.Join(
rfw.fd.Close(),
rfw.open(rfw.fd.Name()),
)
}
// DoRotate the log file creating a backup like xx.2013-01-01.2
func (rfw *RotatingFileWriter) DoRotate() error {
if !rfw.options.Rotate {
return nil
}
rfw.mu.Lock()
defer rfw.mu.Unlock()
prefix := fmt.Sprintf("%s.%s.", rfw.fd.Name(), time.Now().Format("2006-01-02"))
var err error
fname := ""
for i := 1; err == nil && i <= 999; i++ {
fname = prefix + fmt.Sprintf("%03d", i)
_, err = os.Lstat(fname)
if rfw.options.Compress && err != nil {
_, err = os.Lstat(fname + ".gz")
}
}
// return error if the last file checked still existed
if err == nil {
return fmt.Errorf("cannot find free file to rename %s", rfw.fd.Name())
}
fd := rfw.fd
if err := fd.Close(); err != nil { // close file before rename
return err
}
if err := util.Rename(fd.Name(), fname); err != nil {
return err
}
if rfw.options.Compress {
go func() {
err := compressOldFile(fname, rfw.options.CompressionLevel)
if err != nil {
errorf("DoRotate: %v", err)
}
}()
}
if err := rfw.open(fd.Name()); err != nil {
return err
}
go deleteOldFiles(
filepath.Dir(fd.Name()),
filepath.Base(fd.Name()),
time.Now().AddDate(0, 0, -rfw.options.KeepDays),
)
return nil
}
func compressOldFile(fname string, compressionLevel int) error {
reader, err := os.Open(fname)
if err != nil {
return fmt.Errorf("compressOldFile: failed to open existing file %s: %w", fname, err)
}
defer reader.Close()
buffer := bufio.NewReader(reader)
fnameGz := fname + ".gz"
fw, err := os.OpenFile(fnameGz, os.O_WRONLY|os.O_CREATE, 0o660)
if err != nil {
return fmt.Errorf("compressOldFile: failed to open new file %s: %w", fnameGz, err)
}
defer fw.Close()
zw, err := gzip.NewWriterLevel(fw, compressionLevel)
if err != nil {
return fmt.Errorf("compressOldFile: failed to create gzip writer: %w", err)
}
defer zw.Close()
_, err = buffer.WriteTo(zw)
if err != nil {
_ = zw.Close()
_ = fw.Close()
_ = util.Remove(fname + ".gz")
return fmt.Errorf("compressOldFile: failed to write to gz file: %w", err)
}
_ = reader.Close()
err = util.Remove(fname)
if err != nil {
return fmt.Errorf("compressOldFile: failed to delete old file: %w", err)
}
return nil
}
func deleteOldFiles(dir, prefix string, removeBefore time.Time) {
err := filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) (returnErr error) {
defer func() {
if r := recover(); r != nil {
returnErr = fmt.Errorf("unable to delete old file '%s', error: %+v", path, r)
}
}()
if err != nil {
return err
}
if d.IsDir() {
return nil
}
info, err := d.Info()
if err != nil {
return err
}
if info.ModTime().Before(removeBefore) {
if strings.HasPrefix(filepath.Base(path), prefix) {
return util.Remove(path)
}
}
return nil
})
if err != nil {
errorf("deleteOldFiles: failed to delete old file: %v", err)
}
}

@ -0,0 +1,48 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package rotatingfilewriter
import (
"compress/gzip"
"io"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
)
func TestCompressOldFile(t *testing.T) {
tmpDir := t.TempDir()
fname := filepath.Join(tmpDir, "test")
nonGzip := filepath.Join(tmpDir, "test-nonGzip")
f, err := os.OpenFile(fname, os.O_CREATE|os.O_WRONLY, 0o660)
assert.NoError(t, err)
ng, err := os.OpenFile(nonGzip, os.O_CREATE|os.O_WRONLY, 0o660)
assert.NoError(t, err)
for i := 0; i < 999; i++ {
f.WriteString("This is a test file\n")
ng.WriteString("This is a test file\n")
}
f.Close()
ng.Close()
err = compressOldFile(fname, gzip.DefaultCompression)
assert.NoError(t, err)
_, err = os.Lstat(fname + ".gz")
assert.NoError(t, err)
f, err = os.Open(fname + ".gz")
assert.NoError(t, err)
zr, err := gzip.NewReader(f)
assert.NoError(t, err)
data, err := io.ReadAll(zr)
assert.NoError(t, err)
original, err := os.ReadFile(nonGzip)
assert.NoError(t, err)
assert.Equal(t, original, data)
}

@ -5,6 +5,7 @@ package routing
import (
"net/http"
"strings"
"time"
"code.gitea.io/gitea/modules/context"
@ -25,18 +26,18 @@ func NewLoggerHandler() func(next http.Handler) http.Handler {
}
var (
startMessage = log.NewColoredValueBytes("started ", log.DEBUG.Color())
slowMessage = log.NewColoredValueBytes("slow ", log.WARN.Color())
pollingMessage = log.NewColoredValueBytes("polling ", log.INFO.Color())
failedMessage = log.NewColoredValueBytes("failed ", log.WARN.Color())
completedMessage = log.NewColoredValueBytes("completed", log.INFO.Color())
unknownHandlerMessage = log.NewColoredValueBytes("completed", log.ERROR.Color())
startMessage = log.NewColoredValue("started ", log.DEBUG.ColorAttributes()...)
slowMessage = log.NewColoredValue("slow ", log.WARN.ColorAttributes()...)
pollingMessage = log.NewColoredValue("polling ", log.INFO.ColorAttributes()...)
failedMessage = log.NewColoredValue("failed ", log.WARN.ColorAttributes()...)
completedMessage = log.NewColoredValue("completed", log.INFO.ColorAttributes()...)
unknownHandlerMessage = log.NewColoredValue("completed", log.ERROR.ColorAttributes()...)
)
func logPrinter(logger log.Logger) func(trigger Event, record *requestRecord) {
return func(trigger Event, record *requestRecord) {
if trigger == StartEvent {
if !logger.IsTrace() {
if !logger.LevelEnabled(log.TRACE) {
// for performance, if the "started" message shouldn't be logged, we just return as early as possible
// developers can set the router log level to TRACE to get the "started" request messages.
return
@ -59,12 +60,12 @@ func logPrinter(logger log.Logger) func(trigger Event, record *requestRecord) {
if trigger == StillExecutingEvent {
message := slowMessage
level := log.WARN
logf := logger.Warn
if isLongPolling {
level = log.INFO
logf = logger.Info
message = pollingMessage
}
_ = logger.Log(0, level, "router: %s %v %s for %s, elapsed %v @ %s",
logf("router: %s %v %s for %s, elapsed %v @ %s",
message,
log.ColoredMethod(req.Method), req.RequestURI, req.RemoteAddr,
log.ColoredTime(time.Since(record.startTime)),
@ -74,7 +75,7 @@ func logPrinter(logger log.Logger) func(trigger Event, record *requestRecord) {
}
if panicErr != nil {
_ = logger.Log(0, log.WARN, "router: %s %v %s for %s, panic in %v @ %s, err=%v",
logger.Warn("router: %s %v %s for %s, panic in %v @ %s, err=%v",
failedMessage,
log.ColoredMethod(req.Method), req.RequestURI, req.RemoteAddr,
log.ColoredTime(time.Since(record.startTime)),
@ -88,14 +89,17 @@ func logPrinter(logger log.Logger) func(trigger Event, record *requestRecord) {
if v, ok := record.responseWriter.(context.ResponseWriter); ok {
status = v.Status()
}
level := log.INFO
logf := log.Info
if strings.HasPrefix(req.RequestURI, "/assets/") {
logf = log.Trace
}
message := completedMessage
if isUnknownHandler {
level = log.ERROR
logf = log.Error
message = unknownHandlerMessage
}
_ = logger.Log(0, level, "router: %s %v %s for %s, %v %v in %v @ %s",
logf("router: %s %v %s for %s, %v %v in %v @ %s",
message,
log.ColoredMethod(req.Method), req.RequestURI, req.RemoteAddr,
log.ColoredStatus(status), log.ColoredStatus(status, http.StatusText(status)), log.ColoredTime(time.Since(record.startTime)),

@ -3036,15 +3036,10 @@ config.git_pull_timeout = Pull Operation Timeout
config.git_gc_timeout = GC Operation Timeout
config.log_config = Log Configuration
config.log_mode = Log Mode
config.own_named_logger = Named Logger
config.routes_to_default_logger = Routes To Default Logger
config.go_log = Uses Go Log (redirected to default)
config.router_log_mode = Router Log Mode
config.logger_name_fmt = Logger: %s
config.disabled_logger = Disabled
config.access_log_mode = Access Log Mode
config.access_log_template = Template
config.xorm_log_mode = XORM Log Mode
config.access_log_template = Access Log Template
config.xorm_log_sql = Log SQL
config.get_setting_failed = Get setting %s failed

@ -60,11 +60,11 @@ func ProtocolMiddlewares() (handlers []any) {
handlers = append(handlers, proxy.ForwardedHeaders(opt))
}
if !setting.Log.DisableRouterLog {
if setting.IsRouteLogEnabled() {
handlers = append(handlers, routing.NewLoggerHandler())
}
if setting.Log.EnableAccessLog {
if setting.IsAccessLogEnabled() {
handlers = append(handlers, context.AccessLogger())
}

@ -73,7 +73,7 @@ func Routes() *web.Route {
r.Post("/manager/release-and-reopen-logging", ReleaseReopenLogging)
r.Post("/manager/set-log-sql", SetLogSQL)
r.Post("/manager/add-logger", bind(private.LoggerOptions{}), AddLogger)
r.Post("/manager/remove-logger/{group}/{name}", RemoveLogger)
r.Post("/manager/remove-logger/{logger}/{writer}", RemoveLogger)
r.Get("/manager/processes", Processes)
r.Post("/mail/send", SendEmail)
r.Post("/restore_repo", RestoreRepo)

@ -10,7 +10,7 @@ import (
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/graceful/releasereopen"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/private"
"code.gitea.io/gitea/modules/queue"
@ -46,19 +46,19 @@ func FlushQueues(ctx *context.PrivateContext) {
// PauseLogging pauses logging
func PauseLogging(ctx *context.PrivateContext) {
log.Pause()
log.GetManager().PauseAll()
ctx.PlainText(http.StatusOK, "success")
}
// ResumeLogging resumes logging
func ResumeLogging(ctx *context.PrivateContext) {
log.Resume()
log.GetManager().ResumeAll()
ctx.PlainText(http.StatusOK, "success")
}
// ReleaseReopenLogging releases and reopens logging files
func ReleaseReopenLogging(ctx *context.PrivateContext) {
if err := log.ReleaseReopen(); err != nil {
if err := releasereopen.GetManager().ReleaseReopen(); err != nil {
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: fmt.Sprintf("Error during release and reopen: %v", err),
})
@ -75,90 +75,108 @@ func SetLogSQL(ctx *context.PrivateContext) {
// RemoveLogger removes a logger
func RemoveLogger(ctx *context.PrivateContext) {
group := ctx.Params("group")
name := ctx.Params("name")
ok, err := log.GetLogger(group).DelLogger(name)
logger := ctx.Params("logger")
writer := ctx.Params("writer")
err := log.GetManager().GetLogger(logger).RemoveWriter(writer)
if err != nil {
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: fmt.Sprintf("Failed to remove logger: %s %s %v", group, name, err),
Err: fmt.Sprintf("Failed to remove log writer: %s %s %v", logger, writer, err),
})
return
}
if ok {
setting.RemoveSubLogDescription(group, name)
}
ctx.PlainText(http.StatusOK, fmt.Sprintf("Removed %s %s", group, name))
ctx.PlainText(http.StatusOK, fmt.Sprintf("Removed %s %s", logger, writer))
}
// AddLogger adds a logger
func AddLogger(ctx *context.PrivateContext) {
opts := web.GetForm(ctx).(*private.LoggerOptions)
if len(opts.Group) == 0 {
opts.Group = log.DEFAULT
if len(opts.Logger) == 0 {
opts.Logger = log.DEFAULT
}
if _, ok := opts.Config["flags"]; !ok {
switch opts.Group {
writerMode := log.WriterMode{}
writerType := opts.Mode
var flags string
var ok bool
if flags, ok = opts.Config["flags"].(string); !ok {
switch opts.Logger {
case "access":
opts.Config["flags"] = log.FlagsFromString("")
flags = ""
case "router":
opts.Config["flags"] = log.FlagsFromString("date,time")
flags = "date,time"
default:
opts.Config["flags"] = log.FlagsFromString("stdflags")
flags = "stdflags"
}
}
writerMode.Flags = log.FlagsFromString(flags)
if _, ok := opts.Config["colorize"]; !ok && opts.Mode == "console" {
if writerMode.Colorize, ok = opts.Config["colorize"].(bool); !ok && opts.Mode == "console" {
if _, ok := opts.Config["stderr"]; ok {
opts.Config["colorize"] = log.CanColorStderr
writerMode.Colorize = log.CanColorStderr
} else {
opts.Config["colorize"] = log.CanColorStdout
}
}
if _, ok := opts.Config["level"]; !ok {
opts.Config["level"] = setting.Log.Level
}
if _, ok := opts.Config["stacktraceLevel"]; !ok {
opts.Config["stacktraceLevel"] = setting.Log.StacktraceLogLevel
}
if opts.Mode == "file" {
if _, ok := opts.Config["maxsize"]; !ok {
opts.Config["maxsize"] = 1 << 28
}
if _, ok := opts.Config["maxdays"]; !ok {
opts.Config["maxdays"] = 7
}
if _, ok := opts.Config["compressionLevel"]; !ok {
opts.Config["compressionLevel"] = -1
}
writerMode.Colorize = log.CanColorStdout
}
}
writerMode.Level = setting.Log.Level
if level, ok := opts.Config["level"].(string); ok {
writerMode.Level = log.LevelFromString(level)
}
writerMode.StacktraceLevel = setting.Log.StacktraceLogLevel
if stacktraceLevel, ok := opts.Config["level"].(string); ok {
writerMode.StacktraceLevel = log.LevelFromString(stacktraceLevel)
}
writerMode.Prefix, _ = opts.Config["prefix"].(string)
writerMode.Expression, _ = opts.Config["expression"].(string)
switch writerType {
case "console":
writerOption := log.WriterConsoleOption{}
writerOption.Stderr, _ = opts.Config["stderr"].(bool)
writerMode.WriterOption = writerOption
case "file":
writerOption := log.WriterFileOption{}
fileName, _ := opts.Config["filename"].(string)
writerOption.FileName = setting.LogPrepareFilenameForWriter(fileName, opts.Writer+".log")
writerOption.LogRotate = opts.Config["rotate"].(bool)
maxSizeShift, _ := opts.Config["maxsize"].(int)
if maxSizeShift == 0 {
maxSizeShift = 28
}
writerOption.MaxSize = 1 << maxSizeShift
writerOption.DailyRotate, _ = opts.Config["daily"].(bool)
writerOption.MaxDays, _ = opts.Config["maxdays"].(int)
if writerOption.MaxDays == 0 {
writerOption.MaxDays = 7
}
writerOption.Compress, _ = opts.Config["compress"].(bool)
writerOption.CompressionLevel, _ = opts.Config["compressionLevel"].(int)
if writerOption.CompressionLevel == 0 {
writerOption.CompressionLevel = -1
}
writerMode.WriterOption = writerOption
case "conn":
writerOption := log.WriterConnOption{}
writerOption.ReconnectOnMsg, _ = opts.Config["reconnectOnMsg"].(bool)
writerOption.Reconnect, _ = opts.Config["reconnect"].(bool)
writerOption.Protocol, _ = opts.Config["net"].(string)
writerOption.Addr, _ = opts.Config["address"].(string)
writerMode.WriterOption = writerOption
default:
panic(fmt.Sprintf("invalid log writer mode: %s", writerType))
}
bufferLen := setting.Log.BufferLength
byteConfig, err := json.Marshal(opts.Config)
writer, err := log.NewEventWriter(opts.Writer, writerType, writerMode)
if err != nil {
log.Error("Failed to marshal log configuration: %v %v", opts.Config, err)
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: fmt.Sprintf("Failed to marshal log configuration: %v %v", opts.Config, err),
})
return
}
config := string(byteConfig)
if err := log.NewNamedLogger(opts.Group, bufferLen, opts.Name, opts.Mode, config); err != nil {
log.Error("Failed to create new named logger: %s %v", config, err)
log.Error("Failed to create new log writer: %v", err)
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: fmt.Sprintf("Failed to create new named logger: %s %v", config, err),
Err: fmt.Sprintf("Failed to create new log writer: %v", err),
})
return
}
setting.AddSubLogDescription(opts.Group, setting.SubLogDescription{
Name: opts.Name,
Provider: opts.Mode,
Config: config,
})
log.GetManager().GetLogger(opts.Logger).AddWriters(writer)
ctx.PlainText(http.StatusOK, "success")
}

@ -117,7 +117,6 @@ func Config(ctx *context.Context) {
ctx.Data["AppBuiltWith"] = setting.AppBuiltWith
ctx.Data["Domain"] = setting.Domain
ctx.Data["OfflineMode"] = setting.OfflineMode
ctx.Data["DisableRouterLog"] = setting.Log.DisableRouterLog
ctx.Data["RunUser"] = setting.RunUser
ctx.Data["RunMode"] = util.ToTitleCase(setting.RunMode)
ctx.Data["GitVersion"] = git.VersionInfo()
@ -182,13 +181,11 @@ func Config(ctx *context.Context) {
}
ctx.Data["EnvVars"] = envVars
ctx.Data["Loggers"] = setting.GetLogDescriptions()
ctx.Data["EnableAccessLog"] = setting.Log.EnableAccessLog
ctx.Data["AccessLogTemplate"] = setting.Log.AccessLogTemplate
ctx.Data["DisableRouterLog"] = setting.Log.DisableRouterLog
ctx.Data["EnableXORMLog"] = setting.Log.EnableXORMLog
ctx.Data["LogSQL"] = setting.Database.LogSQL
ctx.Data["Loggers"] = log.GetManager().DumpLoggers()
ctx.HTML(http.StatusOK, tplConfig)
}

@ -2742,7 +2742,7 @@ func NewComment(ctx *context.Context) {
log.Trace("Permission Denied: User %-v not the Poster (ID: %d) and cannot read %s in Repo %-v.\n"+
"User in Repo has Permissions: %-+v",
ctx.Doer,
log.NewColoredIDValue(issue.PosterID),
issue.PosterID,
issueType,
ctx.Repo.Repository,
ctx.Repo.Permission)
@ -3020,7 +3020,7 @@ func ChangeIssueReaction(ctx *context.Context) {
log.Trace("Permission Denied: User %-v not the Poster (ID: %d) and cannot read %s in Repo %-v.\n"+
"User in Repo has Permissions: %-+v",
ctx.Doer,
log.NewColoredIDValue(issue.PosterID),
issue.PosterID,
issueType,
ctx.Repo.Repository,
ctx.Repo.Permission)
@ -3122,7 +3122,7 @@ func ChangeCommentReaction(ctx *context.Context) {
log.Trace("Permission Denied: User %-v not the Poster (ID: %d) and cannot read %s in Repo %-v.\n"+
"User in Repo has Permissions: %-+v",
ctx.Doer,
log.NewColoredIDValue(comment.Issue.PosterID),
comment.Issue.PosterID,
issueType,
ctx.Repo.Repository,
ctx.Repo.Permission)

@ -29,7 +29,7 @@ func IssueWatch(ctx *context.Context) {
log.Trace("Permission Denied: User %-v not the Poster (ID: %d) and cannot read %s in Repo %-v.\n"+
"User in Repo has Permissions: %-+v",
ctx.Doer,
log.NewColoredIDValue(issue.PosterID),
issue.PosterID,
issueType,
ctx.Repo.Repository,
ctx.Repo.Permission)

@ -115,13 +115,11 @@ func (d *CodebaseDownloader) String() string {
return fmt.Sprintf("migration from codebase server %s %s/%s", d.baseURL, d.project, d.repoName)
}
// ColorFormat provides a basic color format for a GogsDownloader
func (d *CodebaseDownloader) ColorFormat(s fmt.State) {
func (d *CodebaseDownloader) LogString() string {
if d == nil {
log.ColorFprintf(s, "<nil: CodebaseDownloader>")
return
return "<CodebaseDownloader nil>"
}
log.ColorFprintf(s, "migration from codebase server %s %s/%s", d.baseURL, d.project, d.repoName)
return fmt.Sprintf("<CodebaseDownloader %s %s/%s>", d.baseURL, d.project, d.repoName)
}
// FormatCloneURL add authentication into remote URLs

@ -62,13 +62,11 @@ func (g *GitBucketDownloader) String() string {
return fmt.Sprintf("migration from gitbucket server %s %s/%s", g.baseURL, g.repoOwner, g.repoName)
}
// ColorFormat provides a basic color format for a GitBucketDownloader
func (g *GitBucketDownloader) ColorFormat(s fmt.State) {
func (g *GitBucketDownloader) LogString() string {
if g == nil {
log.ColorFprintf(s, "<nil: GitBucketDownloader>")
return
return "<GitBucketDownloader nil>"
}
log.ColorFprintf(s, "migration from gitbucket server %s %s/%s", g.baseURL, g.repoOwner, g.repoName)
return fmt.Sprintf("<GitBucketDownloader %s %s/%s>", g.baseURL, g.repoOwner, g.repoName)
}
// NewGitBucketDownloader creates a GitBucket downloader

@ -134,13 +134,11 @@ func (g *GiteaDownloader) String() string {
return fmt.Sprintf("migration from gitea server %s %s/%s", g.baseURL, g.repoOwner, g.repoName)
}
// ColorFormat provides a basic color format for a GiteaDownloader
func (g *GiteaDownloader) ColorFormat(s fmt.State) {
func (g *GiteaDownloader) LogString() string {
if g == nil {
log.ColorFprintf(s, "<nil: GiteaDownloader>")
return
return "<GiteaDownloader nil>"
}
log.ColorFprintf(s, "migration from gitea server %s %s/%s", g.baseURL, g.repoOwner, g.repoName)
return fmt.Sprintf("<GiteaDownloader %s %s/%s>", g.baseURL, g.repoOwner, g.repoName)
}
// GetRepoInfo returns a repository information

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save