package ulog
import (
"context"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"time"
)
const (
DefaultMaxFileSize = 10 * 1024 * 1024
)
type LogLevel int
const (
DebugLevel LogLevel = iota
InfoLevel
WarnLevel
ErrorLevel
FatalLevel
)
func (l LogLevel) String() string {
switch l {
case DebugLevel:
return "DEBUG"
case InfoLevel:
return "INFO"
case WarnLevel:
return "WARN"
case ErrorLevel:
return "ERROR"
case FatalLevel:
return "FATAL"
default:
return fmt.Sprintf("UNKNOWN(%d)", l)
}
}
type Logger interface {
Debug(msg string, fields ...Field)
Info(msg string, fields ...Field)
Warn(msg string, fields ...Field)
Error(msg string, fields ...Field)
Fatal(msg string, fields ...Field)
With(fields ...Field) Logger
WithContext(ctx context.Context) Logger
ReplaceField(key string, value interface{}) Logger
}
type Field struct {
Key string
Value interface{}
}
func F(key string, value interface{}) Field {
return Field{Key: key, Value: value}
}
type ulog struct {
mu sync.Mutex
level LogLevel
output *os.File
filePath string
writer writer
fields []Field
sandboxId string
maxFileSize int64
rotation int
}
type writeSyncer struct {
file *os.File
}
type writer interface {
Write([]byte) (int, error)
Sync() error
}
func (w *writeSyncer) Write(p []byte) (n int, err error) {
return w.file.Write(p)
}
func (w *writeSyncer) Sync() error {
return w.file.Sync()
}
type stdoutWriter struct{}
func (w *stdoutWriter) Write(p []byte) (n int, err error) {
return os.Stdout.Write(p)
}
func (w *stdoutWriter) Sync() error {
return nil
}
type Config struct {
Level LogLevel
OutputPath string
OutputFile string
Stdout bool
MaxFileSize int64
}
var (
defaultLogger Logger
defaultConfig = Config{
Level: InfoLevel,
OutputPath: "/var/log/conchd/",
Stdout: true,
MaxFileSize: DefaultMaxFileSize,
}
)
func newStdoutLogger(level LogLevel, maxFileSize int64) Logger {
if maxFileSize == 0 {
maxFileSize = DefaultMaxFileSize
}
return &ulog{
level: level,
output: nil,
filePath: "",
writer: &stdoutWriter{},
maxFileSize: maxFileSize,
rotation: 0,
}
}
func Init(config Config) error {
if config.MaxFileSize == 0 {
config.MaxFileSize = defaultConfig.MaxFileSize
}
logger := &ulog{
level: config.Level,
maxFileSize: config.MaxFileSize,
rotation: 0,
}
onlyStdout := (config.OutputFile == "" && config.OutputPath == "" && config.Stdout)
fileMode := (config.OutputFile != "" || config.OutputPath != "")
bothMode := fileMode && config.Stdout
if onlyStdout {
logger.output = nil
logger.filePath = ""
logger.writer = &stdoutWriter{}
} else if fileMode {
var logFilePath string
if config.OutputFile != "" {
logFilePath = config.OutputFile
} else {
now := time.Now()
datetime := now.Format("20060102-150405")
logFileName := fmt.Sprintf("%s.log", datetime)
logFilePath = filepath.Join(config.OutputPath, logFileName)
}
logDir := filepath.Dir(logFilePath)
if err := os.MkdirAll(logDir, 0755); err != nil {
return fmt.Errorf("failed to create log directory: %w", err)
}
logFile, err := os.OpenFile(logFilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
return fmt.Errorf("failed to create log file: %w", err)
}
logger.output = logFile
logger.filePath = logFilePath
if bothMode {
logger.writer = &multiWriter{
file: logFile,
stdout: os.Stdout,
filePath: logFilePath,
maxFileSize: config.MaxFileSize,
baseName: filepath.Base(logFilePath),
outputPath: filepath.Dir(logFilePath),
rotation: &logger.rotation,
}
} else {
logger.writer = &writeSyncer{file: logFile}
}
} else {
config.OutputPath = defaultConfig.OutputPath
if err := os.MkdirAll(config.OutputPath, 0755); err != nil {
return fmt.Errorf("failed to create log directory: %w", err)
}
now := time.Now()
datetime := now.Format("20060102-150405")
logFileName := fmt.Sprintf("%s.log", datetime)
logFilePath := filepath.Join(config.OutputPath, logFileName)
logFile, err := os.OpenFile(logFilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
return fmt.Errorf("failed to create log file: %w", err)
}
logger.output = logFile
logger.filePath = logFilePath
logger.writer = &writeSyncer{file: logFile}
}
if defaultLogger != nil {
if oldLogger, ok := defaultLogger.(*ulog); ok && oldLogger.output != nil {
_ = oldLogger.output.Close()
}
}
defaultLogger = logger
return nil
}
type multiWriter struct {
file *os.File
stdout *os.File
filePath string
maxFileSize int64
baseName string
outputPath string
rotation *int
mu sync.Mutex
}
func (w *multiWriter) Write(p []byte) (n int, err error) {
w.mu.Lock()
defer w.mu.Unlock()
if err := w.checkAndRotate(); err != nil {
return 0, err
}
n, err = w.file.Write(p)
if err != nil {
return
}
return w.stdout.Write(p)
}
func (w *multiWriter) checkAndRotate() error {
info, err := w.file.Stat()
if err != nil {
return err
}
if info.Size() < w.maxFileSize {
return nil
}
_ = w.file.Sync()
_ = w.file.Close()
*w.rotation++
datetime := time.Now().Format("20060102-150405")
baseName := strings.TrimSuffix(w.baseName, ".log")
newFileName := fmt.Sprintf("%s.%d.log", baseName, *w.rotation)
newFilePath := filepath.Join(w.outputPath, newFileName)
newBaseName := datetime + ".log"
newFileName = fmt.Sprintf("%s.%d.log", newBaseName, *w.rotation)
newFilePath = filepath.Join(w.outputPath, newFileName)
newFile, err := os.OpenFile(newFilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
return fmt.Errorf("failed to create rotated log file: %w", err)
}
w.file = newFile
w.filePath = newFilePath
return nil
}
func (w *multiWriter) Sync() error {
return w.file.Sync()
}
func GetLogger() Logger {
if defaultLogger == nil {
if err := Init(defaultConfig); err != nil {
defaultLogger = newStdoutLogger(defaultConfig.Level, defaultConfig.MaxFileSize)
fmt.Fprintf(os.Stderr, "ulog: fallback to stdout logger: %v\n", err)
}
}
return defaultLogger
}
func SetLogger(logger Logger) {
defaultLogger = logger
}
func Debug(msg string, fields ...Field) {
GetLogger().Debug(msg, fields...)
}
func Info(msg string, fields ...Field) {
GetLogger().Info(msg, fields...)
}
func Warn(msg string, fields ...Field) {
GetLogger().Warn(msg, fields...)
}
func Error(msg string, fields ...Field) {
GetLogger().Error(msg, fields...)
}
func Fatal(msg string, fields ...Field) {
GetLogger().Fatal(msg, fields...)
}
func With(fields ...Field) Logger {
return GetLogger().With(fields...)
}
func debugEnabled() bool {
logger := GetLogger()
levelProvider, ok := logger.(interface{ GetLevel() LogLevel })
return !ok || levelProvider.GetLevel() <= DebugLevel
}
func TraceStart() time.Time {
if !debugEnabled() {
return time.Time{}
}
return time.Now()
}
func TraceCost(start time.Time, traceid string, msg string) time.Time {
if !debugEnabled() {
return time.Time{}
}
ms := float64(time.Since(start).Microseconds()) / 1000.0
GetLogger().Debug(msg,
F("traceid", traceid),
F("from", start.Format("05.000")),
F("cost", fmt.Sprintf("%.3fms", ms)))
return time.Now()
}
func WithContext(ctx context.Context) Logger {
return GetLogger().WithContext(ctx)
}
func ReplaceField(key string, value interface{}) Logger {
return GetLogger().ReplaceField(key, value)
}
func (l *ulog) log(level LogLevel, msg string, fields ...Field) {
if l.level > level {
return
}
var b strings.Builder
now := time.Now()
b.WriteString(now.Format("2006-01-02 15:04:05.000"))
b.WriteString(" ")
b.WriteString("[")
b.WriteString(level.String())
b.WriteString("] ")
if _, file, line, ok := caller(3); ok {
b.WriteString(filepath.Base(file))
b.WriteString(":")
b.WriteString(fmt.Sprintf("%d", line))
b.WriteString(" ")
}
if l.sandboxId != "" {
b.WriteString("[")
b.WriteString(l.sandboxId)
b.WriteString("] ")
}
b.WriteString(msg)
if len(fields) > 0 || len(l.fields) > 0 {
allFields := make([]Field, 0, len(fields)+len(l.fields))
allFields = append(allFields, l.fields...)
allFields = append(allFields, fields...)
b.WriteString(" ")
for i, f := range allFields {
if i > 0 {
b.WriteString(" ")
}
b.WriteString(fmt.Sprintf("%s=%v", f.Key, f.Value))
}
}
b.WriteString("\n")
l.mu.Lock()
defer l.mu.Unlock()
if l.maxFileSize > 0 {
if ws, ok := l.writer.(*writeSyncer); ok {
if info, err := l.output.Stat(); err == nil {
if info.Size() >= l.maxFileSize {
_ = l.writer.Sync()
_ = l.output.Close()
l.rotation++
datetime := time.Now().Format("20060102-150405")
ext := filepath.Ext(l.filePath)
dir := filepath.Dir(l.filePath)
newBaseName := datetime + ext
newFilePath := filepath.Join(dir, fmt.Sprintf("%s.%d", newBaseName, l.rotation))
newFile, err := os.OpenFile(newFilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to create rotated log file: %v\n", err)
if l.output, err = os.OpenFile(l.filePath, os.O_WRONLY|os.O_APPEND, 0644); err == nil {
l.writer = &writeSyncer{file: l.output}
}
} else {
l.output = newFile
l.filePath = newFilePath
l.writer = &writeSyncer{file: newFile}
ws.file = newFile
}
}
}
}
}
if _, err := l.writer.Write([]byte(b.String())); err != nil {
fmt.Fprintf(os.Stderr, "failed to write log: %v\n", err)
}
_ = l.writer.Sync()
if level == FatalLevel {
os.Exit(1)
}
}
func (l *ulog) Debug(msg string, fields ...Field) {
l.log(DebugLevel, msg, fields...)
}
func (l *ulog) Info(msg string, fields ...Field) {
l.log(InfoLevel, msg, fields...)
}
func (l *ulog) Warn(msg string, fields ...Field) {
l.log(WarnLevel, msg, fields...)
}
func (l *ulog) Error(msg string, fields ...Field) {
l.log(ErrorLevel, msg, fields...)
}
func (l *ulog) Fatal(msg string, fields ...Field) {
l.log(FatalLevel, msg, fields...)
}
func (l *ulog) With(fields ...Field) Logger {
newLogger := &ulog{
level: l.level,
output: l.output,
filePath: l.filePath,
writer: l.writer,
sandboxId: l.sandboxId,
}
newLogger.fields = make([]Field, 0, len(l.fields)+len(fields))
newLogger.fields = append(newLogger.fields, l.fields...)
for _, f := range fields {
if f.Key == "sandboxId" {
newLogger.sandboxId = fmt.Sprintf("%v", f.Value)
} else {
newLogger.fields = append(newLogger.fields, f)
}
}
return newLogger
}
func (l *ulog) ReplaceField(key string, value interface{}) Logger {
newLogger := &ulog{
level: l.level,
output: l.output,
filePath: l.filePath,
writer: l.writer,
sandboxId: l.sandboxId,
}
if key == "sandboxId" {
newLogger.sandboxId = fmt.Sprintf("%v", value)
newLogger.fields = append([]Field{}, l.fields...)
return newLogger
}
newLogger.fields = make([]Field, 0, len(l.fields))
found := false
for _, f := range l.fields {
if f.Key == key {
newLogger.fields = append(newLogger.fields, F(key, value))
found = true
} else {
newLogger.fields = append(newLogger.fields, f)
}
}
if !found {
newLogger.fields = append(newLogger.fields, F(key, value))
}
return newLogger
}
func (l *ulog) WithContext(ctx context.Context) Logger {
newLogger := &ulog{
level: l.level,
output: l.output,
filePath: l.filePath,
writer: l.writer,
sandboxId: l.sandboxId,
}
newLogger.fields = append([]Field{}, l.fields...)
if requestID := ctx.Value("request_id"); requestID != nil {
newLogger.fields = append(newLogger.fields, F("request_id", requestID))
}
if userID := ctx.Value("user_id"); userID != nil {
newLogger.fields = append(newLogger.fields, F("user_id", userID))
}
if traceID := ctx.Value("trace_id"); traceID != nil {
newLogger.fields = append(newLogger.fields, F("trace_id", traceID))
}
return newLogger
}
func caller(skip int) (uintptr, string, int, bool) {
pc, file, line, ok := runtime.Caller(skip)
return pc, file, line, ok
}
func (l *ulog) Close() error {
l.mu.Lock()
defer l.mu.Unlock()
if l.output == nil {
return nil
}
return l.output.Close()
}
func (l *ulog) GetFilePath() string {
return l.filePath
}
func (l *ulog) GetLevel() LogLevel {
l.mu.Lock()
defer l.mu.Unlock()
return l.level
}