pkg/logging/logging.go (166 lines of code) (raw):

// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. package logging import ( "fmt" "io" "io/fs" "io/ioutil" "log" "os" "path" "path/filepath" "runtime/debug" "sort" "strconv" "strings" "time" "github.com/Azure/azure-extension-platform/pkg/handlerenv" ) const ( logLevelError = "Error " logLevelWarning = "Warning " logLevelInfo = "Info " ) const ( thirtyMB = 30 * 1024 * 1034 // 31,457,280 bytes fortyMB = 40 * 1024 * 1024 // 41,943,040 bytes logDirThresholdLow = thirtyMB logDirThresholdHigh = fortyMB ) type StreamLogReader interface { ErrorFromStream(prefix string, streamReader io.Reader) WarnFromStream(prefix string, streamReader io.Reader) InfoFromStream(prefix string, streamReader io.Reader) } // Target interface for Extentsion-Platform type ILogger interface { StreamLogReader Error(format string, v ...interface{}) Warn(format string, v ...interface{}) Info(format string, v ...interface{}) Close() } // ExtensionLogger exposes logging capabilities to the extension // It automatically appends time stamps and debug level to each message // and ensures all logs are placed in the logs folder passed by the agent type ExtensionLogger struct { errorLogger *log.Logger infoLogger *log.Logger warnLogger *log.Logger file *os.File } // New creates a new logging instance. If the handlerEnvironment is nil, we'll use a // standard output logger func New(he *handlerenv.HandlerEnvironment) *ExtensionLogger { return NewWithName(he, "") } // Allows the caller to specify their own name for the file // Supports cycling of logs to prevent filling up the disk func NewWithName(he *handlerenv.HandlerEnvironment, logFileFormat string) *ExtensionLogger { if he == nil { return newStandardOutput() } if logFileFormat == "" { logFileFormat = "log_%v" } // Rotate log folder to prevent filling up the disk err := rotateLogFolder(he.LogFolder, logFileFormat) if err != nil { return newStandardOutput() } fileName := fmt.Sprintf(logFileFormat, strconv.FormatInt(time.Now().UTC().Unix(), 10)) filePath := path.Join(he.LogFolder, fileName) writer, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666) if err != nil { return newStandardOutput() } return &ExtensionLogger{ errorLogger: log.New(writer, logLevelError, log.Ldate|log.Ltime|log.LUTC), infoLogger: log.New(writer, logLevelInfo, log.Ldate|log.Ltime|log.LUTC), warnLogger: log.New(writer, logLevelWarning, log.Ldate|log.Ltime|log.LUTC), file: writer, } } func GetCallStack() string { return string(debug.Stack()) } func newStandardOutput() *ExtensionLogger { return &ExtensionLogger{ errorLogger: log.New(os.Stdout, logLevelError, 0), infoLogger: log.New(os.Stdout, logLevelInfo, 0), warnLogger: log.New(os.Stdout, logLevelWarning, 0), file: nil, } } // Close closes the file func (logger *ExtensionLogger) Close() { if logger.file != nil { logger.file.Close() } } // Error logs an error. Format is the same as fmt.Print func (logger *ExtensionLogger) Error(format string, v ...interface{}) { logger.errorLogger.Printf(format+"\n", v...) logger.errorLogger.Printf(GetCallStack() + "\n") } // Warn logs a warning. Format is the same as fmt.Print func (logger *ExtensionLogger) Warn(format string, v ...interface{}) { logger.warnLogger.Printf(format+"\n", v...) } // Info logs an information statement. Format is the same as fmt.Print func (logger *ExtensionLogger) Info(format string, v ...interface{}) { logger.infoLogger.Printf(format+"\n", v...) } // Error logs an error. Get the message from a stream directly func (logger *ExtensionLogger) ErrorFromStream(prefix string, streamReader io.Reader) { logger.errorLogger.Print(prefix) io.Copy(logger.errorLogger.Writer(), streamReader) logger.errorLogger.Writer().Write([]byte(fmt.Sprintln())) // add a newline at the end of the stream contents } // Warn logs a warning. Get the message from a stream directly func (logger *ExtensionLogger) WarnFromStream(prefix string, streamReader io.Reader) { logger.warnLogger.Print(prefix) io.Copy(logger.warnLogger.Writer(), streamReader) logger.warnLogger.Writer().Write([]byte(fmt.Sprintln())) // add a newline at the end of the stream contents } // Info logs an information statement. Get the message from a stream directly func (logger *ExtensionLogger) InfoFromStream(prefix string, streamReader io.Reader) { logger.infoLogger.Print(prefix) io.Copy(logger.infoLogger.Writer(), streamReader) logger.infoLogger.Writer().Write([]byte(fmt.Sprintln())) // add a newline at the end of the stream contents } // Function to get directory size func getDirSize(dirPath string) (size int64, err error) { err = filepath.Walk(dirPath, func(_ string, info os.FileInfo, err error) error { if err != nil { return err } if !info.IsDir() { size += info.Size() } return err }) if err != nil { err = fmt.Errorf("unable to compute directory size, error: %v", err) } return } // Function to rotate log files present in logFolder to avoid filling customer disk space // File name matching is done on file name pattern provided before '%' func rotateLogFolder(logFolder string, logFileFormat string) (err error) { size, err := getDirSize(logFolder) if err != nil { return } // If directory size is still under high threshold value, nothing to do if size < logDirThresholdHigh { return } // Get all log files in logFolder // Files are already sorted according to filenames // Log file names contains unix timestamp as suffix, Thus we have files sorted according to age as well var dirEntries []fs.FileInfo dirEntries, err = ioutil.ReadDir(logFolder) if err != nil { err = fmt.Errorf("unable to read log folder, error: %v", err) return } // Sort directory entries according to time (oldest to newest) sort.Slice(dirEntries, func(idx1, idx2 int) bool { return dirEntries[idx1].ModTime().Before(dirEntries[idx2].ModTime()) }) // Get log file name prefix logFilePrefix := strings.Split(logFileFormat, "%") for _, file := range dirEntries { // Once directory size goes below lower threshold limit, stop deletion if size < logDirThresholdLow { break } // Skip directories if file.IsDir() { continue } // log file names are prefixed according to logFileFormat specified if !strings.HasPrefix(file.Name(), logFilePrefix[0]) { continue } // Delete the file err = os.Remove(filepath.Join(logFolder, file.Name())) if err != nil { err = fmt.Errorf("unable to delete log files, error: %v", err) return } // Subtract file size from total directory size size = size - file.Size() } return }