diff --git a/app/api/api.go b/app/api/api.go index 170c44d6..6a3e6e92 100644 --- a/app/api/api.go +++ b/app/api/api.go @@ -10,6 +10,7 @@ import ( gonet "net" gohttp "net/http" "net/url" + "os" "path/filepath" "runtime/debug" "sync" @@ -147,7 +148,12 @@ func (a *api) Reload() error { a.errorChan = make(chan error, 1) } - logger := log.New("Core").WithOutput(log.NewConsoleWriter(a.log.writer, log.Lwarn, true)) + logger := log.New("Core").WithOutput( + log.NewLevelWriter( + log.NewConsoleWriter(a.log.writer, true), + log.Lwarn, + ), + ) store, err := configstore.NewJSON(a.config.path, func() { a.errorChan <- ErrConfigReload @@ -183,31 +189,54 @@ func (a *api) Reload() error { break } - buffer := log.NewBufferWriter(loglevel, cfg.Log.MaxLines) + buffer := log.NewBufferWriter(cfg.Log.MaxLines) + var writer log.Writer - logger = logger.WithOutput(log.NewLevelRewriter( - log.NewMultiWriter( - log.NewTopicWriter( - log.NewConsoleWriter(a.log.writer, loglevel, true), - cfg.Log.Topics, - ), - buffer, - ), - []log.LevelRewriteRule{ - // FFmpeg annoyance, move all warnings about unathorized access to memfs from ffmpeg to debug level - // ts=2022-04-28T07:24:27Z level=WARN component="HTTP" address=":8080" client="::1" latency_ms=0 method="PUT" path="/memfs/00a10a69-416a-4cd5-9d4f-6d88ed3dd7f5_0917.ts" proto="HTTP/1.1" size_bytes=65 status=401 status_text="Unauthorized" user_agent="Lavf/58.76.100" - { - Level: log.Ldebug, - Component: "HTTP", - Match: map[string]string{ - "client": "^(::1|127.0.0.1)$", - "method": "^(PUT|POST|DELETE)$", - "status_text": "^Unauthorized$", - "user_agent": "^Lavf/", + if cfg.Log.Target.Output == "stdout" { + writer = log.NewConsoleWriter( + os.Stdout, + true, + ) + } else if cfg.Log.Target.Output == "file" { + writer = log.NewFileWriter( + cfg.Log.Target.Path, + log.NewJSONFormatter(), + ) + } else { + writer = log.NewConsoleWriter( + os.Stderr, + true, + ) + } + + logger = logger.WithOutput( + log.NewLevelWriter( + log.NewLevelRewriter( + log.NewMultiWriter( + log.NewTopicWriter( + writer, + cfg.Log.Topics, + ), + buffer, + ), + []log.LevelRewriteRule{ + // FFmpeg annoyance, move all warnings about unathorized access to memfs from ffmpeg to debug level + // ts=2022-04-28T07:24:27Z level=WARN component="HTTP" address=":8080" client="::1" latency_ms=0 method="PUT" path="/memfs/00a10a69-416a-4cd5-9d4f-6d88ed3dd7f5_0917.ts" proto="HTTP/1.1" size_bytes=65 status=401 status_text="Unauthorized" user_agent="Lavf/58.76.100" + { + Level: log.Ldebug, + Component: "HTTP", + Match: map[string]string{ + "client": "^(::1|127.0.0.1)$", + "method": "^(PUT|POST|DELETE)$", + "status_text": "^Unauthorized$", + "user_agent": "^Lavf/", + }, + }, }, - }, - }, - )) + ), + loglevel, + ), + ) logfields := log.Fields{ "application": app.Name, @@ -1297,4 +1326,6 @@ func (a *api) Destroy() { a.memfs.DeleteAll() a.memfs = nil } + + a.log.logger.core.Close() } diff --git a/app/ffmigrate/main.go b/app/ffmigrate/main.go index 036af80f..fa6b746d 100644 --- a/app/ffmigrate/main.go +++ b/app/ffmigrate/main.go @@ -17,7 +17,12 @@ import ( ) func main() { - logger := log.New("Migration").WithOutput(log.NewConsoleWriter(os.Stderr, log.Linfo, true)).WithFields(log.Fields{ + logger := log.New("Migration").WithOutput( + log.NewLevelWriter( + log.NewConsoleWriter(os.Stderr, true), + log.Linfo, + ), + ).WithFields(log.Fields{ "from": "ffmpeg4", "to": "ffmpeg5", }) diff --git a/app/import/main.go b/app/import/main.go index ebfe1aa0..02368b8b 100644 --- a/app/import/main.go +++ b/app/import/main.go @@ -13,7 +13,12 @@ import ( ) func main() { - logger := log.New("Import").WithOutput(log.NewConsoleWriter(os.Stderr, log.Linfo, true)).WithField("version", "v1") + logger := log.New("Import").WithOutput( + log.NewLevelWriter( + log.NewConsoleWriter(os.Stderr, true), + log.Linfo, + ), + ).WithField("version", "v1") configstore, err := cfgstore.NewJSON(os.Getenv("CORE_CONFIGFILE"), nil) if err != nil { diff --git a/config/config.go b/config/config.go index b8a5028e..2625bad1 100644 --- a/config/config.go +++ b/config/config.go @@ -141,6 +141,8 @@ func (d *Config) init() { d.vars.Register(value.NewString(&d.Log.Level, "info"), "log.level", "CORE_LOG_LEVEL", nil, "Loglevel: silent, error, warn, info, debug", false, false) d.vars.Register(value.NewStringList(&d.Log.Topics, []string{}, ","), "log.topics", "CORE_LOG_TOPICS", nil, "Show only selected log topics", false, false) d.vars.Register(value.NewInt(&d.Log.MaxLines, 1000), "log.max_lines", "CORE_LOG_MAXLINES", nil, "Number of latest log lines to keep in memory", false, false) + d.vars.Register(value.NewString(&d.Log.Target.Output, "stderr"), "log.target.output", "CORE_LOG_TARGET_OUTPUT", nil, "Where to write the logs to: stdout, stderr, file", false, false) + d.vars.Register(value.NewString(&d.Log.Target.Path, ""), "log.target.path", "CORE_LOG_TARGET_PATH", nil, "Path to log file if output is 'file'", false, false) // DB d.vars.Register(value.NewMustDir(&d.DB.Dir, "./config"), "db.dir", "CORE_DB_DIR", nil, "Directory for holding the operational data", false, false) diff --git a/config/data.go b/config/data.go index bb836b3f..b6f4e34d 100644 --- a/config/data.go +++ b/config/data.go @@ -22,6 +22,10 @@ type Data struct { Level string `json:"level" enums:"debug,info,warn,error,silent" jsonschema:"enum=debug,enum=info,enum=warn,enum=error,enum=silent"` Topics []string `json:"topics"` MaxLines int `json:"max_lines"` + Target struct { + Output string `json:"name"` + Path string `json:"path"` + } `json:"target"` // discard, stderr, stdout, file:/path/to/file.log } `json:"log"` DB struct { Dir string `json:"dir"` @@ -182,7 +186,6 @@ func MergeV2toV3(data *Data, d *v2.Data) (*Data, error) { data.Address = d.Address data.CheckForUpdates = d.CheckForUpdates - data.Log = d.Log data.DB = d.DB data.Host = d.Host data.API = d.API @@ -195,8 +198,6 @@ func MergeV2toV3(data *Data, d *v2.Data) (*Data, error) { data.Service = d.Service data.Router = d.Router - data.Log.Topics = copy.Slice(d.Log.Topics) - data.Host.Name = copy.Slice(d.Host.Name) data.API.Access.HTTP.Allow = copy.Slice(d.API.Access.HTTP.Allow) @@ -228,6 +229,12 @@ func MergeV2toV3(data *Data, d *v2.Data) (*Data, error) { data.Storage.Memory = d.Storage.Memory // Actual changes + data.Log.Level = d.Log.Level + data.Log.Topics = copy.Slice(d.Log.Topics) + data.Log.MaxLines = d.Log.MaxLines + data.Log.Target.Output = "stderr" + data.Log.Target.Path = "" + data.Debug.Profiling = d.Debug.Profiling data.Debug.ForceGC = d.Debug.ForceGC data.Debug.MemoryLimit = 0 @@ -263,7 +270,6 @@ func DowngradeV3toV2(d *Data) (*v2.Data, error) { data.Address = d.Address data.CheckForUpdates = d.CheckForUpdates - data.Log = d.Log data.DB = d.DB data.Host = d.Host data.API = d.API @@ -276,8 +282,6 @@ func DowngradeV3toV2(d *Data) (*v2.Data, error) { data.Service = d.Service data.Router = d.Router - data.Log.Topics = copy.Slice(d.Log.Topics) - data.Host.Name = copy.Slice(d.Host.Name) data.API.Access.HTTP.Allow = copy.Slice(d.API.Access.HTTP.Allow) @@ -302,6 +306,10 @@ func DowngradeV3toV2(d *Data) (*v2.Data, error) { data.Router.Routes = copy.StringMap(d.Router.Routes) // Actual changes + data.Log.Level = d.Log.Level + data.Log.Topics = copy.Slice(d.Log.Topics) + data.Log.MaxLines = d.Log.MaxLines + data.Debug.Profiling = d.Debug.Profiling data.Debug.ForceGC = d.Debug.ForceGC diff --git a/http/graph/resolver/log.resolvers.go b/http/graph/resolver/log.resolvers.go index 50006348..a6dbf1c8 100644 --- a/http/graph/resolver/log.resolvers.go +++ b/http/graph/resolver/log.resolvers.go @@ -12,7 +12,7 @@ import ( func (r *queryResolver) Log(ctx context.Context) ([]string, error) { if r.LogBuffer == nil { - r.LogBuffer = log.NewBufferWriter(log.Lsilent, 1) + r.LogBuffer = log.NewBufferWriter(1) } events := r.LogBuffer.Events() diff --git a/http/handler/api/log.go b/http/handler/api/log.go index 8ef4dbed..847ddf51 100644 --- a/http/handler/api/log.go +++ b/http/handler/api/log.go @@ -22,7 +22,7 @@ func NewLog(buffer log.BufferWriter) *LogHandler { } if l.buffer == nil { - l.buffer = log.NewBufferWriter(log.Lsilent, 1) + l.buffer = log.NewBufferWriter(1) } return l diff --git a/log/log.go b/log/log.go index be226028..4a6f6d42 100644 --- a/log/log.go +++ b/log/log.go @@ -14,28 +14,29 @@ import ( type Level uint const ( - Lsilent Level = 0 - Lerror Level = 1 - Lwarn Level = 2 - Linfo Level = 3 - Ldebug Level = 4 + Lsilent Level = 0b0000 + Lerror Level = 0b0001 + Lwarn Level = 0b0010 + Linfo Level = 0b0100 + Ldebug Level = 0b1000 ) // String returns a string representing the log level. func (level Level) String() string { - names := []string{ - "SILENT", - "ERROR", - "WARN", - "INFO", - "DEBUG", - } - - if level > Ldebug { + switch level { + case Lsilent: + return "SILENT" + case Lerror: + return "ERROR" + case Lwarn: + return "WARN" + case Linfo: + return "INFO" + case Ldebug: + return "DEBUG" + default: return `¯\_(ツ)_/¯` } - - return names[level] } func (level *Level) MarshalJSON() ([]byte, error) { @@ -97,6 +98,9 @@ type Logger interface { // Write implements the io.Writer interface such that it can be used in e.g. the // the log/Logger facility. Messages will be printed with debug level. Write(p []byte) (int, error) + + // Close closes the underlying writer. + Close() } // logger is an implementation of the Logger interface. @@ -184,6 +188,10 @@ func (l *logger) Write(p []byte) (int, error) { return newEvent(l).Write(p) } +func (l *logger) Close() { + l.output.Close() +} + type Event struct { logger *logger @@ -352,12 +360,6 @@ func (l *Event) Write(p []byte) (int, error) { return len(p), nil } -type Eventx struct { - Time time.Time `json:"ts"` - Level Level `json:"level"` - Component string `json:"component"` - Reference string `json:"ref"` - Message string `json:"message"` - Caller string `json:"caller"` - Detail interface{} `json:"detail"` +func (l *Event) Close() { + l.logger.Close() } diff --git a/log/log_test.go b/log/log_test.go index 1a04a1f0..36190033 100644 --- a/log/log_test.go +++ b/log/log_test.go @@ -5,25 +5,25 @@ import ( "bytes" "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestLoglevelNames(t *testing.T) { - assert.Equal(t, "DEBUG", Ldebug.String()) - assert.Equal(t, "ERROR", Lerror.String()) - assert.Equal(t, "WARN", Lwarn.String()) - assert.Equal(t, "INFO", Linfo.String()) - assert.Equal(t, `SILENT`, Lsilent.String()) + require.Equal(t, "DEBUG", Ldebug.String()) + require.Equal(t, "ERROR", Lerror.String()) + require.Equal(t, "WARN", Lwarn.String()) + require.Equal(t, "INFO", Linfo.String()) + require.Equal(t, `SILENT`, Lsilent.String()) } func TestLogColorToNotTTY(t *testing.T) { var buffer bytes.Buffer writer := bufio.NewWriter(&buffer) - w := NewConsoleWriter(writer, Linfo, true).(*syncWriter) + w := NewLevelWriter(NewConsoleWriter(writer, true), Linfo).(*levelWriter).writer.(*syncWriter) formatter := w.writer.(*consoleWriter).formatter.(*consoleFormatter) - assert.NotEqual(t, true, formatter.color, "Color should not be used on a buffer logger") + require.NotEqual(t, true, formatter.color, "Color should not be used on a buffer logger") } func TestLogContext(t *testing.T) { @@ -31,7 +31,7 @@ func TestLogContext(t *testing.T) { var buffer bytes.Buffer writer := bufio.NewWriter(&buffer) - logger := New("component").WithOutput(NewConsoleWriter(writer, Ldebug, false)) + logger := New("component").WithOutput(NewLevelWriter(NewConsoleWriter(writer, false), Ldebug)) logger.Debug().Log("debug") logger.Info().Log("info") @@ -53,19 +53,19 @@ func TestLogContext(t *testing.T) { lenWithoutCtx := buffer.Len() buffer.Reset() - assert.Greater(t, lenWithCtx, lenWithoutCtx, "Log line length without context is not shorter than with context") + require.Greater(t, lenWithCtx, lenWithoutCtx, "Log line length without context is not shorter than with context") } func TestLogClone(t *testing.T) { var buffer bytes.Buffer writer := bufio.NewWriter(&buffer) - logger := New("test").WithOutput(NewConsoleWriter(writer, Linfo, false)) + logger := New("test").WithOutput(NewLevelWriter(NewConsoleWriter(writer, false), Linfo)) logger.Info().Log("info") writer.Flush() - assert.Contains(t, buffer.String(), `component="test"`) + require.Contains(t, buffer.String(), `component="test"`) buffer.Reset() @@ -74,33 +74,33 @@ func TestLogClone(t *testing.T) { logger2.Info().Log("info") writer.Flush() - assert.Contains(t, buffer.String(), `component="tset"`) + require.Contains(t, buffer.String(), `component="tset"`) } func TestLogSilent(t *testing.T) { var buffer bytes.Buffer writer := bufio.NewWriter(&buffer) - logger := New("test").WithOutput(NewConsoleWriter(writer, Lsilent, false)) + logger := New("test").WithOutput(NewLevelWriter(NewConsoleWriter(writer, false), Lsilent)) logger.Debug().Log("debug") writer.Flush() - assert.Equal(t, 0, buffer.Len(), "Buffer should be empty") + require.Equal(t, 0, buffer.Len(), "Buffer should be empty") buffer.Reset() logger.Info().Log("info") writer.Flush() - assert.Equal(t, 0, buffer.Len(), "Buffer should be empty") + require.Equal(t, 0, buffer.Len(), "Buffer should be empty") buffer.Reset() logger.Warn().Log("warn") writer.Flush() - assert.Equal(t, 0, buffer.Len(), "Buffer should be empty") + require.Equal(t, 0, buffer.Len(), "Buffer should be empty") buffer.Reset() logger.Error().Log("error") writer.Flush() - assert.Equal(t, 0, buffer.Len(), "Buffer should be empty") + require.Equal(t, 0, buffer.Len(), "Buffer should be empty") buffer.Reset() } @@ -108,26 +108,26 @@ func TestLogDebug(t *testing.T) { var buffer bytes.Buffer writer := bufio.NewWriter(&buffer) - logger := New("test").WithOutput(NewConsoleWriter(writer, Ldebug, false)) + logger := New("test").WithOutput(NewLevelWriter(NewConsoleWriter(writer, false), Ldebug)) logger.Debug().Log("debug") writer.Flush() - assert.NotEqual(t, 0, buffer.Len(), "Buffer should not be empty") + require.NotEqual(t, 0, buffer.Len(), "Buffer should not be empty") buffer.Reset() logger.Info().Log("info") writer.Flush() - assert.NotEqual(t, 0, buffer.Len(), "Buffer should not be empty") + require.NotEqual(t, 0, buffer.Len(), "Buffer should not be empty") buffer.Reset() logger.Warn().Log("warn") writer.Flush() - assert.NotEqual(t, 0, buffer.Len(), "Buffer should not be empty") + require.NotEqual(t, 0, buffer.Len(), "Buffer should not be empty") buffer.Reset() logger.Error().Log("error") writer.Flush() - assert.NotEqual(t, 0, buffer.Len(), "Buffer should not be empty") + require.NotEqual(t, 0, buffer.Len(), "Buffer should not be empty") buffer.Reset() } @@ -135,26 +135,26 @@ func TestLogInfo(t *testing.T) { var buffer bytes.Buffer writer := bufio.NewWriter(&buffer) - logger := New("test").WithOutput(NewConsoleWriter(writer, Linfo, false)) + logger := New("test").WithOutput(NewLevelWriter(NewConsoleWriter(writer, false), Linfo)) logger.Debug().Log("debug") writer.Flush() - assert.Equal(t, 0, buffer.Len(), "Buffer should be empty") + require.Equal(t, 0, buffer.Len(), "Buffer should be empty") buffer.Reset() logger.Info().Log("info") writer.Flush() - assert.NotEqual(t, 0, buffer.Len(), "Buffer should not be empty") + require.NotEqual(t, 0, buffer.Len(), "Buffer should not be empty") buffer.Reset() logger.Warn().Log("warn") writer.Flush() - assert.NotEqual(t, 0, buffer.Len(), "Buffer should not be empty") + require.NotEqual(t, 0, buffer.Len(), "Buffer should not be empty") buffer.Reset() logger.Error().Log("error") writer.Flush() - assert.NotEqual(t, 0, buffer.Len(), "Buffer should not be empty") + require.NotEqual(t, 0, buffer.Len(), "Buffer should not be empty") buffer.Reset() } @@ -162,26 +162,26 @@ func TestLogWarn(t *testing.T) { var buffer bytes.Buffer writer := bufio.NewWriter(&buffer) - logger := New("test").WithOutput(NewConsoleWriter(writer, Lwarn, false)) + logger := New("test").WithOutput(NewLevelWriter(NewConsoleWriter(writer, false), Lwarn)) logger.Debug().Log("debug") writer.Flush() - assert.Equal(t, 0, buffer.Len(), "Buffer should be empty") + require.Equal(t, 0, buffer.Len(), "Buffer should be empty") buffer.Reset() logger.Info().Log("info") writer.Flush() - assert.Equal(t, 0, buffer.Len(), "Buffer should be empty") + require.Equal(t, 0, buffer.Len(), "Buffer should be empty") buffer.Reset() logger.Warn().Log("warn") writer.Flush() - assert.NotEqual(t, 0, buffer.Len(), "Buffer should not be empty") + require.NotEqual(t, 0, buffer.Len(), "Buffer should not be empty") buffer.Reset() logger.Error().Log("error") writer.Flush() - assert.NotEqual(t, 0, buffer.Len(), "Buffer should not be empty") + require.NotEqual(t, 0, buffer.Len(), "Buffer should not be empty") buffer.Reset() } @@ -189,25 +189,25 @@ func TestLogError(t *testing.T) { var buffer bytes.Buffer writer := bufio.NewWriter(&buffer) - logger := New("test").WithOutput(NewConsoleWriter(writer, Lerror, false)) + logger := New("test").WithOutput(NewLevelWriter(NewConsoleWriter(writer, false), Lerror)) logger.Debug().Log("debug") writer.Flush() - assert.Equal(t, 0, buffer.Len(), "Buffer should be empty") + require.Equal(t, 0, buffer.Len(), "Buffer should be empty") buffer.Reset() logger.Info().Log("info") writer.Flush() - assert.Equal(t, 0, buffer.Len(), "Buffer should be empty") + require.Equal(t, 0, buffer.Len(), "Buffer should be empty") buffer.Reset() logger.Warn().Log("warn") writer.Flush() - assert.Equal(t, 0, buffer.Len(), "Buffer should be empty") + require.Equal(t, 0, buffer.Len(), "Buffer should be empty") buffer.Reset() logger.Error().Log("error") writer.Flush() - assert.NotEqual(t, 0, buffer.Len(), "Buffer should not be empty") + require.NotEqual(t, 0, buffer.Len(), "Buffer should not be empty") buffer.Reset() } diff --git a/log/output.go b/log/output.go new file mode 100644 index 00000000..5ffd9497 --- /dev/null +++ b/log/output.go @@ -0,0 +1,43 @@ +package log + +import ( + "io" + "os" + + "github.com/mattn/go-isatty" +) + +type consoleOutput struct { + writer io.Writer + formatter Formatter +} + +func NewConsoleOutput(w io.Writer, useColor bool) Writer { + writer := &consoleOutput{ + writer: w, + } + + color := useColor + + if color { + if w, ok := w.(*os.File); ok { + if !isatty.IsTerminal(w.Fd()) && !isatty.IsCygwinTerminal(w.Fd()) { + color = false + } + } else { + color = false + } + } + + writer.formatter = NewConsoleFormatter(color) + + return NewSyncWriter(writer) +} + +func (w *consoleOutput) Write(e *Event) error { + _, err := w.writer.Write(w.formatter.Bytes(e)) + + return err +} + +func (w *consoleOutput) Close() {} diff --git a/log/writer.go b/log/writer.go index f04da26d..868ecb17 100644 --- a/log/writer.go +++ b/log/writer.go @@ -13,18 +13,50 @@ import ( type Writer interface { Write(e *Event) error + Close() +} + +type discardWriter struct{} + +func NewDiscardWriter() Writer { + return &discardWriter{} +} + +func (w *discardWriter) Write(e *Event) error { return nil } +func (w *discardWriter) Close() {} + +type levelWriter struct { + writer Writer + level Level +} + +func NewLevelWriter(w Writer, level Level) Writer { + return &levelWriter{ + writer: w, + level: level, + } +} + +func (w *levelWriter) Write(e *Event) error { + if w.level < e.Level || e.Level == Lsilent { + return nil + } + + return w.writer.Write(e) +} + +func (w *levelWriter) Close() { + w.writer.Close() } type jsonWriter struct { writer io.Writer - level Level formatter Formatter } -func NewJSONWriter(w io.Writer, level Level) Writer { +func NewJSONWriter(w io.Writer) Writer { writer := &jsonWriter{ writer: w, - level: level, formatter: NewJSONFormatter(), } @@ -32,25 +64,21 @@ func NewJSONWriter(w io.Writer, level Level) Writer { } func (w *jsonWriter) Write(e *Event) error { - if w.level < e.Level || e.Level == Lsilent { - return nil - } - _, err := w.writer.Write(w.formatter.Bytes(e)) return err } +func (w *jsonWriter) Close() {} + type consoleWriter struct { writer io.Writer - level Level formatter Formatter } -func NewConsoleWriter(w io.Writer, level Level, useColor bool) Writer { +func NewConsoleWriter(w io.Writer, useColor bool) Writer { writer := &consoleWriter{ writer: w, - level: level, } color := useColor @@ -71,15 +99,13 @@ func NewConsoleWriter(w io.Writer, level Level, useColor bool) Writer { } func (w *consoleWriter) Write(e *Event) error { - if w.level < e.Level || e.Level == Lsilent { - return nil - } - _, err := w.writer.Write(w.formatter.Bytes(e)) return err } +func (w *consoleWriter) Close() {} + type topicWriter struct { writer Writer topics map[string]struct{} @@ -112,6 +138,10 @@ func (w *topicWriter) Write(e *Event) error { return err } +func (w *topicWriter) Close() { + w.writer.Close() +} + type levelRewriter struct { writer Writer rules []levelRewriteRule @@ -182,6 +212,10 @@ rules: return w.writer.Write(e) } +func (w *levelRewriter) Close() { + w.writer.Close() +} + type syncWriter struct { mu sync.Mutex writer Writer @@ -193,11 +227,15 @@ func NewSyncWriter(writer Writer) Writer { } } -func (s *syncWriter) Write(e *Event) error { - s.mu.Lock() - defer s.mu.Unlock() +func (w *syncWriter) Write(e *Event) error { + w.mu.Lock() + defer w.mu.Unlock() - return s.writer.Write(e) + return w.writer.Write(e) +} + +func (w *syncWriter) Close() { + w.writer.Close() } type multiWriter struct { @@ -212,8 +250,8 @@ func NewMultiWriter(writer ...Writer) Writer { return mw } -func (m *multiWriter) Write(e *Event) error { - for _, w := range m.writer { +func (w *multiWriter) Write(e *Event) error { + for _, w := range w.writer { if err := w.Write(e); err != nil { return err } @@ -222,6 +260,12 @@ func (m *multiWriter) Write(e *Event) error { return nil } +func (w *multiWriter) Close() { + for _, w := range w.writer { + w.Close() + } +} + type BufferWriter interface { Writer Events() []*Event @@ -230,13 +274,10 @@ type BufferWriter interface { type bufferWriter struct { lines *ring.Ring lock sync.RWMutex - level Level } -func NewBufferWriter(level Level, lines int) BufferWriter { - b := &bufferWriter{ - level: level, - } +func NewBufferWriter(lines int) BufferWriter { + b := &bufferWriter{} if lines > 0 { b.lines = ring.New(lines) @@ -245,33 +286,31 @@ func NewBufferWriter(level Level, lines int) BufferWriter { return b } -func (b *bufferWriter) Write(e *Event) error { - if b.level < e.Level || e.Level == Lsilent { - return nil - } +func (w *bufferWriter) Write(e *Event) error { + w.lock.Lock() + defer w.lock.Unlock() - b.lock.Lock() - defer b.lock.Unlock() - - if b.lines != nil { - b.lines.Value = e.clone() - b.lines = b.lines.Next() + if w.lines != nil { + w.lines.Value = e.clone() + w.lines = w.lines.Next() } return nil } -func (b *bufferWriter) Events() []*Event { +func (w *bufferWriter) Close() {} + +func (w *bufferWriter) Events() []*Event { var lines = []*Event{} - if b.lines == nil { + if w.lines == nil { return lines } - b.lock.RLock() - defer b.lock.RUnlock() + w.lock.RLock() + defer w.lock.RUnlock() - b.lines.Do(func(l interface{}) { + w.lines.Do(func(l interface{}) { if l == nil { return } @@ -281,3 +320,32 @@ func (b *bufferWriter) Events() []*Event { return lines } + +type fileWriter struct { + writer *os.File + formatter Formatter +} + +func NewFileWriter(path string, formatter Formatter) Writer { + file, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_RDWR|os.O_SYNC, 0600) + if err != nil { + return NewDiscardWriter() + } + + writer := &fileWriter{ + writer: file, + formatter: formatter, + } + + return NewSyncWriter(writer) +} + +func (w *fileWriter) Write(e *Event) error { + _, err := w.writer.Write(append(w.formatter.Bytes(e), '\n')) + + return err +} + +func (w *fileWriter) Close() { + w.writer.Close() +} diff --git a/main.go b/main.go index 6d16a134..aa65385d 100644 --- a/main.go +++ b/main.go @@ -12,7 +12,15 @@ import ( ) func main() { - logger := log.New("Core").WithOutput(log.NewConsoleWriter(os.Stderr, log.Lwarn, true)) + logger := log.New("Core").WithOutput( + log.NewLevelWriter( + log.NewConsoleWriter( + os.Stderr, + true, + ), + log.Lwarn, + ), + ) configfile := findConfigfile() @@ -54,6 +62,8 @@ func main() { signal.Notify(quit, os.Interrupt) <-quit + logger.Close() + // Stop the app app.Destroy() }