Copyright(C)2020-2023. Huawei Technologies Co.,Ltd. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package log
import (
"compress/gzip"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"syscall"
"time"
)
const (
fileModeUserRoForArchiveLog = 0440
fileModeUserRwForWritingLog = 0640
dirModeUserRwxForLogDir = 0750
hourPerDay = 24
compressSuffix = ".gz"
defaultLogPath = "/var/log"
defaultLogMaxSize = 20
backupTimeFormat = "20060102T150405.000"
)
type FileLogger struct {
fileName string
maxSize int
maxAge int
maxBackups int
localTime bool
writtenLen int64
file *os.File
mutex sync.Mutex
staleLogCh chan bool
startHandleStaleLog sync.Once
}
var _ io.WriteCloser = (*FileLogger)(nil)
var (
currentTime = time.Now
megabyte = 1024 * 1024
)
type logInfosWithTimestamp []logInfo
type logInfo struct {
timestamp time.Time
os.FileInfo
}
func (b logInfosWithTimestamp) Less(i, j int) bool {
if i >= b.Len() || j >= b.Len() {
return false
}
return b[i].timestamp.After(b[j].timestamp)
}
func (b logInfosWithTimestamp) Swap(i, j int) {
if i >= b.Len() || j >= b.Len() {
return
}
b[i], b[j] = b[j], b[i]
}
func (b logInfosWithTimestamp) Len() int {
return len(b)
}
func openLogFile(name string, flag int, perm os.FileMode) (*os.File, error) {
file, err := os.OpenFile(name, flag, perm)
if err != nil {
return nil, err
}
return file, nil
}
func (l *FileLogger) Write(bytes []byte) (int, error) {
l.mutex.Lock()
defer l.mutex.Unlock()
writeLen := int64(len(bytes))
if writeLen > l.maxLogContextLen() {
return 0, fmt.Errorf("write length %d exceeds maximum file writtenLen %d", writeLen, l.maxLogContextLen())
}
if l.file == nil {
if err := l.openExistingOrNew(len(bytes)); err != nil {
return 0, err
}
}
if l.writtenLen+writeLen >= l.maxLogContextLen() {
if err := l.rotate(); err != nil {
return 0, err
}
}
n, err := l.file.Write(bytes)
l.writtenLen += int64(n)
return n, err
}
func (l *FileLogger) Close() error {
l.mutex.Lock()
defer l.mutex.Unlock()
return l.close()
}
func (l *FileLogger) close() error {
if l.file == nil {
return nil
}
err := l.file.Close()
l.file = nil
return err
}
func (l *FileLogger) Rotate() error {
l.mutex.Lock()
defer l.mutex.Unlock()
return l.rotate()
}
func (l *FileLogger) rotate() error {
if err := l.close(); err != nil {
return err
}
if err := l.openNew(); err != nil {
return err
}
l.postRotateCompressLog()
return nil
}
func (l *FileLogger) openNew() error {
err := os.MkdirAll(l.dir(), dirModeUserRwxForLogDir)
if err != nil {
return fmt.Errorf("can't make directories for new logfile: %s", err)
}
name := l.filename()
info, err := os.Stat(name)
if err == nil {
newName := backupName(name, l.localTime)
if err := os.Rename(name, newName); err != nil {
return fmt.Errorf("can't rename log file: %s", err)
}
err = os.Chmod(newName, os.FileMode(fileModeUserRoForArchiveLog))
if err != nil {
return fmt.Errorf("error changing permissions: %v", err)
}
if err := createAndChown(name, info); err != nil {
return err
}
}
f, err := openLogFile(name, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(fileModeUserRwForWritingLog))
if err != nil {
return fmt.Errorf("can't open new logfile: %s", err)
}
l.file = f
l.writtenLen = 0
return nil
}
func backupName(name string, local bool) string {
dir := filepath.Dir(name)
fileName := filepath.Base(name)
ext := filepath.Ext(fileName)
prefix := fileName[:len(fileName)-len(ext)]
t := currentTime()
if !local {
t = t.UTC()
}
timestamp := t.Format(backupTimeFormat)
return filepath.Join(dir, fmt.Sprintf("%s-%s%s", prefix, timestamp, ext))
}
func (l *FileLogger) openExistingOrNew(writeLen int) error {
l.postRotateCompressLog()
filename := l.filename()
info, err := os.Stat(filename)
if os.IsNotExist(err) {
return l.openNew()
}
if err != nil {
return fmt.Errorf("error getting log file info: %s", err)
}
if info.Size()+int64(writeLen) >= l.maxLogContextLen() {
return l.rotate()
}
file, err := openLogFile(filename, os.O_APPEND|os.O_WRONLY, os.FileMode(fileModeUserRwForWritingLog))
if err != nil {
return l.openNew()
}
l.file = file
l.writtenLen = info.Size()
return nil
}
func (l *FileLogger) filename() string {
l.fileName = filepath.Clean(l.fileName)
isAbsolute := filepath.IsAbs(l.fileName)
if !isAbsolute {
l.fileName = filepath.Join(defaultLogPath, filepath.Base(l.fileName))
}
return l.fileName
}
func (l *FileLogger) handleStaleLogRunOnce() error {
if l.maxBackups == 0 && l.maxAge == 0 {
return nil
}
files, err := l.oldLogFiles()
if err != nil {
return err
}
preserved := make(map[string]bool)
cutoff := time.Now().Add(-time.Duration(l.maxAge) * hourPerDay * time.Hour)
var toCompress, toRemove []logInfo
for _, f := range files {
baseName := f.Name()
isCompressed := strings.HasSuffix(baseName, compressSuffix)
if isCompressed {
baseName = baseName[:len(baseName)-len(compressSuffix)]
}
if (l.maxBackups != 0 && len(preserved) >= l.maxBackups) || (l.maxAge != 0 && f.timestamp.Before(cutoff)) {
toRemove = append(toRemove, f)
} else {
preserved[baseName] = true
if !isCompressed {
toCompress = append(toCompress, f)
}
}
}
for _, f := range toRemove {
if errRemove := os.Remove(filepath.Join(l.dir(), f.Name())); errRemove != nil {
err = errRemove
}
}
for _, f := range toCompress {
fn := filepath.Join(l.dir(), f.Name())
if errCompress := compressLogFile(fn, fn+compressSuffix); errCompress != nil {
err = errCompress
}
}
return err
}
func (l *FileLogger) handleStaleLogRun() {
for range l.staleLogCh {
err := l.handleStaleLogRunOnce()
if err != nil {
fmt.Errorf("handle stale log err: %v", err)
}
}
}
func (l *FileLogger) postRotateCompressLog() {
l.startHandleStaleLog.Do(func() {
l.staleLogCh = make(chan bool, 1)
go l.handleStaleLogRun()
})
select {
case l.staleLogCh <- true:
default:
}
}
func (l *FileLogger) oldLogFiles() ([]logInfo, error) {
l.mutex.Lock()
defer l.mutex.Unlock()
files, err := os.ReadDir(l.dir())
if err != nil {
return nil, fmt.Errorf("can't read log file directory: %s", err)
}
var logFiles []logInfo
prefix, ext := l.getLogFilePrefixAndExt()
for _, f := range files {
fileInfo, fileErr := f.Info()
if fileErr != nil {
fmt.Printf("can't read log file info: %s\n", fileErr)
}
if fileInfo.IsDir() {
continue
}
if t, err := l.extractTimeFromFileName(f.Name(), prefix, ext); err == nil {
logFiles = append(logFiles, logInfo{timestamp: t, FileInfo: fileInfo})
continue
}
if t, err := l.extractTimeFromFileName(f.Name(), prefix, ext+compressSuffix); err == nil {
logFiles = append(logFiles, logInfo{timestamp: t, FileInfo: fileInfo})
continue
}
}
sort.Sort(logInfosWithTimestamp(logFiles))
return logFiles, nil
}
func (l *FileLogger) extractTimeFromFileName(filename, prefix, ext string) (time.Time, error) {
if !strings.HasPrefix(filename, prefix) {
return time.Time{}, errors.New("mismatched prefix")
}
if !strings.HasSuffix(filename, ext) {
return time.Time{}, errors.New("mismatched extension")
}
ts := filename[len(prefix) : len(filename)-len(ext)]
return time.Parse(backupTimeFormat, ts)
}
func (l *FileLogger) maxLogContextLen() int64 {
if l.maxSize == 0 {
return int64(defaultLogMaxSize * megabyte)
}
return int64(l.maxSize) * int64(megabyte)
}
func (l *FileLogger) dir() string {
return filepath.Dir(l.filename())
}
func (l *FileLogger) getLogFilePrefixAndExt() (prefix, ext string) {
filename := filepath.Base(l.filename())
ext = filepath.Ext(filename)
prefix = filename[:len(filename)-len(ext)] + "-"
return prefix, ext
}
func createByOpen(name string, info os.FileInfo) error {
f, err := os.OpenFile(name, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, info.Mode())
if err != nil {
return err
}
defer f.Close()
return nil
}
func createAndChown(name string, info os.FileInfo) error {
if err := createByOpen(name, info); err != nil {
return err
}
stat, ok := info.Sys().(*syscall.Stat_t)
if !ok {
return fmt.Errorf("file info does not contain syscall.Stat_t")
}
return os.Chown(name, int(stat.Uid), int(stat.Gid))
}
func compressLogFile(src, dst string) (err error) {
srcFile, err := os.Open(src)
if err != nil {
return fmt.Errorf("open log file failed: %v", err)
}
defer srcFile.Close()
srcFileInfo, err := os.Stat(src)
if err != nil {
return fmt.Errorf("stat log file failed: %v", err)
}
if err := createAndChown(dst, srcFileInfo); err != nil {
return fmt.Errorf("createAndChown compressed log file failed: %v", err)
}
gzWriter, err := os.OpenFile(dst, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, srcFileInfo.Mode())
if err != nil {
return fmt.Errorf("open compressed log file failed: %v", err)
}
defer gzWriter.Close()
gzFile := gzip.NewWriter(gzWriter)
defer func() {
if err != nil {
if errRemove := os.Remove(dst); errRemove == nil {
err = fmt.Errorf("compress log file failed: %v", err)
} else {
err = fmt.Errorf("compress log file failed: %v, Remove %s failed too", err, dst)
}
}
}()
if _, err := io.Copy(gzFile, srcFile); err != nil {
return err
}
if err := gzFile.Close(); err != nil {
return err
}
if err := gzWriter.Close(); err != nil {
return err
}
if err := srcFile.Close(); err != nil {
return err
}
if err := os.Remove(src); err != nil {
return err
}
return nil
}