f8e93ce423
Even if the log mode is `file`, there are still few logs printed to the console at the very beginning. That's fine but confusing. Someone will think the console is the only place to find logs, and get nothing helpful. See https://github.com/go-gitea/gitea/issues/22274#issuecomment-1367917717. There should be a reminder that there are no more logs to the console. And to avoid log loss, we should add configured loggers first, then remove console logger if there's no `console` in the mode. Tests with `MODE = file`: Before: <img width="1792" alt="image" src="https://user-images.githubusercontent.com/9418365/210079862-d591677f-347e-46ed-a548-bb2ddbb0885c.png"> After: <img width="1792" alt="image" src="https://user-images.githubusercontent.com/9418365/210080002-d66cc418-6888-4909-b370-d03f5986ef41.png"> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: delvh <dev.lh@web.de>
367 lines
11 KiB
Go
367 lines
11 KiB
Go
// Copyright 2019 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package setting
|
|
|
|
import (
|
|
"fmt"
|
|
golog "log"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
|
|
"code.gitea.io/gitea/modules/json"
|
|
"code.gitea.io/gitea/modules/log"
|
|
"code.gitea.io/gitea/modules/util"
|
|
|
|
ini "gopkg.in/ini.v1"
|
|
)
|
|
|
|
var (
|
|
filenameSuffix = ""
|
|
descriptionLock = sync.RWMutex{}
|
|
logDescriptions = make(map[string]*LogDescription)
|
|
)
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
type defaultLogOptions struct {
|
|
levelName string // LogLevel
|
|
flags string
|
|
filename string // path.Join(LogRootPath, "gitea.log")
|
|
bufferLength int64
|
|
disableConsole bool
|
|
}
|
|
|
|
func newDefaultLogOptions() defaultLogOptions {
|
|
return defaultLogOptions{
|
|
levelName: LogLevel.String(),
|
|
flags: "stdflags",
|
|
filename: filepath.Join(LogRootPath, "gitea.log"),
|
|
bufferLength: 10000,
|
|
disableConsole: false,
|
|
}
|
|
}
|
|
|
|
// SubLogDescription describes a sublogger
|
|
type SubLogDescription struct {
|
|
Name string
|
|
Provider string
|
|
Config string
|
|
}
|
|
|
|
// LogDescription describes a named logger
|
|
type LogDescription struct {
|
|
Name string
|
|
SubLogDescriptions []SubLogDescription
|
|
}
|
|
|
|
func getLogLevel(section *ini.Section, key string, defaultValue log.Level) log.Level {
|
|
value := section.Key(key).MustString(defaultValue.String())
|
|
return log.FromString(value)
|
|
}
|
|
|
|
func getStacktraceLogLevel(section *ini.Section, key, defaultValue string) string {
|
|
value := section.Key(key).MustString(defaultValue)
|
|
return log.FromString(value).String()
|
|
}
|
|
|
|
func generateLogConfig(sec *ini.Section, name string, defaults defaultLogOptions) (mode, jsonConfig, levelName string) {
|
|
level := getLogLevel(sec, "LEVEL", LogLevel)
|
|
levelName = level.String()
|
|
stacktraceLevelName := getStacktraceLogLevel(sec, "STACKTRACE_LEVEL", 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(LogRootPath, logPath)
|
|
}
|
|
case "FLAGS":
|
|
flags = log.FlagsFromString(key.MustString(defaults.flags))
|
|
case "EXPRESSION":
|
|
expression = key.MustString("")
|
|
case "PREFIX":
|
|
prefix = key.MustString("")
|
|
}
|
|
}
|
|
|
|
logConfig := map[string]interface{}{
|
|
"level": level.String(),
|
|
"expression": expression,
|
|
"prefix": prefix,
|
|
"flags": flags,
|
|
"stacktraceLevel": stacktraceLevel.String(),
|
|
}
|
|
|
|
// Generate log configuration.
|
|
switch mode {
|
|
case "console":
|
|
useStderr := sec.Key("STDERR").MustBool(false)
|
|
logConfig["stderr"] = useStderr
|
|
if useStderr {
|
|
logConfig["colorize"] = sec.Key("COLORIZE").MustBool(log.CanColorStderr)
|
|
} else {
|
|
logConfig["colorize"] = sec.Key("COLORIZE").MustBool(log.CanColorStdout)
|
|
}
|
|
|
|
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)
|
|
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)
|
|
}
|
|
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
|
|
}
|
|
|
|
func generateNamedLogger(key string, options defaultLogOptions) *LogDescription {
|
|
description := LogDescription{
|
|
Name: key,
|
|
}
|
|
|
|
sections := strings.Split(Cfg.Section("log").Key(strings.ToUpper(key)).MustString(""), ",")
|
|
|
|
for i := 0; i < len(sections); i++ {
|
|
sections[i] = strings.TrimSpace(sections[i])
|
|
}
|
|
|
|
for _, name := range sections {
|
|
if len(name) == 0 || (name == "console" && options.disableConsole) {
|
|
continue
|
|
}
|
|
sec, err := Cfg.GetSection("log." + name + "." + key)
|
|
if err != nil {
|
|
sec, _ = Cfg.NewSection("log." + name + "." + key)
|
|
}
|
|
|
|
provider, config, levelName := generateLogConfig(sec, name, options)
|
|
|
|
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())
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
AddLogDescription(key, &description)
|
|
|
|
return &description
|
|
}
|
|
|
|
func newAccessLogService() {
|
|
EnableAccessLog = Cfg.Section("log").Key("ENABLE_ACCESS_LOG").MustBool(false)
|
|
AccessLogTemplate = Cfg.Section("log").Key("ACCESS_LOG_TEMPLATE").MustString(
|
|
`{{.Ctx.RemoteAddr}} - {{.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 `MustString` updates the default value, and `log.ACCESS` is used by `generateNamedLogger("access")` later
|
|
_ = Cfg.Section("log").Key("ACCESS").MustString("file")
|
|
if EnableAccessLog {
|
|
options := newDefaultLogOptions()
|
|
options.filename = filepath.Join(LogRootPath, "access.log")
|
|
options.flags = "" // For the router we don't want any prefixed flags
|
|
options.bufferLength = Cfg.Section("log").Key("BUFFER_LEN").MustInt64(10000)
|
|
generateNamedLogger("access", options)
|
|
}
|
|
}
|
|
|
|
func newRouterLogService() {
|
|
Cfg.Section("log").Key("ROUTER").MustString("console")
|
|
// Allow [log] DISABLE_ROUTER_LOG to override [server] DISABLE_ROUTER_LOG
|
|
DisableRouterLog = Cfg.Section("log").Key("DISABLE_ROUTER_LOG").MustBool(DisableRouterLog)
|
|
|
|
if !DisableRouterLog {
|
|
options := newDefaultLogOptions()
|
|
options.filename = filepath.Join(LogRootPath, "router.log")
|
|
options.flags = "date,time" // For the router we don't want any prefixed flags
|
|
options.bufferLength = Cfg.Section("log").Key("BUFFER_LEN").MustInt64(10000)
|
|
generateNamedLogger("router", options)
|
|
}
|
|
}
|
|
|
|
func newLogService() {
|
|
options := newDefaultLogOptions()
|
|
options.bufferLength = Cfg.Section("log").Key("BUFFER_LEN").MustInt64(10000)
|
|
EnableSSHLog = Cfg.Section("log").Key("ENABLE_SSH_LOG").MustBool(false)
|
|
|
|
description := LogDescription{
|
|
Name: log.DEFAULT,
|
|
}
|
|
|
|
sections := strings.Split(Cfg.Section("log").Key("MODE").MustString("console"), ",")
|
|
|
|
useConsole := false
|
|
for _, name := range sections {
|
|
name = strings.TrimSpace(name)
|
|
if name == "" {
|
|
continue
|
|
}
|
|
if name == "console" {
|
|
useConsole = true
|
|
}
|
|
|
|
sec, err := Cfg.GetSection("log." + name + ".default")
|
|
if err != nil {
|
|
sec, err = Cfg.GetSection("log." + name)
|
|
if err != nil {
|
|
sec, _ = Cfg.NewSection("log." + name)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
// Finally redirect the default golog to here
|
|
golog.SetFlags(0)
|
|
golog.SetPrefix("")
|
|
golog.SetOutput(log.NewLoggerAsWriter("INFO", log.GetLogger(log.DEFAULT)))
|
|
}
|
|
|
|
// RestartLogsWithPIDSuffix restarts the logs with a PID suffix on files
|
|
func RestartLogsWithPIDSuffix() {
|
|
filenameSuffix = fmt.Sprintf(".%d", os.Getpid())
|
|
NewLogServices(false)
|
|
}
|
|
|
|
// NewLogServices creates all the log services
|
|
func NewLogServices(disableConsole bool) {
|
|
newLogService()
|
|
newRouterLogService()
|
|
newAccessLogService()
|
|
NewXORMLogService(disableConsole)
|
|
}
|
|
|
|
// NewXORMLogService initializes xorm logger service
|
|
func NewXORMLogService(disableConsole bool) {
|
|
EnableXORMLog = Cfg.Section("log").Key("ENABLE_XORM_LOG").MustBool(true)
|
|
if EnableXORMLog {
|
|
options := newDefaultLogOptions()
|
|
options.filename = filepath.Join(LogRootPath, "xorm.log")
|
|
options.bufferLength = Cfg.Section("log").Key("BUFFER_LEN").MustInt64(10000)
|
|
options.disableConsole = disableConsole
|
|
|
|
Cfg.Section("log").Key("XORM").MustString(",")
|
|
generateNamedLogger("xorm", options)
|
|
}
|
|
}
|