Frontend Labs
⋯

Day 18 — File I/O & os Package

Learning Objectives

  • Write a rotating log file — compliance requirement for audit trails
  • Use os.Stat for 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.


Generated from week3.md · 8 Oct 2026 16:36