245 lines
4.5 KiB
Go
245 lines
4.5 KiB
Go
package log
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"sync"
|
|
"time"
|
|
|
|
"novit.nc/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
|
|
outPath 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) {
|
|
path := fmt.Sprintf("/var/log/%s.%s.log",
|
|
l.name, e.Time.Truncate(time.Hour).Format(time.RFC3339))
|
|
|
|
currentPath := fmt.Sprintf("/var/log/%s.log", l.name)
|
|
|
|
if l.outPath != path {
|
|
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)
|
|
}
|
|
os.Remove(currentPath)
|
|
go compress(l.outPath)
|
|
}
|
|
l.out = nil
|
|
l.outPath = ""
|
|
}
|
|
|
|
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.outPath = path
|
|
|
|
os.Remove(currentPath)
|
|
if err := os.Symlink(path, currentPath); err != nil {
|
|
fmt.Fprintf(os.Stderr, "failed to symlink %s.log: %v\n", l.name, err)
|
|
}
|
|
}
|
|
|
|
_, err = e.WriteTo(l.out)
|
|
|
|
return
|
|
}
|