pkg/log/log.go
2024-01-20 14:16:45 +01:00

246 lines
4.5 KiB
Go

package log
import (
"bufio"
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"sync"
"time"
"novit.tech/direktil/pkg/color"
)
const (
// AppendNL indicates that a forced '\n' is added.
AppendNL byte = 1
)
var (
logs = map[string]*Log{}
mutex = sync.Mutex{}
logOutputEnabled = false
)
// Log is a log target
type Log struct {
name string
l sync.Mutex
writeToFile bool
console io.Writer
pending []Entry
out *os.File
outTS string
}
func Get(name string) *Log {
mutex.Lock()
defer mutex.Unlock()
if log, ok := logs[name]; ok {
return log
}
log := &Log{
name: name,
pending: make([]Entry, 0),
}
if logOutputEnabled {
log.enableFileOutput()
}
logs[name] = log
return log
}
// EnableFiles flushes current logs to files, and enables output to files.
func EnableFiles() {
mutex.Lock()
defer mutex.Unlock()
if logOutputEnabled {
return
}
for _, log := range logs {
// we'll let the kernel optimize, just do it all parallel
go log.enableFileOutput()
}
logOutputEnabled = true
}
// DisableFiles flushes and closes current logs files, and disables output to files.
func DisableFiles() {
mutex.Lock()
defer mutex.Unlock()
if !logOutputEnabled {
return
}
for _, log := range logs {
// we'll let the kernel optimize, just do it all parallel
go log.disableFileOutput()
}
logOutputEnabled = false
}
func (l *Log) enableFileOutput() {
l.l.Lock()
defer l.l.Unlock()
for _, e := range l.pending {
if err := l.writeEntry(e); err != nil {
l.emergencyLog(e, err)
}
}
l.writeToFile = true
}
func (l *Log) disableFileOutput() {
l.l.Lock()
defer l.l.Unlock()
if l.out != nil {
l.out.Close()
}
l.writeToFile = false
}
func (l *Log) SetConsole(console io.Writer) {
l.console = console
}
// StreamLines will copy the input line by line as log entries.
func (l *Log) StreamLines(r io.Reader) {
in := bufio.NewReader(r)
for {
line, err := in.ReadBytes('\n')
if err != nil {
if err != io.EOF {
fmt.Fprintf(os.Stderr, "log %s: read lines failed: %v\n", l.name, err)
time.Sleep(1 * time.Second)
}
return
}
l.Write(line)
}
}
// Print to this log.
func (l *Log) Print(v ...interface{}) {
fmt.Fprint(l, v...)
}
// Printf to this log.
func (l *Log) Printf(pattern string, v ...interface{}) {
fmt.Fprintf(l, pattern, v...)
}
// Taint is Print to this log with a taint.
func (l *Log) Taint(taint Taint, v ...interface{}) {
l.append(taint, []byte(fmt.Sprint(v...)))
}
// Taintf is Printf to this log with a taint.
func (l *Log) Taintf(taint Taint, pattern string, v ...interface{}) {
l.append(taint, []byte(fmt.Sprintf(pattern, v...)))
}
func (l *Log) append(taint Taint, data []byte) {
// we serialize writes
l.l.Lock()
defer l.l.Unlock()
e := Entry{
Time: time.Now(),
Taint: taint,
Data: data,
}
console := l.console
if console != nil {
buf := &bytes.Buffer{}
buf.WriteString(string(color.DarkGreen))
buf.WriteString(e.Time.Format("2006/01/02 15:04:05.000 "))
buf.WriteString(string(color.Reset))
buf.WriteString(string(e.Taint.Color()))
buf.Write(data)
if data[len(data)-1] != '\n' {
buf.Write([]byte{'\n'})
}
buf.WriteString(string(color.Reset))
buf.WriteTo(console)
}
if !l.writeToFile {
l.pending = append(l.pending, e)
// TODO if len(pending) > maxPending { pending = pending[len(pending)-underMaxPending:] }
// or use a ring
return
}
if err := l.writeEntry(e); err != nil {
l.emergencyLog(e, err)
}
}
func (l *Log) emergencyLog(entry Entry, err error) {
fmt.Fprintf(os.Stderr, "log %s: failed to write entry: %v\n -> lost entry: ", l.name, err)
entry.WriteTo(os.Stderr)
}
// Write is part of the io.Writer interface.
func (l *Log) Write(b []byte) (n int, err error) {
l.append(Normal, b)
return len(b), nil
}
func (l *Log) writeEntry(e Entry) (err error) {
ts := e.Time.Truncate(time.Hour).Format(time.RFC3339)
path := fmt.Sprintf("/var/log/%s.log", l.name)
if l.outTS != ts {
if l.out != nil {
if err := l.out.Close(); err != nil {
fmt.Fprintf(os.Stderr, "log %s: failed to close output: %v\n", l.name, err)
}
archPath := fmt.Sprintf("/var/log/archives/%s.%s.log", l.name, l.outTS)
os.MkdirAll(filepath.Dir(archPath), 0700)
if err := os.Rename(path, archPath); err != nil {
fmt.Fprintf(os.Stderr, "log %s: failed to achive: %v", l.name, err)
}
go compress(archPath)
}
l.out = nil
l.outTS = ""
}
if l.out == nil {
l.out, err = os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0640)
if err != nil {
return
}
l.outTS = ts
}
_, err = e.WriteTo(l.out)
return
}