Skip to content

Commit

Permalink
logger docs
Browse files Browse the repository at this point in the history
  • Loading branch information
iesreza committed Dec 17, 2024
1 parent 97ab23d commit 1b51ca2
Show file tree
Hide file tree
Showing 4 changed files with 262 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
121 changes: 121 additions & 0 deletions docs/file_logger.md
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
└── ...
```
1 change: 1 addition & 0 deletions docs/log.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
139 changes: 139 additions & 0 deletions lib/log/file/file.go
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)
}

0 comments on commit 1b51ca2

Please sign in to comment.