diff --git a/console-log.go b/console-log.go new file mode 100644 index 0000000..93ff6d9 --- /dev/null +++ b/console-log.go @@ -0,0 +1,149 @@ +// console-log.go +package logimpl + +import ( + "fmt" + "os" + + "portale-stac.it/packages/logger" + "portale-stac.it/packages/utils" +) + +// Implementation specific properties +const ( + CONSOLE_LOG_VERBOSE_ENABLE = iota + FULL_LOGGER_DERIVED_BASE_ID + CONSOLE_LOG_TTY // read-only + CONSOLE_LOG_DERIVED_BASE_ID +) + +type consoleLog struct { + logImpl + TTY utils.TTYContext + verboseEnabled bool +} + +func NewConsoleLog(filePath string, maxLevel int) logger.VerboseLogger { + log := &consoleLog{ + logImpl: logImpl{}, + verboseEnabled: true, + } + log.logImpl.initLogger(filePath, maxLevel, true) + log.TTY.Init() + return log +} + +func (log *consoleLog) SetProperty(propertyId int, value any) (success bool) { + var boolValue bool + + switch propertyId { + case CONSOLE_LOG_VERBOSE_ENABLE: + if boolValue, success = value.(bool); success { + log.verboseEnabled = boolValue + } + default: + success = log.logImpl.SetProperty(propertyId, value) + } + return +} + +func (log *consoleLog) GetProperty(propertyId int) (value any) { + switch propertyId { + case CONSOLE_LOG_VERBOSE_ENABLE: + value = log.verboseEnabled + case CONSOLE_LOG_TTY: + value = &log.TTY + default: + value = log.logImpl.GetProperty(propertyId) + } + return +} + +func (log *consoleLog) Msgf(color uint, templ string, args ...any) { + if log.TTY.IsTTY(utils.STDERR) { + var extTempl string + if color != utils.NONE { + c := log.TTY.FgColor(utils.STDERR, color) + extTempl = c + templ + log.TTY.Reset(utils.STDERR) + } + if len(extTempl) > 0 { + fmt.Fprintf(os.Stderr, extTempl+"\n", args...) + } else { + fmt.Fprintf(os.Stderr, templ+"\n", args...) + // os.Stderr.Sync() + } + } +} + +func (log *consoleLog) MsgItalicf(color uint, templ string, args ...any) { + var extTempl string + if log.TTY.IsTTY(utils.STDERR) { + if color != utils.NONE { + c := log.TTY.FgColor(utils.STDERR, color) + extTempl = c + log.TTY.Italic(utils.STDERR) + templ + log.TTY.Reset(utils.STDERR) + } else { + extTempl = log.TTY.Italic(utils.STDERR) + templ + log.TTY.Reset(utils.STDERR) + } + } + if len(extTempl) > 0 { + fmt.Fprintf(os.Stderr, extTempl+"\n", args...) + } else { + fmt.Fprintf(os.Stderr, templ+"\n", args...) + // os.Stderr.Sync() + } +} + +func (log *consoleLog) Alertf(prefix string, upstack int, templ string, args ...any) (text string) { + var extTempl string + if log.TTY.IsTTY(utils.STDERR) { + var color string = log.TTY.RedFg(utils.STDERR) + + if prefix == "" { + extTempl = color + templ + log.TTY.Reset(utils.STDERR) + } else { + extTempl = color + log.TTY.Bold(utils.STDERR) + prefix + log.TTY.BoldOff(utils.STDERR) + templ + log.TTY.Reset(utils.STDERR) + } + } + + if len(extTempl) > 0 { + fmt.Fprintf(os.Stderr, extTempl+"\n", args...) + } else { + fmt.Fprintf(os.Stderr, templ+"\n", args...) + // os.Stderr.Sync() + } + text = log.logImpl.Logf(LOG_ERROR, 1+upstack, templ, args...) + return +} + +func (log *consoleLog) Verbosef(color uint, templ string, args ...any) string { + if log.verboseEnabled { + log.Msgf(color, templ, args...) + } + return log.Logf(LOG_INFO, 2, templ, args...) +} + +func (log *consoleLog) VerboseItalicf(color uint, templ string, args ...any) string { + if log.verboseEnabled { + log.MsgItalicf(color, templ, args...) + } + return log.Logf(LOG_INFO, 2, templ, args...) +} + +func (log *consoleLog) VerboseNoColorf(templ string, args ...any) string { + return log.Verbosef(utils.NONE, templ, args...) +} + +func (log *consoleLog) VerboseRedf(templ string, args ...any) string { + return log.Verbosef(utils.RED, templ, args...) +} + +func (log *consoleLog) VerboseGreenf(templ string, args ...any) string { + return log.Verbosef(utils.GREEN, templ, args...) +} + +func (log *consoleLog) VerboseBluef(templ string, args ...any) string { + return log.Verbosef(utils.BLUE, templ, args...) +} + +func (log *consoleLog) VerboseBrownf(templ string, args ...any) string { + return log.VerboseItalicf(utils.BROWN, templ, args...) +} diff --git a/contextual-log.go b/contextual-log.go new file mode 100644 index 0000000..5d66388 --- /dev/null +++ b/contextual-log.go @@ -0,0 +1,57 @@ +// contextual-log.go +package logimpl + +import ( + "fmt" + + "portale-stac.it/packages/logger" +) + +type ContextualLog struct { + log logger.FullLogger + tag string + baseStackOffset int +} + +func (clog ContextualLog) Init(log logger.FullLogger, template string, args ...any) { + clog.log = log + clog.tag = fmt.Sprintf(template, args...) + clog.baseStackOffset, _ = log.GetProperty(FULL_LOGGER_STACK_OFFSET).(int) +} + +func NewCtxLog(log logger.FullLogger, template string, args ...any) (clog *ContextualLog) { + offset, _ := log.GetProperty(FULL_LOGGER_STACK_OFFSET).(int) + return &ContextualLog{ + log: log, + tag: fmt.Sprintf(template, args...), + baseStackOffset: offset, + } +} + +func (clog *ContextualLog) Logger() logger.FullLogger { + return clog.log +} + +func (clog *ContextualLog) Debugf(templ string, args ...any) { + clog.log.SetProperty(FULL_LOGGER_STACK_OFFSET, clog.baseStackOffset+1) + clog.log.Debugf(clog.tag+templ, args...) + clog.log.SetProperty(FULL_LOGGER_STACK_OFFSET, clog.baseStackOffset) +} + +func (clog *ContextualLog) Infof(templ string, args ...any) { + clog.log.SetProperty(FULL_LOGGER_STACK_OFFSET, clog.baseStackOffset+1) + clog.log.Infof(clog.tag+templ, args...) + clog.log.SetProperty(FULL_LOGGER_STACK_OFFSET, clog.baseStackOffset) +} + +func (clog *ContextualLog) Warnf(templ string, args ...any) { + clog.log.SetProperty(FULL_LOGGER_STACK_OFFSET, clog.baseStackOffset+1) + clog.log.Warnf(clog.tag+templ, args...) + clog.log.SetProperty(FULL_LOGGER_STACK_OFFSET, clog.baseStackOffset) +} + +func (clog *ContextualLog) Errorf(templ string, args ...any) { + clog.log.SetProperty(FULL_LOGGER_STACK_OFFSET, clog.baseStackOffset+1) + clog.log.Errorf(clog.tag+templ, args...) + clog.log.SetProperty(FULL_LOGGER_STACK_OFFSET, clog.baseStackOffset) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ec1ea80 --- /dev/null +++ b/go.mod @@ -0,0 +1,17 @@ +module portale-stac.it/packages/logimpl + +go 1.21.6 + +require ( + portale-stac.it/packages/golang v0.0.0 + portale-stac.it/packages/logger v0.0.0 + portale-stac.it/packages/utils v0.0.0 +) + +require golang.org/x/text v0.3.7 // indirect + +replace ( + portale-stac.it/packages/golang => ../golang + portale-stac.it/packages/logger => ../logger + portale-stac.it/packages/utils => ../utils +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..1f78e03 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= diff --git a/logimpl.go b/logimpl.go new file mode 100644 index 0000000..289a6b5 --- /dev/null +++ b/logimpl.go @@ -0,0 +1,447 @@ +// logimpl.go +package logimpl + +import ( + "fmt" + "io/fs" + "os" + "path" + "slices" + "strings" + "time" + + "portale-stac.it/packages/golang" + "portale-stac.it/packages/logger" + "portale-stac.it/packages/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-- + } + } + } +} diff --git a/report.go b/report.go new file mode 100644 index 0000000..6f2f4a3 --- /dev/null +++ b/report.go @@ -0,0 +1,105 @@ +// report.go +package logimpl + +import ( + "fmt" + "slices" + "strings" + + "portale-stac.it/packages/logger" + "portale-stac.it/packages/utils" +) + +// Report params +const ( + DOT_PAD = "." + VALUE_TEMPL = ": %#v" + NAME_FIELD_SIZE = 30 + TOTAL_FIELD_SIZE = 34 +) + +type Report struct { + pad string + valueTempl string + nameFieldSize int + totalFieldSize int + log logger.VerboseLogger +} + +func NewReport(log logger.VerboseLogger) (report *Report) { + report = &Report{ + pad: DOT_PAD, + valueTempl: VALUE_TEMPL, + nameFieldSize: NAME_FIELD_SIZE, + totalFieldSize: TOTAL_FIELD_SIZE, + log: log, + } + return +} + +func (rep *Report) Log() logger.VerboseLogger { + return rep.log +} + +func (rep *Report) PutBrownWithPad(name string, value any, fieldSize int) { + name = strings.Title(name) + name = utils.PadStringRight(name, rep.pad, fieldSize, rep.valueTempl) + rep.log.VerboseBrownf(name, value) +} + +func (rep *Report) PutBlueWithPad(name string, value any, fieldSize int) { + name = strings.Title(name) + name = utils.PadStringRight(name, rep.pad, fieldSize, rep.valueTempl) + rep.log.VerboseBluef(name, value) +} + +func (rep *Report) PutParam(name string, value any) { + rep.PutBrownWithPad(name, value, rep.nameFieldSize) +} + +func (rep *Report) PutParamWithError(name string, err error, value any) { + if err != nil { + tty, _ := rep.log.GetProperty(CONSOLE_LOG_TTY).(*utils.TTYContext) + value = fmt.Sprintf("%v -- %s%v%s", value, tty.RedFg(utils.STDERR), err, tty.Reset(utils.STDERR)) + } + rep.PutBrownWithPad(name, value, rep.nameFieldSize) +} + +func (rep *Report) PutTotal(name string, value any) { + rep.PutBlueWithPad(name, value, rep.totalFieldSize) +} + +func (rep *Report) PutTotalCond(name string, good bool, value any) { + var styledValue string + tty, _ := rep.log.GetProperty(CONSOLE_LOG_TTY).(*utils.TTYContext) + name = utils.PadStringRight(name, DOT_PAD, TOTAL_FIELD_SIZE, rep.valueTempl) + if good { + styledValue = fmt.Sprintf("%s%v%s", tty.GreenFg(utils.STDERR), value, tty.Reset(utils.STDERR)) + } else { + styledValue = fmt.Sprintf("%s%v%s", tty.RedFg(utils.STDERR), value, tty.Reset(utils.STDERR)) + } + rep.log.VerboseBluef(name, styledValue) +} + +func (rep *Report) PutVars(vars map[string]string, sortKeys bool) { + if vars != nil { + if sortKeys { + keys := make([]string, len(vars)) + i := 0 + for key := range vars { + keys[i] = key + i++ + } + slices.Sort(keys) + for _, key := range keys { + name := utils.PadStringRight(key, rep.pad, rep.nameFieldSize, rep.valueTempl) + rep.log.VerboseBrownf(name, vars[key]) + } + } else { + for key, value := range vars { + name := utils.PadStringRight(key, rep.pad, rep.nameFieldSize, rep.valueTempl) + rep.log.VerboseBrownf(name, value) + } + } + } +}