From 4647660776436f0a83129b4ceb8426b1fb0599bb Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Mon, 22 May 2023 06:35:11 +0800 Subject: [PATCH] Rewrite logger system (#24726) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## ⚠️ Breaking The `log..` 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..` syntax. ## The new design See `logger.go` for documents. ## Screenshot
![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)
## TODO * [x] add some new tests * [x] fix some tests * [x] test some sub-commands (manually ....) --------- Co-authored-by: Jason Song Co-authored-by: delvh Co-authored-by: Giteabot --- cmd/cmd.go | 17 +- cmd/doctor.go | 90 ++- cmd/dump.go | 5 +- cmd/embedded.go | 8 +- cmd/manager_logging.go | 120 +--- cmd/serv.go | 5 +- cmd/web.go | 10 +- custom/conf/app.example.ini | 92 +-- .../config-cheat-sheet.en-us.md | 40 +- .../administration/logging-config.en-us.md | 295 ++++++++++ .../logging-documentation.en-us.md | 524 ------------------ main.go | 2 + models/db/log.go | 52 +- models/issues/pull.go | 39 +- models/migrations/base/tests.go | 6 +- models/organization/team.go | 16 +- models/perm/access/repo_permission.go | 55 +- models/perm/access_mode.go | 9 +- models/repo/repo.go | 16 +- models/unit/unit.go | 7 +- models/user/user.go | 14 +- modules/context/access_log.go | 5 +- modules/doctor/doctor.go | 74 ++- modules/git/git_test.go | 3 - modules/graceful/manager.go | 2 +- modules/graceful/manager_unix.go | 3 +- .../graceful/releasereopen/releasereopen.go | 61 ++ .../releasereopen/releasereopen_test.go | 43 ++ modules/indexer/code/elastic_search.go | 20 +- modules/indexer/issues/elastic_search.go | 21 +- modules/lfs/pointer.go | 12 +- modules/log/color.go | 115 ++++ modules/log/color_console.go | 14 + ...onsole_other.go => color_console_other.go} | 0 ...le_windows.go => color_console_windows.go} | 2 +- .../log/{colors_router.go => color_router.go} | 60 +- modules/log/colors.go | 435 --------------- modules/log/conn.go | 137 ----- modules/log/conn_test.go | 230 -------- modules/log/console.go | 93 ---- modules/log/console_test.go | 137 ----- modules/log/errors.go | 61 -- modules/log/event.go | 460 --------------- modules/log/event_format.go | 246 ++++++++ modules/log/event_format_test.go | 57 ++ modules/log/event_writer.go | 54 ++ modules/log/event_writer_base.go | 160 ++++++ modules/log/event_writer_conn.go | 111 ++++ modules/log/event_writer_conn_test.go | 75 +++ modules/log/event_writer_console.go | 40 ++ modules/log/event_writer_file.go | 48 ++ modules/log/file.go | 283 ---------- modules/log/file_test.go | 235 -------- modules/log/flags.go | 134 +++-- modules/log/flags_test.go | 30 + modules/log/init.go | 35 ++ modules/log/level.go | 99 ++-- modules/log/log.go | 305 ---------- modules/log/log_test.go | 152 ----- modules/log/logger.go | 163 ++---- modules/log/logger_global.go | 83 +++ modules/log/logger_impl.go | 239 ++++++++ modules/log/logger_test.go | 145 +++++ modules/log/manager.go | 142 +++++ modules/log/manager_test.go | 42 ++ modules/log/misc.go | 78 +++ modules/log/multichannel.go | 104 ---- modules/log/provider.go | 25 - modules/log/smtp.go | 114 ---- modules/log/smtp_test.go | 85 --- modules/log/stack.go | 6 +- modules/log/writer.go | 269 --------- modules/log/writer_test.go | 275 --------- modules/private/manager.go | 14 +- modules/setting/config_provider.go | 52 ++ modules/setting/config_provider_test.go | 66 +++ modules/setting/database.go | 2 +- modules/setting/log.go | 487 ++++++---------- modules/setting/log_test.go | 387 +++++++++++++ modules/setting/repository.go | 1 - modules/setting/server.go | 1 - modules/setting/setting.go | 15 +- modules/ssh/ssh.go | 16 +- modules/templates/htmlrenderer.go | 2 +- modules/test/logchecker.go | 67 +-- modules/test/logchecker_test.go | 2 - modules/testlogger/testlogger.go | 98 +--- modules/util/rotatingfilewriter/writer.go | 246 ++++++++ .../util/rotatingfilewriter/writer_test.go | 48 ++ modules/web/routing/logger.go | 32 +- options/locale/locale_en-US.ini | 9 +- routers/common/middleware.go | 4 +- routers/private/internal.go | 2 +- routers/private/manager.go | 126 +++-- routers/web/admin/config.go | 7 +- routers/web/repo/issue.go | 6 +- routers/web/repo/issue_watch.go | 2 +- services/migrations/codebase.go | 8 +- services/migrations/gitbucket.go | 8 +- services/migrations/gitea_downloader.go | 8 +- services/migrations/github.go | 8 +- services/migrations/gitlab.go | 8 +- services/migrations/gogs.go | 8 +- services/migrations/onedev.go | 8 +- templates/admin/config.tmpl | 65 +-- tests/e2e/e2e_test.go | 2 +- tests/integration/integration_test.go | 2 +- .../migration-test/migration_test.go | 8 +- tests/test_utils.go | 6 +- 109 files changed, 3772 insertions(+), 5303 deletions(-) create mode 100644 docs/content/doc/administration/logging-config.en-us.md delete mode 100644 docs/content/doc/administration/logging-documentation.en-us.md create mode 100644 modules/graceful/releasereopen/releasereopen.go create mode 100644 modules/graceful/releasereopen/releasereopen_test.go create mode 100644 modules/log/color.go create mode 100644 modules/log/color_console.go rename modules/log/{console_other.go => color_console_other.go} (100%) rename modules/log/{console_windows.go => color_console_windows.go} (94%) rename modules/log/{colors_router.go => color_router.go} (54%) delete mode 100644 modules/log/colors.go delete mode 100644 modules/log/conn.go delete mode 100644 modules/log/conn_test.go delete mode 100644 modules/log/console.go delete mode 100644 modules/log/console_test.go delete mode 100644 modules/log/errors.go delete mode 100644 modules/log/event.go create mode 100644 modules/log/event_format.go create mode 100644 modules/log/event_format_test.go create mode 100644 modules/log/event_writer.go create mode 100644 modules/log/event_writer_base.go create mode 100644 modules/log/event_writer_conn.go create mode 100644 modules/log/event_writer_conn_test.go create mode 100644 modules/log/event_writer_console.go create mode 100644 modules/log/event_writer_file.go delete mode 100644 modules/log/file.go delete mode 100644 modules/log/file_test.go create mode 100644 modules/log/flags_test.go create mode 100644 modules/log/init.go delete mode 100644 modules/log/log.go delete mode 100644 modules/log/log_test.go create mode 100644 modules/log/logger_global.go create mode 100644 modules/log/logger_impl.go create mode 100644 modules/log/logger_test.go create mode 100644 modules/log/manager.go create mode 100644 modules/log/manager_test.go create mode 100644 modules/log/misc.go delete mode 100644 modules/log/multichannel.go delete mode 100644 modules/log/provider.go delete mode 100644 modules/log/smtp.go delete mode 100644 modules/log/smtp_test.go delete mode 100644 modules/log/writer.go delete mode 100644 modules/log/writer_test.go create mode 100644 modules/setting/config_provider_test.go create mode 100644 modules/setting/log_test.go create mode 100644 modules/util/rotatingfilewriter/writer.go create mode 100644 modules/util/rotatingfilewriter/writer_test.go diff --git a/cmd/cmd.go b/cmd/cmd.go index cf2d9ef89e831..b148007fbe3be 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -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) +} diff --git a/cmd/doctor.go b/cmd/doctor.go index 65c028c5ed19d..b596e9ac0cb7a 100644 --- a/cmd/doctor.go +++ b/cmd/doctor.go @@ -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)) + // if no doctor log-file is set, do not show any log from default logger return } - defer func() { - recovered := recover() - if recovered == nil { - 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) } diff --git a/cmd/dump.go b/cmd/dump.go index 32ccc5566c8a9..40524f48d4d1b 100644 --- a/cmd/dump.go +++ b/cmd/dump.go @@ -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) { diff --git a/cmd/embedded.go b/cmd/embedded.go index 3f849bea0a264..e51f8477b445c 100644 --- a/cmd/embedded.go +++ b/cmd/embedded.go @@ -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{ diff --git a/cmd/manager_logging.go b/cmd/manager_logging.go index 914210d370372..dd85cc26d8087 100644 --- a/cmd/manager_logging.go +++ b/cmd/manager_logging.go @@ -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) } diff --git a/cmd/serv.go b/cmd/serv.go index a79f314d00b69..87bf1cce20e4e 100644 --- a/cmd/serv.go +++ b/cmd/serv.go @@ -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 { diff --git a/cmd/web.go b/cmd/web.go index 3a01d07b05f59..bc344db54017c 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -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 } diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 91d829c0ca955..27e56542ee8cf 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -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..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 = ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/docs/content/doc/administration/config-cheat-sheet.en-us.md b/docs/content/doc/administration/config-cheat-sheet.en-us.md index fdd1235f89793..5fa4c5624c404 100644 --- a/docs/content/doc/administration/config-cheat-sheet.en-us.md +++ b/docs/content/doc/administration/config-cheat-sheet.en-us.md @@ -835,22 +835,16 @@ Default templates for project boards: ## Log (`log`) - `ROOT_PATH`: **\**: 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`: **\**: 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`: **\**: 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.`) -- `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. diff --git a/docs/content/doc/administration/logging-config.en-us.md b/docs/content/doc/administration/logging-config.en-us.md new file mode 100644 index 0000000000000..857eb19b563fd --- /dev/null +++ b/docs/content/doc/administration/logging-config.en-us.md @@ -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.]` 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..` + +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: **\**) 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`. diff --git a/docs/content/doc/administration/logging-documentation.en-us.md b/docs/content/doc/administration/logging-documentation.en-us.md deleted file mode 100644 index 3b2bf8076922d..0000000000000 --- a/docs/content/doc/administration/logging-documentation.en-us.md +++ /dev/null @@ -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.]` sections for the configuration of different log outputs -- `[log..]` 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..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`. diff --git a/main.go b/main.go index 1589fa97db434..49093eb8a7038 100644 --- a/main.go +++ b/main.go @@ -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) { diff --git a/models/db/log.go b/models/db/log.go index fec2ea3c3d38c..dd95f64ca80b9 100644 --- a/models/db/log.go +++ b/models/db/log.go @@ -14,67 +14,62 @@ import ( // XORMLogBridge a logger bridge from Logger to xorm type XORMLogBridge struct { - showSQLint *int32 - logger log.Logger + 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_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() } diff --git a/models/issues/pull.go b/models/issues/pull.go index 855d37867d814..218a265741e2b 100644 --- a/models/issues/pull.go +++ b/models/issues/pull.go @@ -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("/"), - log.NewColoredIDValue(0), - log.NewColoredValue(""), - log.NewColoredValue("/"), - log.NewColoredValue(""), - ) - return - } - - log.ColorFprintf(s, "PR[%d]", log.NewColoredIDValue(pr.ID)) + return "" + } + + s := new(strings.Builder) + fmt.Fprintf(s, "') + return s.String() } // MustHeadUserName returns the HeadRepo's username if failed return blank diff --git a/models/migrations/base/tests.go b/models/migrations/base/tests.go index 08eded7fdc020..dd99a1eda280c 100644 --- a/models/migrations/base/tests.go +++ b/models/migrations/base/tests.go @@ -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() diff --git a/models/organization/team.go b/models/organization/team.go index 5e3c9ecffe22b..3624fdc46663d 100644 --- a/models/organization/team.go +++ b/models/organization/team.go @@ -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), - "", - log.NewColoredIDValue(0), - 0) - return + return "" } - log.ColorFprintf(s, "%d:%s (OrgID: %d) %-v", - log.NewColoredIDValue(t.ID), - t.Name, - log.NewColoredIDValue(t.OrgID), - t.AccessMode) + return fmt.Sprintf("", t.ID, t.Name, t.OrgID, t.AccessMode.LogString()) } // LoadUnits load a list of available units for a team diff --git a/models/perm/access/repo_permission.go b/models/perm/access/repo_permission.go index ee76e482001a1..64df5355bba6a 100644 --- a/models/perm/access/repo_permission.go +++ b/models/perm/access/repo_permission.go @@ -102,45 +102,28 @@ 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) - - 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 { - configBytes, err := unit.Config.ToDB() - config = string(configBytes) - if err != nil { - config = err.Error() - } +func (p *Permission) LogString() string { + format := "", mode, mode.String()) } // ParseAccessMode returns corresponding access mode to given permission string. diff --git a/models/repo/repo.go b/models/repo/repo.go index bcf8e5bbe8e04..d3e6daa95b53a 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -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), - "", - "") - return - } - log.ColorFprintf(s, "%d:%s/%s", - log.NewColoredIDValue(repo.ID), - repo.OwnerName, - repo.Name) + return "" + } + return fmt.Sprintf("", repo.ID, repo.OwnerName, repo.Name) } // IsBeingMigrated indicates that repository is being migrated diff --git a/models/unit/unit.go b/models/unit/unit.go index 7cd679116f1bb..5f5e20de1e0e0 100644 --- a/models/unit/unit.go +++ b/models/unit/unit.go @@ -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("", u, u.String()) } var ( diff --git a/models/user/user.go b/models/user/user.go index 7428e51065d59..0be36e69abaf1 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -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("")) - return - } - log.ColorFprintf(s, "%d:%s", - log.NewColoredIDValue(u.ID), - log.NewColoredValue(u.Name)) + return "" + } + return fmt.Sprintf("", u.ID, u.Name) } // BeforeUpdate is invoked from XORM before updating this object. diff --git a/modules/context/access_log.go b/modules/context/access_log.go index b6468d139bf9f..9b649a6a01335 100644 --- a/modules/context/access_log.go +++ b/modules/context/access_log.go @@ -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()) }) } } diff --git a/modules/doctor/doctor.go b/modules/doctor/doctor.go index 32eb5938c307c..10838a751217d 100644 --- a/modules/doctor/doctor.go +++ b/modules/doctor/doctor.go @@ -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 } diff --git a/modules/git/git_test.go b/modules/git/git_test.go index e3bfe496da704..25eb3085319d3 100644 --- a/modules/git/git_test.go +++ b/modules/git/git_test.go @@ -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) diff --git a/modules/graceful/manager.go b/modules/graceful/manager.go index c7b4c101ef280..d32788092d4f0 100644 --- a/modules/graceful/manager.go +++ b/modules/graceful/manager.go @@ -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 diff --git a/modules/graceful/manager_unix.go b/modules/graceful/manager_unix.go index 5d72111bff3de..d89f6fc725bcd 100644 --- a/modules/graceful/manager_unix.go +++ b/modules/graceful/manager_unix.go @@ -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: diff --git a/modules/graceful/releasereopen/releasereopen.go b/modules/graceful/releasereopen/releasereopen.go new file mode 100644 index 0000000000000..de5b07c0a6c82 --- /dev/null +++ b/modules/graceful/releasereopen/releasereopen.go @@ -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() diff --git a/modules/graceful/releasereopen/releasereopen_test.go b/modules/graceful/releasereopen/releasereopen_test.go new file mode 100644 index 0000000000000..0e8b48257d64d --- /dev/null +++ b/modules/graceful/releasereopen/releasereopen_test.go @@ -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) +} diff --git a/modules/indexer/code/elastic_search.go b/modules/indexer/code/elastic_search.go index 609753800910f..0e56a865880e7 100644 --- a/modules/indexer/code/elastic_search.go +++ b/modules/indexer/code/elastic_search.go @@ -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 { diff --git a/modules/indexer/issues/elastic_search.go b/modules/indexer/issues/elastic_search.go index fd1dd4b452325..ec62f857adace 100644 --- a/modules/indexer/issues/elastic_search.go +++ b/modules/indexer/issues/elastic_search.go @@ -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 { diff --git a/modules/lfs/pointer.go b/modules/lfs/pointer.go index f7f225bf1c1e7..d7653e836c911 100644 --- a/modules/lfs/pointer.go +++ b/modules/lfs/pointer.go @@ -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, "") - return + return "" } - log.ColorFprintf(s, "%s:%d", - log.NewColoredIDValue(p.Oid), - p.Size) + return fmt.Sprintf("", p.Oid, p.Size) } // GeneratePointer generates a pointer for arbitrary content diff --git a/modules/log/color.go b/modules/log/color.go new file mode 100644 index 0000000000000..dcbba5f6d65d6 --- /dev/null +++ b/modules/log/color.go @@ -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 +} diff --git a/modules/log/color_console.go b/modules/log/color_console.go new file mode 100644 index 0000000000000..2658652ec600f --- /dev/null +++ b/modules/log/color_console.go @@ -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 diff --git a/modules/log/console_other.go b/modules/log/color_console_other.go similarity index 100% rename from modules/log/console_other.go rename to modules/log/color_console_other.go diff --git a/modules/log/console_windows.go b/modules/log/color_console_windows.go similarity index 94% rename from modules/log/console_windows.go rename to modules/log/color_console_windows.go index 54dac12fa0d2e..3f59e934daddc 100644 --- a/modules/log/console_windows.go +++ b/modules/log/color_console_windows.go @@ -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) diff --git a/modules/log/colors_router.go b/modules/log/color_router.go similarity index 54% rename from modules/log/colors_router.go rename to modules/log/color_router.go index efc7337b6b257..80e7e02079e6c 100644 --- a/modules/log/colors_router.go +++ b/modules/log/color_router.go @@ -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) } diff --git a/modules/log/colors.go b/modules/log/colors.go deleted file mode 100644 index 85e205cb6764d..0000000000000 --- a/modules/log/colors.go +++ /dev/null @@ -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 - } -} diff --git a/modules/log/conn.go b/modules/log/conn.go deleted file mode 100644 index b21a744037bce..0000000000000 --- a/modules/log/conn.go +++ /dev/null @@ -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) -} diff --git a/modules/log/conn_test.go b/modules/log/conn_test.go deleted file mode 100644 index 445bd7765341d..0000000000000 --- a/modules/log/conn_test.go +++ /dev/null @@ -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() -} diff --git a/modules/log/console.go b/modules/log/console.go deleted file mode 100644 index ce0415d1390c0..0000000000000 --- a/modules/log/console.go +++ /dev/null @@ -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) -} diff --git a/modules/log/console_test.go b/modules/log/console_test.go deleted file mode 100644 index e4c3882d4f8a3..0000000000000 --- a/modules/log/console_test.go +++ /dev/null @@ -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) -} diff --git a/modules/log/errors.go b/modules/log/errors.go deleted file mode 100644 index 942639a434449..0000000000000 --- a/modules/log/errors.go +++ /dev/null @@ -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) -} diff --git a/modules/log/event.go b/modules/log/event.go deleted file mode 100644 index 723c8810bc428..0000000000000 --- a/modules/log/event.go +++ /dev/null @@ -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 -} diff --git a/modules/log/event_format.go b/modules/log/event_format.go new file mode 100644 index 0000000000000..524ca3dd872ed --- /dev/null +++ b/modules/log/event_format.go @@ -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 +} diff --git a/modules/log/event_format_test.go b/modules/log/event_format_test.go new file mode 100644 index 0000000000000..7c299a607d862 --- /dev/null +++ b/modules/log/event_format_test.go @@ -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)) +} diff --git a/modules/log/event_writer.go b/modules/log/event_writer.go new file mode 100644 index 0000000000000..4b77e488de7bf --- /dev/null +++ b/modules/log/event_writer.go @@ -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) +} diff --git a/modules/log/event_writer_base.go b/modules/log/event_writer_base.go new file mode 100644 index 0000000000000..f61d9a7b9d8a2 --- /dev/null +++ b/modules/log/event_writer_base.go @@ -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()) + } +} diff --git a/modules/log/event_writer_conn.go b/modules/log/event_writer_conn.go new file mode 100644 index 0000000000000..022206aa4d516 --- /dev/null +++ b/modules/log/event_writer_conn.go @@ -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 +} diff --git a/modules/log/event_writer_conn_test.go b/modules/log/event_writer_conn_test.go new file mode 100644 index 0000000000000..e08ec025a328c --- /dev/null +++ b/modules/log/event_writer_conn_test.go @@ -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() +} diff --git a/modules/log/event_writer_console.go b/modules/log/event_writer_console.go new file mode 100644 index 0000000000000..78183de644baa --- /dev/null +++ b/modules/log/event_writer_console.go @@ -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) +} diff --git a/modules/log/event_writer_file.go b/modules/log/event_writer_file.go new file mode 100644 index 0000000000000..4f41b96453a00 --- /dev/null +++ b/modules/log/event_writer_file.go @@ -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) +} diff --git a/modules/log/file.go b/modules/log/file.go deleted file mode 100644 index 2ec6de450c7ea..0000000000000 --- a/modules/log/file.go +++ /dev/null @@ -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) -} diff --git a/modules/log/file_test.go b/modules/log/file_test.go deleted file mode 100644 index 34f74598067ca..0000000000000 --- a/modules/log/file_test.go +++ /dev/null @@ -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) -} diff --git a/modules/log/flags.go b/modules/log/flags.go index 4a3732600bb9a..f025159d53946 100644 --- a/modules/log/flags.go +++ b/modules/log/flags.go @@ -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,26 +20,30 @@ 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 - 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 - Lshortfile // final file name element and line number: d.go:23. overrides Llongfile - 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 - Llevel // Provided level in brackets [INFO] - - // Last 20 characters of the filename - Lmedfile = Lshortfile | Llongfile - - // LstdFlags is the initial value for the standard logger - LstdFlags = Ldate | Ltime | Lmedfile | Lshortfuncname | Llevelinitial + 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 + Lshortfile // final file name element and line number: d.go:23. overrides Llongfile + 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 + Llevel // Provided level in brackets [INFO] + Lgopid // the Goroutine-PID of the context + + 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, - "medfile": Lmedfile, - "stdflags": LstdFlags, + "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 flags == 0 { - return -1 + if len(flagNames) == 0 { + return "none" + } + 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} } diff --git a/modules/log/flags_test.go b/modules/log/flags_test.go new file mode 100644 index 0000000000000..03972a9fb06b0 --- /dev/null +++ b/modules/log/flags_test.go @@ -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()) +} diff --git a/modules/log/init.go b/modules/log/init.go new file mode 100644 index 0000000000000..798ba86410d29 --- /dev/null +++ b/modules/log/init.go @@ -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)) + } + } +} diff --git a/modules/log/level.go b/modules/log/level.go index 3c8a736b30a6c..01fa3f5e46b04 100644 --- a/modules/log/level.go +++ b/modules/log/level.go @@ -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{ - TRACE: "trace", - DEBUG: "debug", - INFO: "info", - WARN: "warn", - ERROR: "error", - CRITICAL: "critical", - FATAL: "fatal", - NONE: "none", + UNDEFINED: "undefined", + + TRACE: "trace", + DEBUG: "debug", + INFO: "info", + WARN: "warn", + ERROR: "error", + + FATAL: "fatal", + NONE: "none", } var toLevel = map[string]Level{ - "trace": TRACE, - "debug": DEBUG, - "info": INFO, - "warn": WARN, - "error": ERROR, - "critical": CRITICAL, - "fatal": FATAL, - "none": NONE, + "undefined": UNDEFINED, + + "trace": TRACE, + "debug": DEBUG, + "info": INFO, + "warn": WARN, + "warning": WARN, + "error": ERROR, + + "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 +} diff --git a/modules/log/log.go b/modules/log/log.go deleted file mode 100644 index eee2728bf68d1..0000000000000 --- a/modules/log/log.go +++ /dev/null @@ -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) - } -} diff --git a/modules/log/log_test.go b/modules/log/log_test.go deleted file mode 100644 index 819cdb521fa59..0000000000000 --- a/modules/log/log_test.go +++ /dev/null @@ -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) -} diff --git a/modules/log/logger.go b/modules/log/logger.go index 71949e29b8136..a833b6ef0fa71 100644 --- a/modules/log/logger.go +++ b/modules/log/logger.go @@ -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 } diff --git a/modules/log/logger_global.go b/modules/log/logger_global.go new file mode 100644 index 0000000000000..f100341254a5c --- /dev/null +++ b/modules/log/logger_global.go @@ -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) +} diff --git a/modules/log/logger_impl.go b/modules/log/logger_impl.go new file mode 100644 index 0000000000000..c7e8fde3c082d --- /dev/null +++ b/modules/log/logger_impl.go @@ -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 +} diff --git a/modules/log/logger_test.go b/modules/log/logger_test.go new file mode 100644 index 0000000000000..1fb63bf629ea4 --- /dev/null +++ b/modules/log/logger_test.go @@ -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()) +} diff --git a/modules/log/manager.go b/modules/log/manager.go new file mode 100644 index 0000000000000..bbaef7eb20ad9 --- /dev/null +++ b/modules/log/manager.go @@ -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 +} diff --git a/modules/log/manager_test.go b/modules/log/manager_test.go new file mode 100644 index 0000000000000..aa01f79980c59 --- /dev/null +++ b/modules/log/manager_test.go @@ -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) +} diff --git a/modules/log/misc.go b/modules/log/misc.go new file mode 100644 index 0000000000000..ae4ce04cf32b1 --- /dev/null +++ b/modules/log/misc.go @@ -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} +} diff --git a/modules/log/multichannel.go b/modules/log/multichannel.go deleted file mode 100644 index 6b8a9b82464ac..0000000000000 --- a/modules/log/multichannel.go +++ /dev/null @@ -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 -} diff --git a/modules/log/provider.go b/modules/log/provider.go deleted file mode 100644 index b5058139d7054..0000000000000 --- a/modules/log/provider.go +++ /dev/null @@ -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 -} diff --git a/modules/log/smtp.go b/modules/log/smtp.go deleted file mode 100644 index 4e896496d75fa..0000000000000 --- a/modules/log/smtp.go +++ /dev/null @@ -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) -} diff --git a/modules/log/smtp_test.go b/modules/log/smtp_test.go deleted file mode 100644 index d7d28f28f83bd..0000000000000 --- a/modules/log/smtp_test.go +++ /dev/null @@ -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() -} diff --git a/modules/log/stack.go b/modules/log/stack.go index d4496cff03fd6..9b22e92867c39 100644 --- a/modules/log/stack.go +++ b/modules/log/stack.go @@ -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() } diff --git a/modules/log/writer.go b/modules/log/writer.go deleted file mode 100644 index 61f1d866ee543..0000000000000 --- a/modules/log/writer.go +++ /dev/null @@ -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 "" -} diff --git a/modules/log/writer_test.go b/modules/log/writer_test.go deleted file mode 100644 index 8c03f87d907c1..0000000000000 --- a/modules/log/writer_test.go +++ /dev/null @@ -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) -} diff --git a/modules/private/manager.go b/modules/private/manager.go index 5853db34e4b74..3448f2e34c53f 100644 --- a/modules/private/manager.go +++ b/modules/private/manager.go @@ -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") } diff --git a/modules/setting/config_provider.go b/modules/setting/config_provider.go index ce9ef72248ad1..37f5754ffdb0c 100644 --- a/modules/setting/config_provider.go +++ b/modules/setting/config_provider.go @@ -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 diff --git a/modules/setting/config_provider_test.go b/modules/setting/config_provider_test.go new file mode 100644 index 0000000000000..76f7048d59c9e --- /dev/null +++ b/modules/setting/config_provider_test.go @@ -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")) +} diff --git a/modules/setting/database.go b/modules/setting/database.go index 8c4dfb21d7efd..7a7c7029a430d 100644 --- a/modules/setting/database.go +++ b/modules/setting/database.go @@ -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) diff --git a/modules/setting/log.go b/modules/setting/log.go index d9a9e5af8fd5c..af64ea8d85373 100644 --- a/modules/setting/log.go +++ b/modules/setting/log.go @@ -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, - } - } - return descs -} - -// AddLogDescription adds a set of descriptions to the complete description -func AddLogDescription(key string, description *LogDescription) { - descriptionLock.Lock() - defer descriptionLock.Unlock() - logDescriptions[key] = description -} +var Log LogGlobalConfig -// 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 - } - for i, sub := range desc.SubLogDescriptions { - if sub.Name == subLogDescription.Name { - desc.SubLogDescriptions[i] = subLogDescription - return true - } - } - desc.SubLogDescriptions = append(desc.SubLogDescriptions, subLogDescription) - return true -} +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}}"` -// 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 - } - for i, sub := range desc.SubLogDescriptions { - if sub.Name == name { - desc.SubLogDescriptions = append(desc.SubLogDescriptions[:i], desc.SubLogDescriptions[i+1:]...) - return true - } - } - return false -} +func loadLogGlobalFrom(rootCfg ConfigProvider) { + sec := rootCfg.Section("log") -type defaultLogOptions struct { - levelName string // LogLevel - flags string - filename string // path.Join(LogRootPath, "gitea.log") - bufferLength int64 - disableConsole bool -} + 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") -func newDefaultLogOptions() defaultLogOptions { - return defaultLogOptions{ - levelName: Log.Level.String(), - flags: "stdflags", - filename: filepath.Join(Log.RootPath, "gitea.log"), - bufferLength: 10000, - disableConsole: false, + Log.RootPath = sec.Key("ROOT_PATH").MustString(path.Join(AppWorkPath, "log")) + if !filepath.IsAbs(Log.RootPath) { + Log.RootPath = filepath.Join(AppWorkPath, Log.RootPath) } -} + Log.RootPath = util.FilePathJoinAbs(Log.RootPath) -// SubLogDescription describes a sublogger -type SubLogDescription struct { - Name string - Provider string - Config string -} + Log.EnableSSHLog = sec.Key("ENABLE_SSH_LOG").MustBool(false) -// LogDescription describes a named logger -type LogDescription struct { - Name string - SubLogDescriptions []SubLogDescription + Log.AccessLogTemplate = sec.Key("ACCESS_LOG_TEMPLATE").MustString(accessLogTemplateDefault) + Log.RequestIDHeaders = sec.Key("REQUEST_ID_HEADERS").Strings(",") } -func getLogLevel(section ConfigSection, key string, defaultValue log.Level) log.Level { - value := section.Key(key).MustString(defaultValue.String()) - return log.FromString(value) -} +func prepareLoggerConfig(rootCfg ConfigProvider) { + sec := rootCfg.Section("log") -func getStacktraceLogLevel(section ConfigSection, key, defaultValue string) string { - value := section.Key(key).MustString(defaultValue) - return log.FromString(value).String() -} + if !sec.HasKey("logger.default.MODE") { + sec.Key("logger.default.MODE").MustString(",") + } -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) + 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("") + } - 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") + 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) + } + 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("") + } - 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) + 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("") + } +} - Log.EnableXORMLog = rootCfg.Section("log").Key("ENABLE_XORM_LOG").MustBool(true) +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 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("") - } +func loadLogModeByName(rootCfg ConfigProvider, loggerName, modeName string) (writerName, writerType string, writerMode log.WriterMode, err error) { + sec := rootCfg.Section("log." + modeName) + + 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) + log.FallbackErrorf("Failed to load writer mode %q for logger %s: %v", modeName, loggerName, err) + continue + } + if writerMode.BufferLen == 0 { + writerMode.BufferLen = Log.BufferLen + } + eventWriter := manager.GetSharedWriter(writerName) + if eventWriter == nil { + eventWriter, err = manager.NewSharedWriter(writerName, writerType, writerMode) if err != nil { - sec, _ = rootCfg.NewSection("log." + name) + log.FallbackErrorf("Failed to create event writer for logger %s: %v", loggerName, err) + continue } } - - 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) - } - - 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))) + manager.GetLogger(loggerName).RemoveAllWriters().AddWriters(eventWriters...) } -// RestartLogsWithPIDSuffix restarts the logs with a PID suffix on files -func RestartLogsWithPIDSuffix() { - filenameSuffix = fmt.Sprintf(".%d", os.Getpid()) - InitLogs(false) +func InitSQLLoggersForCli(level log.Level) { + log.SetConsoleLogger("xorm", "console", level) } -// 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) -} - -// 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") } diff --git a/modules/setting/log_test.go b/modules/setting/log_test.go new file mode 100644 index 0000000000000..c07651f5488fa --- /dev/null +++ b/modules/setting/log_test.go @@ -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)) +} diff --git a/modules/setting/repository.go b/modules/setting/repository.go index 900b56cc52b33..5520c992b969e 100644 --- a/modules/setting/repository.go +++ b/modules/setting/repository.go @@ -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 { diff --git a/modules/setting/server.go b/modules/setting/server.go index 183906268576c..d937faca1012b 100644 --- a/modules/setting/server.go +++ b/modules/setting/server.go @@ -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 } diff --git a/modules/setting/setting.go b/modules/setting/setting.go index b085a7b321495..8f20ef085690b 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -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) diff --git a/modules/ssh/ssh.go b/modules/ssh/ssh.go index 9ec14f2caa744..4bf57eafb721f 100644 --- a/modules/ssh/ssh.go +++ b/modules/ssh/ssh.go @@ -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.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()) - } + 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()) - } + 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) diff --git a/modules/templates/htmlrenderer.go b/modules/templates/htmlrenderer.go index d60be887278a5..311e5b741d430 100644 --- a/modules/templates/htmlrenderer.go +++ b/modules/templates/htmlrenderer.go @@ -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 { diff --git a/modules/test/logchecker.go b/modules/test/logchecker.go index 8f8c753c76fcc..7bf234f5604a9 100644 --- a/modules/test/logchecker.go +++ b/modules/test/logchecker.go @@ -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. diff --git a/modules/test/logchecker_test.go b/modules/test/logchecker_test.go index 4dfea8c3e3825..6b093ab1b36ac 100644 --- a/modules/test/logchecker_test.go +++ b/modules/test/logchecker_test.go @@ -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() diff --git a/modules/testlogger/testlogger.go b/modules/testlogger/testlogger.go index cc80e86c81cb7..b4275e6005706 100644 --- a/modules/testlogger/testlogger.go +++ b/modules/testlogger/testlogger.go @@ -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...) -} - -// 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() { + _, _ = fmt.Fprintf(os.Stdout, "\t"+format, args...) } -// 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() { diff --git a/modules/util/rotatingfilewriter/writer.go b/modules/util/rotatingfilewriter/writer.go new file mode 100644 index 0000000000000..5243bfe353861 --- /dev/null +++ b/modules/util/rotatingfilewriter/writer.go @@ -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) + } +} diff --git a/modules/util/rotatingfilewriter/writer_test.go b/modules/util/rotatingfilewriter/writer_test.go new file mode 100644 index 0000000000000..88392797b375a --- /dev/null +++ b/modules/util/rotatingfilewriter/writer_test.go @@ -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) +} diff --git a/modules/web/routing/logger.go b/modules/web/routing/logger.go index d1b0ff0cda6d1..b58065aa7365b 100644 --- a/modules/web/routing/logger.go +++ b/modules/web/routing/logger.go @@ -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)), diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index e092d3722cf45..6305a3907b7be 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -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 diff --git a/routers/common/middleware.go b/routers/common/middleware.go index a25ff1ee00a5e..8a39dda179893 100644 --- a/routers/common/middleware.go +++ b/routers/common/middleware.go @@ -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()) } diff --git a/routers/private/internal.go b/routers/private/internal.go index c6ea9e026383d..b09fb58d05ddf 100644 --- a/routers/private/internal.go +++ b/routers/private/internal.go @@ -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) diff --git a/routers/private/manager.go b/routers/private/manager.go index 38ad83326fe10..8ed05da6a593b 100644 --- a/routers/private/manager.go +++ b/routers/private/manager.go @@ -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 + writerMode.Colorize = log.CanColorStdout } } - if _, ok := opts.Config["level"]; !ok { - opts.Config["level"] = setting.Log.Level + writerMode.Level = setting.Log.Level + if level, ok := opts.Config["level"].(string); ok { + writerMode.Level = log.LevelFromString(level) } - if _, ok := opts.Config["stacktraceLevel"]; !ok { - opts.Config["stacktraceLevel"] = setting.Log.StacktraceLogLevel + writerMode.StacktraceLevel = setting.Log.StacktraceLogLevel + if stacktraceLevel, ok := opts.Config["level"].(string); ok { + writerMode.StacktraceLevel = log.LevelFromString(stacktraceLevel) } - if opts.Mode == "file" { - if _, ok := opts.Config["maxsize"]; !ok { - opts.Config["maxsize"] = 1 << 28 + 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 } - if _, ok := opts.Config["maxdays"]; !ok { - opts.Config["maxdays"] = 7 + writerOption.MaxSize = 1 << maxSizeShift + writerOption.DailyRotate, _ = opts.Config["daily"].(bool) + writerOption.MaxDays, _ = opts.Config["maxdays"].(int) + if writerOption.MaxDays == 0 { + writerOption.MaxDays = 7 } - if _, ok := opts.Config["compressionLevel"]; !ok { - opts.Config["compressionLevel"] = -1 + 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") } diff --git a/routers/web/admin/config.go b/routers/web/admin/config.go index 7460ea24a7456..be662c22efdcc 100644 --- a/routers/web/admin/config.go +++ b/routers/web/admin/config.go @@ -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) } diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index 7a0dc9940beb0..d7ea3f12f1a7a 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -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) diff --git a/routers/web/repo/issue_watch.go b/routers/web/repo/issue_watch.go index 1837c2b632c96..d3d3a2af21e36 100644 --- a/routers/web/repo/issue_watch.go +++ b/routers/web/repo/issue_watch.go @@ -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) diff --git a/services/migrations/codebase.go b/services/migrations/codebase.go index c02c8e13a2e3a..22bf0f73de715 100644 --- a/services/migrations/codebase.go +++ b/services/migrations/codebase.go @@ -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, "") - return + return "" } - log.ColorFprintf(s, "migration from codebase server %s %s/%s", d.baseURL, d.project, d.repoName) + return fmt.Sprintf("", d.baseURL, d.project, d.repoName) } // FormatCloneURL add authentication into remote URLs diff --git a/services/migrations/gitbucket.go b/services/migrations/gitbucket.go index cc3d4fc93674a..5f11555839b5c 100644 --- a/services/migrations/gitbucket.go +++ b/services/migrations/gitbucket.go @@ -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, "") - return + return "" } - log.ColorFprintf(s, "migration from gitbucket server %s %s/%s", g.baseURL, g.repoOwner, g.repoName) + return fmt.Sprintf("", g.baseURL, g.repoOwner, g.repoName) } // NewGitBucketDownloader creates a GitBucket downloader diff --git a/services/migrations/gitea_downloader.go b/services/migrations/gitea_downloader.go index 470090b5010a5..b9ba93325b91a 100644 --- a/services/migrations/gitea_downloader.go +++ b/services/migrations/gitea_downloader.go @@ -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, "") - return + return "" } - log.ColorFprintf(s, "migration from gitea server %s %s/%s", g.baseURL, g.repoOwner, g.repoName) + return fmt.Sprintf("", g.baseURL, g.repoOwner, g.repoName) } // GetRepoInfo returns a repository information diff --git a/services/migrations/github.go b/services/migrations/github.go index 3e63fddb6a5d9..ca6e037b68cd6 100644 --- a/services/migrations/github.go +++ b/services/migrations/github.go @@ -125,13 +125,11 @@ func (g *GithubDownloaderV3) String() string { return fmt.Sprintf("migration from github server %s %s/%s", g.baseURL, g.repoOwner, g.repoName) } -// ColorFormat provides a basic color format for a GithubDownloader -func (g *GithubDownloaderV3) ColorFormat(s fmt.State) { +func (g *GithubDownloaderV3) LogString() string { if g == nil { - log.ColorFprintf(s, "") - return + return "" } - log.ColorFprintf(s, "migration from github server %s %s/%s", g.baseURL, g.repoOwner, g.repoName) + return fmt.Sprintf("", g.baseURL, g.repoOwner, g.repoName) } func (g *GithubDownloaderV3) addClient(client *http.Client, baseURL string) { diff --git a/services/migrations/gitlab.go b/services/migrations/gitlab.go index 8034869a4ae4b..015c38cd3b0cb 100644 --- a/services/migrations/gitlab.go +++ b/services/migrations/gitlab.go @@ -137,13 +137,11 @@ func (g *GitlabDownloader) String() string { return fmt.Sprintf("migration from gitlab server %s [%d]/%s", g.baseURL, g.repoID, g.repoName) } -// ColorFormat provides a basic color format for a GitlabDownloader -func (g *GitlabDownloader) ColorFormat(s fmt.State) { +func (g *GitlabDownloader) LogString() string { if g == nil { - log.ColorFprintf(s, "") - return + return "" } - log.ColorFprintf(s, "migration from gitlab server %s [%d]/%s", g.baseURL, g.repoID, g.repoName) + return fmt.Sprintf("", g.baseURL, g.repoID, g.repoName) } // SetContext set context diff --git a/services/migrations/gogs.go b/services/migrations/gogs.go index d01934ac6d525..72c52d180b9bc 100644 --- a/services/migrations/gogs.go +++ b/services/migrations/gogs.go @@ -77,13 +77,11 @@ func (g *GogsDownloader) String() string { return fmt.Sprintf("migration from gogs server %s %s/%s", g.baseURL, g.repoOwner, g.repoName) } -// ColorFormat provides a basic color format for a GogsDownloader -func (g *GogsDownloader) ColorFormat(s fmt.State) { +func (g *GogsDownloader) LogString() string { if g == nil { - log.ColorFprintf(s, "") - return + return "" } - log.ColorFprintf(s, "migration from gogs server %s %s/%s", g.baseURL, g.repoOwner, g.repoName) + return fmt.Sprintf("", g.baseURL, g.repoOwner, g.repoName) } // SetContext set context diff --git a/services/migrations/onedev.go b/services/migrations/onedev.go index d4b1b73d37c27..33fc43c349619 100644 --- a/services/migrations/onedev.go +++ b/services/migrations/onedev.go @@ -114,13 +114,11 @@ func (d *OneDevDownloader) String() string { return fmt.Sprintf("migration from oneDev server %s [%d]/%s", d.baseURL, d.repoID, d.repoName) } -// ColorFormat provides a basic color format for a OneDevDownloader -func (d *OneDevDownloader) ColorFormat(s fmt.State) { +func (d *OneDevDownloader) LogString() string { if d == nil { - log.ColorFprintf(s, "") - return + return "" } - log.ColorFprintf(s, "migration from oneDev server %s [%d]/%s", d.baseURL, d.repoID, d.repoName) + return fmt.Sprintf("", d.baseURL, d.repoID, d.repoName) } func (d *OneDevDownloader) callAPI(endpoint string, parameter map[string]string, result interface{}) error { diff --git a/templates/admin/config.tmpl b/templates/admin/config.tmpl index c4f77ec1ae231..629632a7187a5 100644 --- a/templates/admin/config.tmpl +++ b/templates/admin/config.tmpl @@ -361,66 +361,23 @@
- {{range .Loggers.default.SubLogDescriptions}} -
{{$.locale.Tr "admin.config.log_mode"}}
-
{{.Name}} ({{.Provider}})
-
{{$.locale.Tr "admin.config.log_config"}}
-
{{JsonUtils.PrettyIndent .Config}}
- {{end}} -
-
{{$.locale.Tr "admin.config.router_log_mode"}}
- {{if .DisableRouterLog}} -
{{$.locale.Tr "admin.config.disabled_logger"}}
- {{else}} - {{if .Loggers.router.SubLogDescriptions}} -
{{$.locale.Tr "admin.config.own_named_logger"}}
- {{range .Loggers.router.SubLogDescriptions}} -
{{$.locale.Tr "admin.config.log_mode"}}
-
{{.Name}} ({{.Provider}})
-
{{$.locale.Tr "admin.config.log_config"}}
-
{{JsonUtils.PrettyIndent .Config}}
- {{end}} - {{else}} -
{{$.locale.Tr "admin.config.routes_to_default_logger"}}
- {{end}} + {{if .Loggers.xorm.IsEnabled}} +
{{$.locale.Tr "admin.config.xorm_log_sql"}}
+
{{if $.LogSQL}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}
{{end}} -
-
{{$.locale.Tr "admin.config.access_log_mode"}}
- {{if .EnableAccessLog}} - {{if .Loggers.access.SubLogDescriptions}} -
{{$.locale.Tr "admin.config.own_named_logger"}}
- {{range .Loggers.access.SubLogDescriptions}} -
{{$.locale.Tr "admin.config.log_mode"}}
-
{{.Name}} ({{.Provider}})
-
{{$.locale.Tr "admin.config.log_config"}}
-
{{JsonUtils.PrettyIndent .Config}}
- {{end}} - {{else}} -
{{$.locale.Tr "admin.config.routes_to_default_logger"}}
- {{end}} + + {{if .Loggers.access.IsEnabled}}
{{$.locale.Tr "admin.config.access_log_template"}}
{{$.AccessLogTemplate}}
- {{else}} -
{{$.locale.Tr "admin.config.disabled_logger"}}
{{end}} -
-
{{$.locale.Tr "admin.config.xorm_log_mode"}}
- {{if .EnableXORMLog}} - {{if .Loggers.xorm.SubLogDescriptions}} -
{{$.locale.Tr "admin.config.own_named_logger"}}
- {{range .Loggers.xorm.SubLogDescriptions}} -
{{$.locale.Tr "admin.config.log_mode"}}
-
{{.Name}} ({{.Provider}})
-
{{$.locale.Tr "admin.config.log_config"}}
-
{{JsonUtils.PrettyIndent .Config}}
- {{end}} + + {{range $loggerName, $loggerDetail := .Loggers}} +
{{$.locale.Tr "admin.config.logger_name_fmt" $loggerName}}
+ {{if $loggerDetail.IsEnabled}} +
{{$loggerDetail.EventWriters | JsonUtils.EncodeToString | JsonUtils.PrettyIndent}}
{{else}} -
{{$.locale.Tr "admin.config.routes_to_default_logger"}}
+
{{$.locale.Tr "admin.config.disabled_logger"}}
{{end}} -
{{$.locale.Tr "admin.config.xorm_log_sql"}}
-
{{if $.LogSQL}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}
- {{else}} -
{{$.locale.Tr "admin.config.disabled_logger"}}
{{end}}
diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go index e75b3db38ffb9..fbcb0e7da56a4 100644 --- a/tests/e2e/e2e_test.go +++ b/tests/e2e/e2e_test.go @@ -31,7 +31,7 @@ import ( var c *web.Route func TestMain(m *testing.M) { - defer log.Close() + defer log.GetManager().Close() managerCtx, cancel := context.WithCancel(context.Background()) graceful.InitManager(managerCtx) diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go index 01f26d567fd30..27918a9cccac8 100644 --- a/tests/integration/integration_test.go +++ b/tests/integration/integration_test.go @@ -80,7 +80,7 @@ func NewNilResponseHashSumRecorder() *NilResponseHashSumRecorder { } func TestMain(m *testing.M) { - defer log.Close() + defer log.GetManager().Close() managerCtx, cancel := context.WithCancel(context.Background()) graceful.InitManager(managerCtx) diff --git a/tests/integration/migration-test/migration_test.go b/tests/integration/migration-test/migration_test.go index a68d458a0b1ea..556a54015b4f1 100644 --- a/tests/integration/migration-test/migration_test.go +++ b/tests/integration/migration-test/migration_test.go @@ -24,7 +24,9 @@ import ( "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/testlogger" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/tests" @@ -35,6 +37,8 @@ import ( var currentEngine *xorm.Engine func initMigrationTest(t *testing.T) func() { + log.RegisterEventWriter("test", testlogger.NewTestLoggerWriter) + deferFn := tests.PrintCurrentTest(t, 2) giteaRoot := base.SetupGiteaRoot() if giteaRoot == "" { @@ -84,7 +88,7 @@ func initMigrationTest(t *testing.T) func() { assert.NoError(t, git.InitFull(context.Background())) setting.LoadDBSetting() - setting.InitLogs(true) + setting.InitLoggersForTest() return deferFn } @@ -292,7 +296,7 @@ func doMigrationTest(t *testing.T, version string) { return } - setting.InitSQLLog(false) + setting.InitSQLLoggersForCli(log.INFO) err := db.InitEngineWithMigration(context.Background(), wrappedMigrate) assert.NoError(t, err) diff --git a/tests/test_utils.go b/tests/test_utils.go index de022fde0a1f7..6f0fb25560d9b 100644 --- a/tests/test_utils.go +++ b/tests/test_utils.go @@ -36,6 +36,8 @@ func exitf(format string, args ...interface{}) { } func InitTest(requireGitea bool) { + log.RegisterEventWriter("test", testlogger.NewTestLoggerWriter) + giteaRoot := base.SetupGiteaRoot() if giteaRoot == "" { exitf("Environment variable $GITEA_ROOT not set") @@ -246,7 +248,3 @@ func PrintCurrentTest(t testing.TB, skip ...int) func() { func Printf(format string, args ...interface{}) { testlogger.Printf(format, args...) } - -func init() { - log.Register("test", testlogger.NewTestLogger) -}