Day 18 — File I/O & os Package
Learning Objectives
- Write a rotating log file — compliance requirement for audit trails
- Use
os.Statfor file watching — the mechanism for hot-reloading WAF rules - Handle file permissions correctly — prevent world-readable log files
Key Concepts
// Reading files
data, err := os.ReadFile("config.json") // small files: read all at once
f, err := os.Open("large.log") // large files: stream with bufio
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := scanner.Text()
}
// Writing files
err := os.WriteFile("output.json", data, 0644) // simple write
f, err := os.OpenFile("audit.log", // append mode
os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
// File watching via polling
for {
info, err := os.Stat(configPath)
if err == nil && info.ModTime().After(lastModTime) {
reload(configPath)
lastModTime = info.ModTime()
}
time.Sleep(5 * time.Second)
}
Exercise: Rotating Audit Log Writer
package main
import (
"fmt"
"os"
"path/filepath"
"sync"
"time"
)
// RotatingLog writes to date-stamped files, rotates at midnight, keeps N days
type RotatingLog struct {
mu sync.Mutex
dir string
keepDays int
current *os.File
currentDay string
}
func NewRotatingLog(dir string, keepDays int) (*RotatingLog, error) {
if err := os.MkdirAll(dir, 0750); err != nil {
return nil, err
}
rl := &RotatingLog{dir: dir, keepDays: keepDays}
if err := rl.rotate(); err != nil {
return nil, err
}
return rl, nil
}
func (rl *RotatingLog) rotate() error {
today := time.Now().Format("2006-01-02")
if rl.currentDay == today {
return nil
}
if rl.current != nil {
rl.current.Close()
}
path := filepath.Join(rl.dir, fmt.Sprintf("audit-%s.log", today))
f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
if err != nil {
return err
}
rl.current = f
rl.currentDay = today
go rl.cleanup()
return nil
}
func (rl *RotatingLog) cleanup() {
cutoff := time.Now().AddDate(0, 0, -rl.keepDays)
entries, _ := os.ReadDir(rl.dir)
for _, e := range entries {
if e.IsDir() { continue }
info, err := e.Info()
if err != nil { continue }
if info.ModTime().Before(cutoff) {
os.Remove(filepath.Join(rl.dir, e.Name()))
}
}
}
func (rl *RotatingLog) Write(entry string) error {
rl.mu.Lock()
defer rl.mu.Unlock()
if err := rl.rotate(); err != nil {
return err
}
_, err := fmt.Fprintf(rl.current, "%s %s\n", time.Now().Format(time.RFC3339), entry)
return err
}
func (rl *RotatingLog) Close() error {
rl.mu.Lock()
defer rl.mu.Unlock()
if rl.current != nil {
return rl.current.Close()
}
return nil
}
func main() {
log, err := NewRotatingLog("./logs", 7)
if err != nil {
fmt.Println("ERROR:", err)
return
}
defer log.Close()
for i := 0; i < 5; i++ {
log.Write(fmt.Sprintf(`{"event":"request","ip":"10.0.0.%d","path":"/api/data","status":200}`, i))
}
fmt.Println("Wrote 5 audit log entries")
}
Real-World Context
GDPR Article 30 requires audit logs of data access. PCI DSS requires logs retained for 1 year, accessible for 3 months. The 0600 permission (owner read/write only) prevents other users from reading audit logs. File permissions are often an audit finding.