feat: implement console plain output for debug logs (#1456)
* feat: implement console plain output for debug logs * chore: rename console encoding to plain * chore: refactor names
This commit is contained in:
@@ -3,10 +3,11 @@ package logx
|
|||||||
// A LogConf is a logging config.
|
// A LogConf is a logging config.
|
||||||
type LogConf struct {
|
type LogConf struct {
|
||||||
ServiceName string `json:",optional"`
|
ServiceName string `json:",optional"`
|
||||||
Mode string `json:",default=console,options=console|file|volume"`
|
Mode string `json:",default=console,options=[console,file,volume]"`
|
||||||
|
Encoding string `json:",default=json,options=[json,plain]"`
|
||||||
TimeFormat string `json:",optional"`
|
TimeFormat string `json:",optional"`
|
||||||
Path string `json:",default=logs"`
|
Path string `json:",default=logs"`
|
||||||
Level string `json:",default=info,options=info|error|severe"`
|
Level string `json:",default=info,options=[info,error,severe]"`
|
||||||
Compress bool `json:",optional"`
|
Compress bool `json:",optional"`
|
||||||
KeepDays int `json:",optional"`
|
KeepDays int `json:",optional"`
|
||||||
StackCooldownMillis int `json:",default=100"`
|
StackCooldownMillis int `json:",default=100"`
|
||||||
|
|||||||
@@ -79,10 +79,15 @@ func (l *durationLogger) WithDuration(duration time.Duration) Logger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *durationLogger) write(writer io.Writer, level string, val interface{}) {
|
func (l *durationLogger) write(writer io.Writer, level string, val interface{}) {
|
||||||
outputJson(writer, &durationLogger{
|
switch encoding {
|
||||||
Timestamp: getTimestamp(),
|
case plainEncodingType:
|
||||||
Level: level,
|
writePlainAny(writer, level, val, l.Duration)
|
||||||
Content: val,
|
default:
|
||||||
Duration: l.Duration,
|
outputJson(writer, &durationLogger{
|
||||||
})
|
Timestamp: getTimestamp(),
|
||||||
|
Level: level,
|
||||||
|
Content: val,
|
||||||
|
Duration: l.Duration,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,19 @@ func TestWithDurationInfo(t *testing.T) {
|
|||||||
assert.True(t, strings.Contains(builder.String(), "duration"), builder.String())
|
assert.True(t, strings.Contains(builder.String(), "duration"), builder.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestWithDurationInfoConsole(t *testing.T) {
|
||||||
|
old := encoding
|
||||||
|
encoding = plainEncodingType
|
||||||
|
defer func() {
|
||||||
|
encoding = old
|
||||||
|
}()
|
||||||
|
|
||||||
|
var builder strings.Builder
|
||||||
|
log.SetOutput(&builder)
|
||||||
|
WithDuration(time.Second).Info("foo")
|
||||||
|
assert.True(t, strings.Contains(builder.String(), "ms"), builder.String())
|
||||||
|
}
|
||||||
|
|
||||||
func TestWithDurationInfof(t *testing.T) {
|
func TestWithDurationInfof(t *testing.T) {
|
||||||
var builder strings.Builder
|
var builder strings.Builder
|
||||||
log.SetOutput(&builder)
|
log.SetOutput(&builder)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package logx
|
package logx
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -31,6 +32,15 @@ const (
|
|||||||
SevereLevel
|
SevereLevel
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
jsonEncodingType = iota
|
||||||
|
plainEncodingType
|
||||||
|
|
||||||
|
jsonEncoding = "json"
|
||||||
|
plainEncoding = "plain"
|
||||||
|
plainEncodingSep = '\t'
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
accessFilename = "access.log"
|
accessFilename = "access.log"
|
||||||
errorFilename = "error.log"
|
errorFilename = "error.log"
|
||||||
@@ -65,6 +75,7 @@ var (
|
|||||||
timeFormat = "2006-01-02T15:04:05.000Z07"
|
timeFormat = "2006-01-02T15:04:05.000Z07"
|
||||||
writeConsole bool
|
writeConsole bool
|
||||||
logLevel uint32
|
logLevel uint32
|
||||||
|
encoding = jsonEncodingType
|
||||||
// use uint32 for atomic operations
|
// use uint32 for atomic operations
|
||||||
disableStat uint32
|
disableStat uint32
|
||||||
infoLog io.WriteCloser
|
infoLog io.WriteCloser
|
||||||
@@ -124,6 +135,12 @@ func SetUp(c LogConf) error {
|
|||||||
if len(c.TimeFormat) > 0 {
|
if len(c.TimeFormat) > 0 {
|
||||||
timeFormat = c.TimeFormat
|
timeFormat = c.TimeFormat
|
||||||
}
|
}
|
||||||
|
switch c.Encoding {
|
||||||
|
case plainEncoding:
|
||||||
|
encoding = plainEncodingType
|
||||||
|
default:
|
||||||
|
encoding = jsonEncodingType
|
||||||
|
}
|
||||||
|
|
||||||
switch c.Mode {
|
switch c.Mode {
|
||||||
case consoleMode:
|
case consoleMode:
|
||||||
@@ -407,21 +424,31 @@ func infoTextSync(msg string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func outputAny(writer io.Writer, level string, val interface{}) {
|
func outputAny(writer io.Writer, level string, val interface{}) {
|
||||||
info := logEntry{
|
switch encoding {
|
||||||
Timestamp: getTimestamp(),
|
case plainEncodingType:
|
||||||
Level: level,
|
writePlainAny(writer, level, val)
|
||||||
Content: val,
|
default:
|
||||||
|
info := logEntry{
|
||||||
|
Timestamp: getTimestamp(),
|
||||||
|
Level: level,
|
||||||
|
Content: val,
|
||||||
|
}
|
||||||
|
outputJson(writer, info)
|
||||||
}
|
}
|
||||||
outputJson(writer, info)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func outputText(writer io.Writer, level, msg string) {
|
func outputText(writer io.Writer, level, msg string) {
|
||||||
info := logEntry{
|
switch encoding {
|
||||||
Timestamp: getTimestamp(),
|
case plainEncodingType:
|
||||||
Level: level,
|
writePlainText(writer, level, msg)
|
||||||
Content: msg,
|
default:
|
||||||
|
info := logEntry{
|
||||||
|
Timestamp: getTimestamp(),
|
||||||
|
Level: level,
|
||||||
|
Content: msg,
|
||||||
|
}
|
||||||
|
outputJson(writer, info)
|
||||||
}
|
}
|
||||||
outputJson(writer, info)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func outputError(writer io.Writer, msg string, callDepth int) {
|
func outputError(writer io.Writer, msg string, callDepth int) {
|
||||||
@@ -565,6 +592,62 @@ func statSync(msg string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func writePlainAny(writer io.Writer, level string, val interface{}, fields ...string) {
|
||||||
|
switch v := val.(type) {
|
||||||
|
case string:
|
||||||
|
writePlainText(writer, level, v, fields...)
|
||||||
|
case error:
|
||||||
|
writePlainText(writer, level, v.Error(), fields...)
|
||||||
|
case fmt.Stringer:
|
||||||
|
writePlainText(writer, level, v.String(), fields...)
|
||||||
|
default:
|
||||||
|
var buf bytes.Buffer
|
||||||
|
buf.WriteString(getTimestamp())
|
||||||
|
buf.WriteByte(plainEncodingSep)
|
||||||
|
buf.WriteString(level)
|
||||||
|
for _, item := range fields {
|
||||||
|
buf.WriteByte(plainEncodingSep)
|
||||||
|
buf.WriteString(item)
|
||||||
|
}
|
||||||
|
buf.WriteByte(plainEncodingSep)
|
||||||
|
if err := json.NewEncoder(&buf).Encode(val); err != nil {
|
||||||
|
log.Println(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
buf.WriteByte('\n')
|
||||||
|
if atomic.LoadUint32(&initialized) == 0 || writer == nil {
|
||||||
|
log.Println(buf.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := writer.Write(buf.Bytes()); err != nil {
|
||||||
|
log.Println(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writePlainText(writer io.Writer, level, msg string, fields ...string) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
buf.WriteString(getTimestamp())
|
||||||
|
buf.WriteByte(plainEncodingSep)
|
||||||
|
buf.WriteString(level)
|
||||||
|
for _, item := range fields {
|
||||||
|
buf.WriteByte(plainEncodingSep)
|
||||||
|
buf.WriteString(item)
|
||||||
|
}
|
||||||
|
buf.WriteByte(plainEncodingSep)
|
||||||
|
buf.WriteString(msg)
|
||||||
|
buf.WriteByte('\n')
|
||||||
|
if atomic.LoadUint32(&initialized) == 0 || writer == nil {
|
||||||
|
log.Println(buf.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := writer.Write(buf.Bytes()); err != nil {
|
||||||
|
log.Println(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type logWriter struct {
|
type logWriter struct {
|
||||||
logger *log.Logger
|
logger *log.Logger
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,6 +141,78 @@ func TestStructedLogInfov(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStructedLogInfoConsoleAny(t *testing.T) {
|
||||||
|
doTestStructedLogConsole(t, func(writer io.WriteCloser) {
|
||||||
|
infoLog = writer
|
||||||
|
}, func(v ...interface{}) {
|
||||||
|
old := encoding
|
||||||
|
encoding = plainEncodingType
|
||||||
|
defer func() {
|
||||||
|
encoding = old
|
||||||
|
}()
|
||||||
|
|
||||||
|
Infov(v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStructedLogInfoConsoleAnyString(t *testing.T) {
|
||||||
|
doTestStructedLogConsole(t, func(writer io.WriteCloser) {
|
||||||
|
infoLog = writer
|
||||||
|
}, func(v ...interface{}) {
|
||||||
|
old := encoding
|
||||||
|
encoding = plainEncodingType
|
||||||
|
defer func() {
|
||||||
|
encoding = old
|
||||||
|
}()
|
||||||
|
|
||||||
|
Infov(fmt.Sprint(v...))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStructedLogInfoConsoleAnyError(t *testing.T) {
|
||||||
|
doTestStructedLogConsole(t, func(writer io.WriteCloser) {
|
||||||
|
infoLog = writer
|
||||||
|
}, func(v ...interface{}) {
|
||||||
|
old := encoding
|
||||||
|
encoding = plainEncodingType
|
||||||
|
defer func() {
|
||||||
|
encoding = old
|
||||||
|
}()
|
||||||
|
|
||||||
|
Infov(errors.New(fmt.Sprint(v...)))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStructedLogInfoConsoleAnyStringer(t *testing.T) {
|
||||||
|
doTestStructedLogConsole(t, func(writer io.WriteCloser) {
|
||||||
|
infoLog = writer
|
||||||
|
}, func(v ...interface{}) {
|
||||||
|
old := encoding
|
||||||
|
encoding = plainEncodingType
|
||||||
|
defer func() {
|
||||||
|
encoding = old
|
||||||
|
}()
|
||||||
|
|
||||||
|
Infov(ValStringer{
|
||||||
|
val: fmt.Sprint(v...),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStructedLogInfoConsoleText(t *testing.T) {
|
||||||
|
doTestStructedLogConsole(t, func(writer io.WriteCloser) {
|
||||||
|
infoLog = writer
|
||||||
|
}, func(v ...interface{}) {
|
||||||
|
old := encoding
|
||||||
|
encoding = plainEncodingType
|
||||||
|
defer func() {
|
||||||
|
encoding = old
|
||||||
|
}()
|
||||||
|
|
||||||
|
Info(fmt.Sprint(v...))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestStructedLogSlow(t *testing.T) {
|
func TestStructedLogSlow(t *testing.T) {
|
||||||
doTestStructedLog(t, levelSlow, func(writer io.WriteCloser) {
|
doTestStructedLog(t, levelSlow, func(writer io.WriteCloser) {
|
||||||
slowLog = writer
|
slowLog = writer
|
||||||
@@ -432,6 +504,17 @@ func doTestStructedLog(t *testing.T, level string, setup func(writer io.WriteClo
|
|||||||
assert.True(t, strings.Contains(val, message))
|
assert.True(t, strings.Contains(val, message))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func doTestStructedLogConsole(t *testing.T, setup func(writer io.WriteCloser),
|
||||||
|
write func(...interface{})) {
|
||||||
|
const message = "hello there"
|
||||||
|
writer := new(mockWriter)
|
||||||
|
setup(writer)
|
||||||
|
atomic.StoreUint32(&initialized, 1)
|
||||||
|
write(message)
|
||||||
|
println(writer.String())
|
||||||
|
assert.True(t, strings.Contains(writer.String(), message))
|
||||||
|
}
|
||||||
|
|
||||||
func testSetLevelTwiceWithMode(t *testing.T, mode string) {
|
func testSetLevelTwiceWithMode(t *testing.T, mode string) {
|
||||||
SetUp(LogConf{
|
SetUp(LogConf{
|
||||||
Mode: mode,
|
Mode: mode,
|
||||||
@@ -456,3 +539,11 @@ func testSetLevelTwiceWithMode(t *testing.T, mode string) {
|
|||||||
ErrorStackf(message)
|
ErrorStackf(message)
|
||||||
assert.Equal(t, 0, writer.builder.Len())
|
assert.Equal(t, 0, writer.builder.Len())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ValStringer struct {
|
||||||
|
val string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v ValStringer) String() string {
|
||||||
|
return v.val
|
||||||
|
}
|
||||||
|
|||||||
@@ -77,16 +77,24 @@ func (l *traceLogger) WithDuration(duration time.Duration) Logger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *traceLogger) write(writer io.Writer, level string, val interface{}) {
|
func (l *traceLogger) write(writer io.Writer, level string, val interface{}) {
|
||||||
outputJson(writer, &traceLogger{
|
traceID := traceIdFromContext(l.ctx)
|
||||||
logEntry: logEntry{
|
spanID := spanIdFromContext(l.ctx)
|
||||||
Timestamp: getTimestamp(),
|
|
||||||
Level: level,
|
switch encoding {
|
||||||
Duration: l.Duration,
|
case plainEncodingType:
|
||||||
Content: val,
|
writePlainAny(writer, level, val, l.Duration, traceID, spanID)
|
||||||
},
|
default:
|
||||||
Trace: traceIdFromContext(l.ctx),
|
outputJson(writer, &traceLogger{
|
||||||
Span: spanIdFromContext(l.ctx),
|
logEntry: logEntry{
|
||||||
})
|
Timestamp: getTimestamp(),
|
||||||
|
Level: level,
|
||||||
|
Duration: l.Duration,
|
||||||
|
Content: val,
|
||||||
|
},
|
||||||
|
Trace: traceID,
|
||||||
|
Span: spanID,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithContext sets ctx to log, for keeping tracing information.
|
// WithContext sets ctx to log, for keeping tracing information.
|
||||||
|
|||||||
@@ -82,6 +82,37 @@ func TestTraceInfo(t *testing.T) {
|
|||||||
assert.True(t, strings.Contains(buf.String(), spanKey))
|
assert.True(t, strings.Contains(buf.String(), spanKey))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTraceInfoConsole(t *testing.T) {
|
||||||
|
old := encoding
|
||||||
|
encoding = plainEncodingType
|
||||||
|
defer func() {
|
||||||
|
encoding = old
|
||||||
|
}()
|
||||||
|
|
||||||
|
var buf mockWriter
|
||||||
|
atomic.StoreUint32(&initialized, 1)
|
||||||
|
infoLog = newLogWriter(log.New(&buf, "", flags))
|
||||||
|
otp := otel.GetTracerProvider()
|
||||||
|
tp := sdktrace.NewTracerProvider(sdktrace.WithSampler(sdktrace.AlwaysSample()))
|
||||||
|
otel.SetTracerProvider(tp)
|
||||||
|
defer otel.SetTracerProvider(otp)
|
||||||
|
|
||||||
|
ctx, _ := tp.Tracer("foo").Start(context.Background(), "bar")
|
||||||
|
l := WithContext(ctx).(*traceLogger)
|
||||||
|
SetLevel(InfoLevel)
|
||||||
|
l.WithDuration(time.Second).Info(testlog)
|
||||||
|
assert.True(t, strings.Contains(buf.String(), traceIdFromContext(ctx)))
|
||||||
|
assert.True(t, strings.Contains(buf.String(), spanIdFromContext(ctx)))
|
||||||
|
buf.Reset()
|
||||||
|
l.WithDuration(time.Second).Infof(testlog)
|
||||||
|
assert.True(t, strings.Contains(buf.String(), traceIdFromContext(ctx)))
|
||||||
|
assert.True(t, strings.Contains(buf.String(), spanIdFromContext(ctx)))
|
||||||
|
buf.Reset()
|
||||||
|
l.WithDuration(time.Second).Infov(testlog)
|
||||||
|
assert.True(t, strings.Contains(buf.String(), traceIdFromContext(ctx)))
|
||||||
|
assert.True(t, strings.Contains(buf.String(), spanIdFromContext(ctx)))
|
||||||
|
}
|
||||||
|
|
||||||
func TestTraceSlow(t *testing.T) {
|
func TestTraceSlow(t *testing.T) {
|
||||||
var buf mockWriter
|
var buf mockWriter
|
||||||
atomic.StoreUint32(&initialized, 1)
|
atomic.StoreUint32(&initialized, 1)
|
||||||
|
|||||||
@@ -160,9 +160,9 @@ func dumpRequest(r *http.Request) string {
|
|||||||
func logBrief(r *http.Request, code int, timer *utils.ElapsedTimer, logs *internal.LogCollector) {
|
func logBrief(r *http.Request, code int, timer *utils.ElapsedTimer, logs *internal.LogCollector) {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
duration := timer.Duration()
|
duration := timer.Duration()
|
||||||
logger := logx.WithContext(r.Context())
|
logger := logx.WithContext(r.Context()).WithDuration(duration)
|
||||||
buf.WriteString(fmt.Sprintf("[HTTP] %s - %d - %s - %s - %s - %s",
|
buf.WriteString(fmt.Sprintf("[HTTP] %s - %d - %s - %s - %s",
|
||||||
r.Method, code, r.RequestURI, httpx.GetRemoteAddr(r), r.UserAgent(), timex.ReprOfDuration(duration)))
|
r.Method, code, r.RequestURI, httpx.GetRemoteAddr(r), r.UserAgent()))
|
||||||
if duration > slowThreshold.Load() {
|
if duration > slowThreshold.Load() {
|
||||||
logger.Slowf("[HTTP] %s - %d - %s - %s - %s - slowcall(%s)",
|
logger.Slowf("[HTTP] %s - %d - %s - %s - %s - slowcall(%s)",
|
||||||
r.Method, code, r.RequestURI, httpx.GetRemoteAddr(r), r.UserAgent(), timex.ReprOfDuration(duration))
|
r.Method, code, r.RequestURI, httpx.GetRemoteAddr(r), r.UserAgent(), timex.ReprOfDuration(duration))
|
||||||
|
|||||||
Reference in New Issue
Block a user