rotate log

เรื่องมันเริ่มจากน้องใบไผ่ มาถามว่าผมทำ rotate log ยังไง ก็เลยพบความจริงว่า สมัยนั้นเราไม่ได้คิดเรื่องนี้กันจริงจัง เราเลยไม่ได้ทำ lol
ซึ่งเมื่อวันที่โปรเจ็คมัน go live ไปแล้ว เราก็เลยได้เห็นกระบวนท่า shell script มาทำสิ่งนี้ให้แทน

ถึงจุดนี้น้องก็เลยบอกว่าผมจะไปใช้ https://github.com/natefinch/lumberjack แล้วนะ ผมก็เลยลองไปค้นดูเจอใน stackoverflow กล่าวถึงเรื่องนี้อยู่เป็น abstract พอประมาณ ที่นี่

ก็เลยอยากจะทดลองด้วยตัวเองว่ามันทำจริงได้อย่างไร แล้วมันใช้ได้จริงหรือเปล่า ซึ่งพอเขียนออกมาแล้วมันค่อนข้างยาวเลยทีเดียว โดยผมเอาต้นแบบจาก stackoverflow มาเขียนต่อกลายเป็นแบบนี้

package rotate

import (
	"fmt"
	"log"
	"os"
	"time"
)

type logger struct {
	*log.Logger
}

func (w logger) Log(l *Log) error {
	w.Println(l.msg)
	return nil
}

type Log struct {
	msg string
}

// This will be your goroutine
func Logging(logch chan *Log) {

	// assume logger creates/opens a file and prepares it for writing
	f, err := os.OpenFile("trace.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		log.Fatal(err)
	}
	defer f.Close()

	l := &logger{
		log.New(f, "rotate", log.Lshortfile),
	}

	for {

		// collect a log. Will also block until a log is available on the channel
		logs := <-logch

		if timeToRotate() {

			RotateLogFile(l)

		}

		// write log
		l.Log(logs)
	}
}

func timeToRotate() bool {
	info, err := os.Stat("trace.log")
	if err != nil {
		log.Fatal("timeToRotate", err)
	}

	if info.Size() > 1024 {
		return true
	}
	return false
}

func RotateLogFile(l *logger) error {
	name := "trace.log"
	newname := backupName("trace.log")
	if err := os.Rename(name, newname); err != nil {
		return fmt.Errorf("can't rename log file: %s", err)
	}

	f, err := os.OpenFile("trace.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		log.Fatal(err)
	}

	l.Logger = log.New(f, "rotate", log.Lshortfile)

	return nil
}

func backupName(name string) string {
	t := time.Now()

	timestamp := t.Format(time.RFC3339)
	return fmt.Sprintf("%s-%s.log", name, timestamp)
}

โดยผมตั้งเงื่อนไขในการ rotate ไว้ง่ายๆว่า ถ้าไฟล์มีขนาดเกิน 1k ก็ให้ rotate เลย แล้วก็เขียนเทสแบบลวกๆใส่มันแบบนี้เลยครับ

package rotate

import (
	"testing"
	"time"
)

func TestRotate(t *testing.T) {
	lch := make(chan *Log)
	go Logging(lch)

	go func() {
		for i := 0; i < 100; i++ {
			l := &Log{msg: "openExistingOrNew opens the logfile if it exists and if the current write"}
			lch <- l
		}
	}()
	go func() {
		for i := 0; i < 100; i++ {
			l := &Log{msg: "openExistingOrNew opens the logfile if it exists and if the current write"}
			lch <- l
		}
	}()
	go func() {
		for i := 0; i < 100; i++ {
			l := &Log{msg: "openExistingOrNew opens the logfile if it exists and if the current write"}
			lch <- l
		}
	}()
	go func() {
		for i := 0; i < 100; i++ {
			l := &Log{msg: "openExistingOrNew opens the logfile if it exists and if the current write"}
			lch <- l
		}
	}()

	<-time.After(1 * time.Second)
}

พอรันเทส มันก็จะพ่นไฟล์ log ออกมา แล้วเราก็ไปดูครับ