From 1b51ca2bc92c0cf8daa1ad60d3d22065f629bf17 Mon Sep 17 00:00:00 2001 From: Reza Date: Tue, 17 Dec 2024 05:04:52 +0100 Subject: [PATCH] logger docs --- README.MD | 1 + docs/file_logger.md | 121 +++++++++++++++++++++++++++++++++++++ docs/log.md | 1 + lib/log/file/file.go | 139 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 262 insertions(+) create mode 100644 docs/file_logger.md create mode 100644 lib/log/file/file.go diff --git a/README.MD b/README.MD index 6ff6f61..0e9cafb 100644 --- a/README.MD +++ b/README.MD @@ -28,6 +28,7 @@ With EVO Framework, you can focus on your programming logic and rapidly develop - [Build](docs/build.md) - [Args](docs/args.md) - [Logging](docs/log.md) +- - [File Logger](docs/file_logger.md) - [Concurrency Patterns](storage_interface.md) - [STract configuration language](storage_interface.md) - [Local Files](storage_interface.md) diff --git a/docs/file_logger.md b/docs/file_logger.md new file mode 100644 index 0000000..d2d503c --- /dev/null +++ b/docs/file_logger.md @@ -0,0 +1,121 @@ +# File Logger + +This library extends your logging capabilities by adding a file-based logger to your existing log package. It supports features like log rotation at midnight, configurable file paths, customizable log formatting, and automatic cleanup of old logs based on expiration settings. + +## Features +- **Automatic Log Rotation**: Creates a new log file every midnight. +- **Customizable Log Format**: Allows custom formatting of log entries. +- **Optional Expiration**: Automatically deletes old log files after a defined number of days. +- **Thread-Safe**: Ensures safe concurrent writes to the log file. +- **Default Configuration**: Works out-of-the-box without requiring any configuration. + +--- + +## Installation +Import the logger package into your project: + +```go +import "your_project/file" // Replace with the correct import path +import "github.com/getevo/evo/v2/lib/log" +``` + +--- + +## Usage +### Adding the File Logger to Log Writers +You can integrate the file logger into your log package's writers using the `NewFileLogger` function. Pass an optional `Config` struct to customize its behavior. + +Here is an example: + +```go +package main + +import ( + "github.com/getevo/evo/v2/lib/log" + "github.com/getevo/evo/v2/lib/log/file" // Replace with the correct import path +) + +func main() { + // Add the file logger with custom configuration + log.AddWriter(file.NewFileLogger(file.Config{ + Path: "./logs", // Directory to store logs + FileName: "app_%y-%m-%d.log", // Filename template with wildcards + Expiration: 7, // Keep logs for 7 days + LogFormat: nil, // Use default log format + })) + + // Example logs + log.Info("Application started") + log.Warning("This is a warning message") + log.Error("An error occurred") +} +``` + +--- + +## Configuration +The `Config` struct allows you to customize the behavior of the logger. Here's a breakdown of the available fields: + +| Field | Type | Default | Description | +|-------------|-------------------------------------|----------------------------------|-----------------------------------------------------------------------------| +| `Path` | `string` | Current working directory | Directory where the log files will be saved. | +| `FileName` | `string` | `executable_name.log` | Filename template. Supports `%y`, `%m`, `%d` for year, month, and day. | +| `Expiration`| `int` | `0` | Number of days to keep log files. `<= 0` means no cleanup of old logs. | +| `LogFormat` | `func(entry *log.Entry) string` | Default format | Custom function to format log entries. Defaults to a standard log format. | + +### Filename Wildcards +The `FileName` field supports the following wildcards: +- `%y` → Year (e.g., `2024`) +- `%m` → Month (e.g., `04`) +- `%d` → Day (e.g., `25`) + +Example: `app_%y-%m-%d.log` → `app_2024-04-25.log` + +--- + +## Default Behavior +If no configuration is provided, the logger uses the following defaults: +1. **Path**: The current working directory. +2. **FileName**: The executable name with a `.log` extension. +3. **Expiration**: No automatic cleanup of old logs. +4. **LogFormat**: A standard log format with the following structure: + ``` + YYYY-MM-DD HH:MM:SS [LEVEL] file:line message + ``` + +**Example**: +``` +2024-04-25 14:30:01 [INFO] main.go:45 Application started +``` + +--- + +## Log Rotation +- At midnight, the logger automatically rotates the log file based on the current date. +- A new log file is created using the `FileName` template. + +--- + +## Expiration +If the `Expiration` field is set (e.g., 7 days), log files older than the specified number of days are automatically deleted during log rotation. + +**Example**: +- Set `Expiration: 7` → Log files older than 7 days will be removed. + +--- + +## Thread Safety +All writes to the log file are protected using a `sync.Mutex`, ensuring thread-safe operations in concurrent environments. + +--- + +## Example File Structure +With the configuration `Path: "./logs"` and `FileName: "app_%y-%m-%d.log"`, the log files will be structured as follows: + +``` +./logs/ + ├── app_2024-04-25.log + ├── app_2024-04-26.log + ├── app_2024-04-27.log + └── ... +``` \ No newline at end of file diff --git a/docs/log.md b/docs/log.md index 7892fe3..4f66fb7 100644 --- a/docs/log.md +++ b/docs/log.md @@ -115,6 +115,7 @@ This version ensures: 1. The log file is opened only once per application lifecycle. 2. Writes to the file are synchronized, making it thread-safe for concurrent logging. +- Official EVO [File Logger](docs/file_logger.md) documentation --- ## 4. How to Define a New StdLog Function and Set It as Default Logger diff --git a/lib/log/file/file.go b/lib/log/file/file.go new file mode 100644 index 0000000..e85ec21 --- /dev/null +++ b/lib/log/file/file.go @@ -0,0 +1,139 @@ +package file + +import ( + "fmt" + "github.com/getevo/evo/v2/lib/log" + "os" + "path/filepath" + "strings" + "sync" + "time" +) + +type Config struct { + Path string // Directory for log files + FileName string // Filename template with wildcards (e.g., log_%y-%m-%d.log) + Expiration int // Expiration in days, if <=0 no cleanup + LogFormat func(entry *log.Entry) string // Function to format log entry +} + +// fileLogger is the internal structure for the file logger +type fileLogger struct { + config Config + file *os.File + filePath string + mutex sync.Mutex + expiryMutex sync.Mutex + currentDate string +} + +// NewFileLogger creates a file logger compatible with the log package +func NewFileLogger(config ...Config) func(log *log.Entry) { + c := Config{} + if len(config) > 0 { + c = config[0] + } + + if c.Path == "" { + c.Path, _ = os.Getwd() + } + if c.FileName == "" { + execName := filepath.Base(os.Args[0]) + c.FileName = fmt.Sprintf("%s.log", execName) + } + if c.LogFormat == nil { + c.LogFormat = defaultLogFormat + } + + logger := &fileLogger{ + config: c, + currentDate: time.Now().Format("2006-01-02"), + } + + logger.openLogFile() + go logger.startLogRotation() + + return func(log *log.Entry) { + logger.writeLog(log) + } +} + +// openLogFile opens or creates the log file with append mode +func (f *fileLogger) openLogFile() { + f.mutex.Lock() + defer f.mutex.Unlock() + + f.filePath = f.getFilePath() + file, err := os.OpenFile(f.filePath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) + if err != nil { + log.Fatalf("failed to open log file: %v", err) + } + + if f.file != nil { + _ = f.file.Close() + } + f.file = file +} + +// getFilePath generates the log file path with the current date +func (f *fileLogger) getFilePath() string { + template := f.config.FileName + now := time.Now() + fileName := strings.ReplaceAll(template, "%y", now.Format("2006")) + fileName = strings.ReplaceAll(fileName, "%m", now.Format("01")) + fileName = strings.ReplaceAll(fileName, "%d", now.Format("02")) + return filepath.Join(f.config.Path, fileName) +} + +// writeLog safely writes the log entry to the file +func (f *fileLogger) writeLog(entry *log.Entry) { + f.mutex.Lock() + defer f.mutex.Unlock() + + logString := f.config.LogFormat(entry) + _, err := f.file.WriteString(logString + "\r\n") + if err != nil { + log.Error("failed to write to log file: %v", err) + } +} + +// startLogRotation rotates the log at midnight +func (f *fileLogger) startLogRotation() { + for { + nextMidnight := time.Now().Truncate(24 * time.Hour).Add(24 * time.Hour) + time.Sleep(time.Until(nextMidnight)) + f.rotateLog() + } +} + +// rotateLog closes the current log file and opens a new one +func (f *fileLogger) rotateLog() { + f.mutex.Lock() + f.currentDate = time.Now().Format("2006-01-02") + f.openLogFile() + f.mutex.Unlock() + f.cleanupOldLogs() +} + +// cleanupOldLogs removes expired log files if expiration is set +func (f *fileLogger) cleanupOldLogs() { + if f.config.Expiration <= 0 { + return + } + + expirationDate := time.Now().AddDate(0, 0, -f.config.Expiration) + files, _ := filepath.Glob(filepath.Join(f.config.Path, "*.log")) + for _, file := range files { + if file == f.filePath { + continue + } + if stat, err := os.Stat(file); err == nil && stat.ModTime().Before(expirationDate) { + _ = os.Remove(file) + } + } +} + +// defaultLogFormat is the default formatter for log entries +func defaultLogFormat(e *log.Entry) string { + return fmt.Sprintf("%s [%s] %s:%d %s", e.Date.Format("2006-01-02 15:04:05"), e.Level, e.File, e.Line, e.Message) +}