-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
262 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
└── ... | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} |