-
Notifications
You must be signed in to change notification settings - Fork 45
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
A new MultiWriters design that doesn't require parsing levels #8
Conversation
It's hard for me answer the question/issue. Please give me a chance to talk about this log motivation. As we have seen, a traditional logging design has 3 layers.
In this log, I removed/cancelled the Formatter layer to try But, the real world is really complicated, I must face to a number of disadvantages of it, var glog = (&log.Logger{
Level: log.InfoLevel,
Caller: 1,
TimeFormat: "0102 15:04:05.999999",
Writer: &log.ConsoleWriter{
Template: template.Must(template.New("").Parse(`{{.Level.First}}{{.Time}} {{.Goid}} {{.Caller}}] {{.Message}}`)),
Writer: FileWriter{
Filename: "main.log",
MaxSize: 2 * 1024 * 1024 * 1024,
MaxBackups: 2,
},
},
}).Sugar(nil) I explained why I think If It is, so the strategy
(Please correct me if there're some mistakes in above comparison) So what will we choose?
Let's see what will be happen, make sense? |
8255165
to
b23266f
Compare
Thanks for your explanation. I agree with you that with an extra "formatter" layer, the problem of writing to console with The problem with As to the cons you listed:
By the way, I've added two benchmarks to see how the new and old solution compared: goos: darwin
goarch: amd64
pkg: github.com/phuslu/log
BenchmarkOldMultiWriter-12 57088 19269 ns/op 648 B/op 6 allocs/op
BenchmarkOldMultiWriter-12 61423 19219 ns/op 648 B/op 6 allocs/op
BenchmarkOldMultiWriter-12 61670 19632 ns/op 648 B/op 6 allocs/op
PASS
ok github.com/phuslu/log 4.550s
goos: darwin
goarch: amd64
pkg: github.com/phuslu/log
BenchmarkNewMultiWriter-12 92205 11290 ns/op 744 B/op 9 allocs/op
BenchmarkNewMultiWriter-12 89068 11454 ns/op 744 B/op 9 allocs/op
BenchmarkNewMultiWriter-12 103980 11342 ns/op 744 B/op 9 allocs/op
PASS
ok github.com/phuslu/log 4.433s |
Sorry, I haven't go through your code carefully. The performance cost in my thought:
type LeveledWriter interface {
WriteLevel(Level, []byte) (int, error)
}
func (e *Event) Msg(msg string) {
// blah blah
if lw, ok := e.w.(LeveledWriter); ok { // performance hit
lw.LevelWrite(e.level, e.buf)
} else {
e.w.Write(e.buf)
}
} This is my initial thought and I think above performance cost does not make sense. (Because it's unfair for non-MultiWriter users) Now, combine my thought and your PR, maybe we could
// A Logger represents an active logging object that generates lines of JSON output to an io.Writer.
type Logger struct {
// blah blah blah
// Writer specifies the writer of output. It uses os.Stderr in if empty.
Writer io.Writer
// a good document to clarify what LeveledWriter for
LeveledWriter LeveledWriter
} Please comment. |
Sure, we can use some more interfaces. By the way, I find the semantics of |
Yes, it's undecided. When I adding I'm not sure how to do a well compromise between this log level desgin and glog design.
|
Then let's defer the decision making around these levels. I'll keep the new design compatible with the original one. |
27bc142
to
0d84e03
Compare
A fair(to non-MultiWiter user) draft, diff --git a/logger.go b/logger.go
index b73696c..f1131af 100644
--- a/logger.go
+++ b/logger.go
@@ -42,6 +42,9 @@ type Logger struct {
// Writer specifies the writer of output. It uses os.Stderr in if empty.
Writer io.Writer
+
+ // a good document needed.
+ LeveledWriter LeveledWriter
}
const (
@@ -288,6 +291,9 @@ func (l *Logger) header(level Level) *Event {
} else {
e.w = os.Stderr
}
+ if l.LeveledWriter != nil {
+ e.w = wrapLeveledWriter{level, l.LeveledWriter}
+ }
// time
if l.TimeField == "" {
e.buf = append(e.buf, "{\"time\":"...)
diff --git a/multi.go b/multi.go
index 85fe0df..de7c376 100644
--- a/multi.go
+++ b/multi.go
@@ -1,10 +1,13 @@
package log
import (
- "bytes"
"io"
)
+type LeveledWriter interface {
+ WriteLevel(Level, []byte) (int, error)
+}
+
// MultiWriter is an io.WriteCloser that log to different writers by different levels
type MultiWriter struct {
// InfoWriter specifies the level large than info logs writes to
@@ -21,16 +24,6 @@ type MultiWriter struct {
// StderrLevel specifies the minimal level logs it will be writes to stderr
StderrLevel Level
-
- // ParseLevel specifies an optional callback for parse log level in output
- // log.DebugLogger.Writer = &log.MultiWriter {
- // InfoWriter: &log.FileWriter{Filename: "main-info.log"},
- // ErrorWriter: &log.FileWriter{Filename: "main-error.log"},
- // StderrWriter: &log.ConsoleWriter{ColorOutput: true},
- // StderrLevel: log.ErrorLevel,
- // ParseLevel: func([]byte) log.Level { return log.ParseLevel(string(p[49])) },
- // }
- ParseLevel func([]byte) Level
}
// Close implements io.Closer, and closes the underlying Writers.
@@ -54,52 +47,10 @@ func (w *MultiWriter) Close() (err error) {
return
}
-var levelBegin = []byte(`"level":"`)
-
-// Write implements io.Writer.
-func (w *MultiWriter) Write(p []byte) (n int, err error) {
- var level = noLevel
- if w.ParseLevel != nil {
- level = w.ParseLevel(p)
- } else {
- var l byte
- // guess level by fixed offset
- lp := len(p)
- if lp > 49 {
- _ = p[49]
- switch {
- case p[32] == 'Z' && p[42] == ':' && p[43] == '"':
- l = p[44]
- case p[32] == '+' && p[47] == ':' && p[48] == '"':
- l = p[49]
- }
- }
- // guess level by "level":" beginning
- if l == 0 {
- if i := bytes.Index(p, levelBegin); i > 0 && i+len(levelBegin)+1 < lp {
- l = p[i+len(levelBegin)]
- }
- }
- // convert byte to Level
- switch l {
- case 't':
- level = TraceLevel
- case 'd':
- level = DebugLevel
- case 'i':
- level = InfoLevel
- case 'w':
- level = WarnLevel
- case 'e':
- level = ErrorLevel
- case 'f':
- level = FatalLevel
- case 'p':
- level = PanicLevel
- }
- }
-
+// WriteLevel implements log.LeveledWriter.
+func (w *MultiWriter) WriteLevel(level Level, p []byte) (n int, err error) {
var err1 error
+
switch level {
case noLevel, PanicLevel, FatalLevel, ErrorLevel:
if w.ErrorWriter != nil {
@@ -132,3 +83,13 @@ func (w *MultiWriter) Write(p []byte) (n int, err error) {
return
}
+
+type wrapLeveledWriter struct {
+ Level Level
+ LeveledWriter LeveledWriter
+}
+
+// WriteLevel implements log.LeveledWriter.
+func (w wrapLeveledWriter) Write(p []byte) (int, error) {
+ return w.LeveledWriter.WriteLevel(w.Level, p)
+} Motivation:
Could you take a look this draft? Maybe it's a good new base for this PR. I admit the draft has deeper interfaces may be hit performance. Perhaps we need a bench for it 😄 |
Cool. It seems to integrate better with your existing code. I'll give it a try. |
Thanks! I think the existing code of |
I've only used |
yep, we're |
I just found some similar requirements from sirupsen/logrus#618 |
0d84e03
to
b9b35f9
Compare
I've changed the implementation to use the interface you shown. I noticed that I've made a off-by-one mistake when running benchmarks for the new implementation. It's actually a little slower than the original implementation: goos: darwin
goarch: amd64
pkg: github.com/phuslu/log
BenchmarkNewMultiWriter-12 56396 19657 ns/op 744 B/op 9 allocs/op
BenchmarkNewMultiWriter-12 60616 19556 ns/op 744 B/op 9 allocs/op
BenchmarkNewMultiWriter-12 60768 19912 ns/op 744 B/op 9 allocs/op
PASS
ok github.com/phuslu/log 4.489s |
Thanks and Cheers ! 🍻 |
Will push/release a new tag soon :D |
Currently, var glog = (&log.Logger{
Level: log.InfoLevel,
Caller: 1,
TimeFormat: "0102 15:04:05.999999",
Writer: &log.ConsoleWriter{
Template: template.Must(template.New("").Parse(`{{.Level.First}}{{.Time}} {{.Goid}} {{.Caller}}] {{.Message}}`)),
Writer: &log.MultiWriter{
InfoWriter: &log.FileWriter{Filename: "server.INFO", MaxSize: 1610612736, MaxBackups: 2},
WarnWriter: &log.FileWriter{Filename: "server.WARNING", MaxSize: 1610612736, MaxBackups: 2},
InfoWriter: &log.FileWriter{Filename: "server.ERROR", MaxSize: 1610612736, MaxBackups: 2},
StderrWriter: os.Stderr,
StderrLevel: log.WarnLevel,
ParseLevel: func(p []data) log.Level { return log.ParseLevel(string(p[0])); },
},
},
}).Sugar(nil) This is a completely |
Cool, I'll take a look later. |
In the current implementation,
MultiWriter
is used asLogger.Writer
. Each time a message is written, we need to first parse the level from the input.I can't help wondering if this can be avoided. In this pull request (still WIP), I want to propose a new design to avoid the level parsing step.
Instead of making
MultiWriter
anio.Writer
, we just make it an optional field forLogger
. If it's set, we use it instead ofWriter
.When creating an
Event
, we use the level to decide what writers to use by callingMultiWriter.CombineWriters
, which collects the matched writers into oneio.Writer
implementation calledCombinedWriter
:Please let me know what you think about this design. Thanks.