You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
245 lines
6.6 KiB
245 lines
6.6 KiB
// Copyright (c) 2020 Proton Technologies AG |
|
// |
|
// This file is part of ProtonMail Bridge. |
|
// |
|
// ProtonMail Bridge is free software: you can redistribute it and/or modify |
|
// it under the terms of the GNU General Public License as published by |
|
// the Free Software Foundation, either version 3 of the License, or |
|
// (at your option) any later version. |
|
// |
|
// ProtonMail Bridge is distributed in the hope that it will be useful, |
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
// GNU General Public License for more details. |
|
// |
|
// You should have received a copy of the GNU General Public License |
|
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>. |
|
|
|
package config |
|
|
|
import ( |
|
"bytes" |
|
"errors" |
|
"fmt" |
|
"io/ioutil" |
|
"os" |
|
"path/filepath" |
|
"regexp" |
|
"runtime" |
|
"runtime/pprof" |
|
"sort" |
|
"strconv" |
|
"time" |
|
|
|
"github.com/ProtonMail/proton-bridge/pkg/sentry" |
|
"github.com/sirupsen/logrus" |
|
) |
|
|
|
type logConfiger interface { |
|
GetLogDir() string |
|
GetLogPrefix() string |
|
} |
|
|
|
const ( |
|
// Zendesk now has a file size limit of 20MB. When the last N log files |
|
// are zipped, it should fit under 20MB. Value in MB (average file has |
|
// few hundreds kB). |
|
maxLogFileSize = 10 * 1024 * 1024 //nolint[gochecknoglobals] |
|
// Including the current logfile. |
|
maxNumberLogFiles = 3 //nolint[gochecknoglobals] |
|
) |
|
|
|
// logFile is pointer to currently open file used by logrus. |
|
var logFile *os.File //nolint[gochecknoglobals] |
|
|
|
var logFileRgx = regexp.MustCompile("^v.*\\.log$") //nolint[gochecknoglobals] |
|
var logCrashRgx = regexp.MustCompile("^v.*_crash_.*\\.log$") //nolint[gochecknoglobals] |
|
|
|
// HandlePanic reports the crash to sentry or local file when sentry fails. |
|
func HandlePanic(cfg *Config, output string) { |
|
if !cfg.IsDevMode() { |
|
apiCfg := cfg.GetAPIConfig() |
|
if err := sentry.ReportSentryCrash(apiCfg.ClientID, apiCfg.AppVersion, apiCfg.UserAgent, errors.New(output)); err != nil { |
|
log.Error("Sentry crash report failed: ", err) |
|
} |
|
} |
|
|
|
filename := getLogFilename(cfg.GetLogPrefix() + "_crash_") |
|
filepath := filepath.Join(cfg.GetLogDir(), filename) |
|
f, err := os.OpenFile(filepath, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600) |
|
if err != nil { |
|
log.Error("Cannot open file to write crash report: ", err) |
|
return |
|
} |
|
|
|
_, _ = f.WriteString(output) |
|
_ = pprof.Lookup("goroutine").WriteTo(f, 2) |
|
|
|
log.Warn("Crash report saved to ", filepath) |
|
} |
|
|
|
// GetGID returns goroutine number which can be used to distiguish logs from |
|
// the concurent processes. Keep in mind that it returns the number of routine |
|
// which executes the function. |
|
func GetGID() uint64 { |
|
b := make([]byte, 64) |
|
b = b[:runtime.Stack(b, false)] |
|
b = bytes.TrimPrefix(b, []byte("goroutine ")) |
|
b = b[:bytes.IndexByte(b, ' ')] |
|
n, _ := strconv.ParseUint(string(b), 10, 64) |
|
return n |
|
} |
|
|
|
// SetupLog set up log level, formatter and output (file or stdout). |
|
// Returns whether should be used debug for IMAP and SMTP servers. |
|
func SetupLog(cfg logConfiger, levelFlag string) (debugClient, debugServer bool) { |
|
level, useFile := getLogLevelAndFile(levelFlag) |
|
|
|
logrus.SetLevel(level) |
|
|
|
if useFile { |
|
logrus.SetFormatter(&logrus.JSONFormatter{}) |
|
setLogFile(cfg.GetLogDir(), cfg.GetLogPrefix()) |
|
watchLogFileSize(cfg.GetLogDir(), cfg.GetLogPrefix()) |
|
} else { |
|
logrus.SetFormatter(&logrus.TextFormatter{ |
|
ForceColors: true, |
|
FullTimestamp: true, |
|
TimestampFormat: time.StampMilli, |
|
}) |
|
logrus.SetOutput(os.Stdout) |
|
} |
|
|
|
switch levelFlag { |
|
case "debug-client", "debug-client-json": |
|
debugClient = true |
|
case "debug-server", "debug-server-json", "trace": |
|
fmt.Println("THE LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA") |
|
log.Warning("================================================") |
|
log.Warning("THIS LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA") |
|
log.Warning("================================================") |
|
debugClient = true |
|
debugServer = true |
|
} |
|
|
|
return debugClient, debugServer |
|
} |
|
|
|
func setLogFile(logDir, logPrefix string) { |
|
if logFile != nil { |
|
return |
|
} |
|
|
|
filename := getLogFilename(logPrefix) |
|
var err error |
|
logFile, err = os.OpenFile(filepath.Join(logDir, filename), os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600) |
|
if err != nil { |
|
panic(err) |
|
} |
|
|
|
logrus.SetOutput(logFile) |
|
|
|
// Users sometimes change the name of the log file. We want to always log |
|
// information about bridge version (included in log prefix) and OS. |
|
log.Warn("Bridge version: ", logPrefix, " ", runtime.GOOS) |
|
} |
|
|
|
func getLogFilename(logPrefix string) string { |
|
currentTime := strconv.Itoa(int(time.Now().Unix())) |
|
return logPrefix + "_" + currentTime + ".log" |
|
} |
|
|
|
func watchLogFileSize(logDir, logPrefix string) { |
|
go func() { |
|
for { |
|
time.Sleep(60 * time.Second) |
|
checkLogFileSize(logDir, logPrefix) |
|
} |
|
}() |
|
} |
|
|
|
func checkLogFileSize(logDir, logPrefix string) { |
|
if logFile == nil { |
|
return |
|
} |
|
|
|
stat, err := logFile.Stat() |
|
if err != nil { |
|
log.Error("Log file size check failed: ", err) |
|
return |
|
} |
|
|
|
if stat.Size() >= maxLogFileSize { |
|
log.Warn("Current log file ", logFile.Name(), " is too big, opening new file") |
|
closeLogFile() |
|
setLogFile(logDir, logPrefix) |
|
} |
|
|
|
if err := clearLogs(logDir); err != nil { |
|
log.Error("Cannot clear logs ", err) |
|
} |
|
} |
|
|
|
func closeLogFile() { |
|
if logFile != nil { |
|
_ = logFile.Close() |
|
logFile = nil |
|
} |
|
} |
|
|
|
func clearLogs(logDir string) error { |
|
files, err := ioutil.ReadDir(logDir) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
var logsWithPrefix []string |
|
var crashesWithPrefix []string |
|
|
|
for _, file := range files { |
|
if logFileRgx.MatchString(file.Name()) { |
|
if logCrashRgx.MatchString(file.Name()) { |
|
crashesWithPrefix = append(crashesWithPrefix, file.Name()) |
|
} else { |
|
logsWithPrefix = append(logsWithPrefix, file.Name()) |
|
} |
|
} else { |
|
// Older versions of Bridge stored logs in subfolders for each version. |
|
// That also has to be cleared and the functionality can be removed after some time. |
|
if file.IsDir() { |
|
if err := clearLogs(filepath.Join(logDir, file.Name())); err != nil { |
|
return err |
|
} |
|
} else { |
|
removeLog(logDir, file.Name()) |
|
} |
|
} |
|
} |
|
|
|
removeOldLogs(logDir, logsWithPrefix) |
|
removeOldLogs(logDir, crashesWithPrefix) |
|
return nil |
|
} |
|
|
|
func removeOldLogs(logDir string, filenames []string) { |
|
count := len(filenames) |
|
if count <= maxNumberLogFiles { |
|
return |
|
} |
|
|
|
sort.Strings(filenames) // Sorted by timestamp: oldest first. |
|
for _, filename := range filenames[:count-maxNumberLogFiles] { |
|
removeLog(logDir, filename) |
|
} |
|
} |
|
|
|
func removeLog(logDir, filename string) { |
|
// We need to be sure to delete only log files. |
|
// Directory with logs can also contain other files. |
|
if !logFileRgx.MatchString(filename) { |
|
return |
|
} |
|
if err := os.RemoveAll(filepath.Join(logDir, filename)); err != nil { |
|
log.Error("Cannot remove old logs ", err) |
|
} |
|
}
|
|
|