// Copyright (c) 2024 Celestino Amoroso (celestino.amoroso@gmail.com). // All rights reserved. // logimpl.go package logimpl import ( "fmt" "io/fs" "os" "path" "slices" "strings" "time" "git.portale-stac.it/go-pkg/golang" "git.portale-stac.it/go-pkg/logger" "git.portale-stac.it/go-pkg/utils" ) // Implementation specific properties const ( // Max log file size that triggers the log rotation process // Rotation-Size == 0 means no rotation at all FULL_LOGGER_ROTATION_SIZE = int(iota + logger.IMPLEMENTATION_PROPERTY_BASE_ID) // Number of most recent files to keep FULL_LOGGER_RETENTION_NUMBER // Calling function offset in the process call stack FULL_LOGGER_STACK_OFFSET // Enable raw data dump into log file. See Dumpf() function. FULL_LOGGER_DUMPER_ENABLED // ATTENTION: Add new property id above this line FULL_LOGGER_DERIVED_BASE_ID ) // Log levels const ( LOG_NOLOG = int(iota) - 1 LOG_OOB LOG_FATAL LOG_ERROR LOG_WARN LOG_INFO LOG_DEBUG ) const ( MIN_LOG_ROTATE_SIZE = int64(5000) LOG_NO_SIZE_LIMIT = int64(0) ) const ( oobOffset = 1000 defaultMaxMessageLength = 1024 baseStackOffset = 3 ) var LOG_CLASS_COLOR = []uint{utils.BLACK, utils.RED, utils.RED, utils.MAGENTA, utils.BROWN, utils.CYAN} var logClassLabel = []string{"OOB", "FATAL", "ERROR", "WARN", "INFO", "DEBUG"} type logImpl struct { // TTY utils.TTYContext appendMode bool enabled bool //Variabile di controllo per bloccare la creazione del log prima che i suoi parametri siano acquisiti filePath string flags int rotateNumber int maxSize int64 size int64 stream *os.File maxLevel int debugOnStderr bool maxMessageLength int stackOffset int dumperEnabled bool customProperties map[int]any } func NewAppendLogger(fileName string, maxLevel int) logger.FullLogger { return newLogger(fileName, maxLevel, true) } func NewLogger(fileName string, maxLevel int) logger.FullLogger { return newLogger(fileName, maxLevel, false) } func newLogger(filePath string, maxLevel int, appendMode bool) logger.FullLogger { return (&logImpl{}).initLogger(filePath, maxLevel, appendMode) } func (log *logImpl) initLogger(filePath string, maxLevel int, appendMode bool) *logImpl { log.filePath = filePath log.maxSize = LOG_NO_SIZE_LIMIT log.size = 0 log.stream = nil log.maxLevel = maxLevel log.appendMode = appendMode log.maxMessageLength = defaultMaxMessageLength log.stackOffset = baseStackOffset log.dumperEnabled = false log.customProperties = make(map[int]any) // log.TTY.Init() return log } func (log *logImpl) SetProperty(propertyId int, value any) (success bool) { var intValue int var int64Value int64 var boolValue bool switch propertyId { case FULL_LOGGER_STACK_OFFSET: if intValue, success = value.(int); success { if intValue < baseStackOffset { intValue = baseStackOffset success = false } log.stackOffset = intValue } case logger.LOGGER_MAX_MESSAGE_LENGTH: if intValue, success = value.(int); success { log.maxMessageLength = intValue } case FULL_LOGGER_ROTATION_SIZE: if int64Value, success = value.(int64); success { log.maxSize = int64Value } case FULL_LOGGER_RETENTION_NUMBER: if intValue, success = value.(int); success { log.rotateNumber = intValue } case FULL_LOGGER_DUMPER_ENABLED: if boolValue, success = value.(bool); success { log.dumperEnabled = boolValue } default: log.customProperties[propertyId] = value } return } func (log *logImpl) GetProperty(propertyId int) (value any) { switch propertyId { case FULL_LOGGER_STACK_OFFSET: value = log.stackOffset case logger.LOGGER_MAX_MESSAGE_LENGTH: value = log.maxMessageLength case FULL_LOGGER_ROTATION_SIZE: value = log.maxSize case FULL_LOGGER_RETENTION_NUMBER: value = log.rotateNumber default: if v, ok := log.customProperties[propertyId]; ok { value = v } } return } func (log *logImpl) GetPropertyBool(propertyId int) (value bool, ok bool) { if rawValue := log.GetProperty(propertyId); rawValue != nil { value, ok = rawValue.(bool) } return } func (log *logImpl) Finalize() { if log.stream != nil { log.stream.Sync() log.stream = nil } } func Label2Level(label string) (level int, found bool) { label = strings.ToUpper(label) for i, lab := range logClassLabel { if lab == label { level = i found = true break } } return } func LogClassLabel(level int) (label string) { if level >= 0 && level < len(logClassLabel) { label = logClassLabel[level] } else { label = "unknown" } return } func (log *logImpl) SetRotation(maxSize int64, rotateNumber int) { log.maxSize = maxSize log.rotateNumber = rotateNumber } func (log *logImpl) Logf(logClass int, upStack int, templ string, args ...any) (text string) { if logClass >= oobOffset { logClass = logClass - oobOffset } else if logClass > log.maxLevel { return } stream := log.getLog() if stream != nil { source, line, _ := golang.Trace(upStack + log.stackOffset) now := time.Now() // stamp := now.Format("2006-01-02 15:04:05.999") stamp := now.Format("2006-01-02 15:04:05") // label := logimpl.LOG_CLASS_LABEL[logClass] label := LogClassLabel(logClass) header := fmt.Sprintf("%v [%-5s] %s::%d -- ", stamp, label, path.Base(source), line) // templ = header + templ msg := fmt.Sprintf(templ, args...) text = msg msgLen := len(msg) + len(header) + 1 if msgLen > log.maxMessageLength { msg = msg[0 : log.maxMessageLength-len(header)-1] msgLen = len(msg) } // Rotazione log.rotateLogFiles(msgLen) n, _ := log.stream.WriteString(header) n, _ = log.stream.WriteString(msg) log.stream.Write([]byte{'\n'}) log.size += int64(n) } return } func (log *logImpl) Rawb(logClass int, msg []byte) { if logClass >= oobOffset { logClass = logClass - oobOffset } else if logClass > log.maxLevel { return } stream := log.getLog() if stream != nil { msgLen := len(msg) // Rotazione log.rotateLogFiles(msgLen) n, _ := log.stream.Write(msg) log.size += int64(n) } } func (log *logImpl) Rawf(logClass int, templ string, args ...any) { if logClass >= oobOffset { logClass = logClass - oobOffset } else if logClass > log.maxLevel { return } stream := log.getLog() if stream != nil { msg := fmt.Sprintf(templ, args...) msgLen := len(msg) // Rotazione log.rotateLogFiles(msgLen) n, _ := log.stream.Write([]byte(msg)) log.size += int64(n) } } func (log *logImpl) Dumpf(data []byte, perm fs.FileMode, templ string, args ...any) (dumpFilePath string) { if log.dumperEnabled { dirPath := path.Dir(log.filePath) dumpFilePath = path.Join(dirPath, fmt.Sprintf(templ, args...)) os.WriteFile(dumpFilePath, data, perm) } return } func (log *logImpl) OobLogf(templ string, args ...any) { log.Logf(oobOffset, 1, templ, args...) } func (log *logImpl) IsDebugEnabled() bool { return log.maxLevel >= LOG_DEBUG } func (log *logImpl) IsInfoEnabled() bool { return log.maxLevel >= LOG_INFO } func (log *logImpl) Debugf(templ string, args ...any) { // Se la variabile d'ambiente _DEBUG è impostata // i messaggi di debug sono mostrati anche su stderr if log.debugOnStderr { // var extTempl string source, line, _ := golang.Trace(3) header := fmt.Sprintf("[%5s] %s::%d -- ", logClassLabel[LOG_DEBUG], path.Base(source), line) // fd := uint(os.Stderr.Fd()) // if log.TTY.IsTTY(fd) { // c := log.TTY.FgColor(fd, utils.CYAN) // extTempl = c + header + templ + log.TTY.Reset(fd) + "\n" // } else { // extTempl = header + templ + "\n" // } extTempl := header + templ + "\n" fmt.Fprintf(os.Stderr, extTempl, args...) } log.Logf(LOG_DEBUG, 1, templ, args...) } func (log *logImpl) DebugRawb(msg []byte) { // Se la variabile d'ambiente _DEBUG è impostata // i messaggi di debug sono mostrati anche su stderr if log.debugOnStderr { os.Stderr.Write(msg) } log.Rawb(LOG_DEBUG, msg) } func (log *logImpl) DebugRawf(templ string, args ...any) { // Se la variabile d'ambiente _DEBUG è impostata // i messaggi di debug sono mostrati anche su stderr if log.debugOnStderr { fmt.Fprintf(os.Stderr, templ, args...) } log.Rawf(LOG_DEBUG, templ, args...) } func (log *logImpl) Infof(templ string, args ...any) { log.Logf(LOG_INFO, 1, templ, args...) } func (log *logImpl) InfoRawf(templ string, args ...any) { log.Rawf(LOG_INFO, templ, args...) } func (log *logImpl) Warnf(templ string, args ...any) { log.Logf(LOG_WARN, 1, templ, args...) } func (log *logImpl) WarnRawf(templ string, args ...any) { log.Rawf(LOG_WARN, templ, args...) } func (log *logImpl) Errorf(templ string, args ...any) { log.Logf(LOG_ERROR, 1, templ, args...) } func (log *logImpl) ErrorRawf(templ string, args ...any) { log.Rawf(LOG_ERROR, templ, args...) } func (log *logImpl) getLog() *os.File { if log.maxLevel == LOG_OOB { return nil } if log.stream == nil && len(log.filePath) > 0 { var err error log.flags = os.O_CREATE | os.O_RDWR if log.appendMode { log.flags |= os.O_APPEND } else { log.flags |= os.O_TRUNC } if _, err = utils.MakeParentDir(log.filePath); err != nil { return nil } log.stream, err = os.OpenFile(log.filePath, log.flags, 0644) if err == nil { log.size = utils.FileSize(log.filePath) // self.Logf(1000+LOG_INFO, 0, VERSION) } else { utils.ExitErrorf(1, "Can't open log file %#v: %v", log.filePath, err) } log.reduceLogStock() } return log.stream } func (log *logImpl) rotateLogFiles(msgLen int) { if log.maxSize != LOG_NO_SIZE_LIMIT && (log.size+int64(msgLen)) >= log.maxSize { var err error // Rotazione now := time.Now() stamp := now.Format("2006-01-02_15-04-05") rotFile := log.filePath + "." + stamp _, err = utils.MoveFile(log.filePath, rotFile) if err != nil { utils.ExitErrorf(1, "Unable to rotate current log file: %v", err) } log.stream, err = os.OpenFile(log.filePath, log.flags, 0644) if err != nil { utils.ExitErrorf(1, "Can't open log file %#v after rotation: %v", log.filePath, err) } log.size = 0 // Sfoltimento log.reduceLogStock() } } func (log *logImpl) reduceLogStock() { if log.rotateNumber > 0 { dirPath := path.Dir(log.filePath) logName := path.Base(log.filePath) entries, err := os.ReadDir(dirPath) if err != nil { utils.ExitErrorf(1, "Can't read program's log directory %#v: %v", dirPath, err) } logFiles := make([]string, 0) rotatedNameSize := len(logName) + 20 for _, e := range entries { if !e.IsDir() { name := e.Name() if len(name) == rotatedNameSize && strings.HasPrefix(name, logName) { logFiles = append(logFiles, name) } } } slices.Sort(logFiles) remainingRotatedCount := len(logFiles) if remainingRotatedCount > log.rotateNumber { for i := 0; i < len(logFiles) && remainingRotatedCount > log.rotateNumber; i++ { filePath := path.Join(dirPath, logFiles[i]) os.Remove(filePath) remainingRotatedCount-- } } } }