Repository: zeromicro/go-zero
Pull request #2401 (2022-09-16 15:44:22)

feat: support caller skip in logx


Patch:
diff --git a/core/logx/contextlogger.go b/core/logx/contextlogger.go
deleted file mode 100644
index 2aec407c..00000000
--- a/core/logx/contextlogger.go
+++ /dev/null
@@ -1,145 +0,0 @@
-package logx
-
-import (
-	"context"
-	"fmt"
-	"time"
-
-	"github.com/zeromicro/go-zero/core/timex"
-	"go.opentelemetry.io/otel/trace"
-)
-
-// WithContext sets ctx to log, for keeping tracing information.
-func WithContext(ctx context.Context) Logger {
-	return &contextLogger{
-		ctx: ctx,
-	}
-}
-
-type contextLogger struct {
-	logEntry
-	ctx context.Context
-}
-
-func (l *contextLogger) Error(v ...interface{}) {
-	l.err(fmt.Sprint(v...))
-}
-
-func (l *contextLogger) Errorf(format string, v ...interface{}) {
-	l.err(fmt.Sprintf(format, v...))
-}
-
-func (l *contextLogger) Errorv(v interface{}) {
-	l.err(fmt.Sprint(v))
-}
-
-func (l *contextLogger) Errorw(msg string, fields ...LogField) {
-	l.err(msg, fields...)
-}
-
-func (l *contextLogger) Info(v ...interface{}) {
-	l.info(fmt.Sprint(v...))
-}
-
-func (l *contextLogger) Infof(format string, v ...interface{}) {
-	l.info(fmt.Sprintf(format, v...))
-}
-
-func (l *contextLogger) Infov(v interface{}) {
-	l.info(v)
-}
-
-func (l *contextLogger) Infow(msg string, fields ...LogField) {
-	l.info(msg, fields...)
-}
-
-func (l *contextLogger) Slow(v ...interface{}) {
-	l.slow(fmt.Sprint(v...))
-}
-
-func (l *contextLogger) Slowf(format string, v ...interface{}) {
-	l.slow(fmt.Sprintf(format, v...))
-}
-
-func (l *contextLogger) Slowv(v interface{}) {
-	l.slow(v)
-}
-
-func (l *contextLogger) Sloww(msg string, fields ...LogField) {
-	l.slow(msg, fields...)
-}
-
-func (l *contextLogger) WithContext(ctx context.Context) Logger {
-	if ctx == nil {
-		return l
-	}
-
-	l.ctx = ctx
-	return l
-}
-
-func (l *contextLogger) WithDuration(duration time.Duration) Logger {
-	l.Duration = timex.ReprOfDuration(duration)
-	return l
-}
-
-func (l *contextLogger) buildFields(fields ...LogField) []LogField {
-	if len(l.Duration) > 0 {
-		fields = append(fields, Field(durationKey, l.Duration))
-	}
-
-	traceID := traceIdFromContext(l.ctx)
-	if len(traceID) > 0 {
-		fields = append(fields, Field(traceKey, traceID))
-	}
-
-	spanID := spanIdFromContext(l.ctx)
-	if len(spanID) > 0 {
-		fields = append(fields, Field(spanKey, spanID))
-	}
-
-	val := l.ctx.Value(fieldsContextKey)
-	if val != nil {
-		if arr, ok := val.([]LogField); ok {
-			fields = append(fields, arr...)
-		}
-	}
-
-	return fields
-}
-
-func (l *contextLogger) err(v interface{}, fields ...LogField) {
-	if shallLog(ErrorLevel) {
-		getWriter().Error(v, l.buildFields(fields...)...)
-	}
-}
-
-func (l *contextLogger) info(v interface{}, fields ...LogField) {
-	if shallLog(InfoLevel) {
-		getWriter().Info(v, l.buildFields(fields...)...)
-	}
-}
-
-func (l *contextLogger) slow(v interface{}, fields ...LogField) {
-	if shallLog(ErrorLevel) {
-		getWriter().Slow(v, l.buildFields(fields...)...)
-	}
-}
-
-func spanIdFromContext(ctx context.Context) string {
-	spanCtx := trace.SpanContextFromContext(ctx)
-	if spanCtx.HasSpanID() {
-		return spanCtx.SpanID().String()
-	}
-
-	return ""
-}
-
-func traceIdFromContext(ctx context.Context) string {
-	spanCtx := trace.SpanContextFromContext(ctx)
-	if spanCtx.HasTraceID() {
-		return spanCtx.TraceID().String()
-	}
-
-	return ""
-}
diff --git a/core/logx/durationlogger.go b/core/logx/durationlogger.go
deleted file mode 100644
index a9c3e500..00000000
--- a/core/logx/durationlogger.go
+++ /dev/null
@@ -1,101 +0,0 @@
-package logx
-
-import (
-	"context"
-	"fmt"
-	"time"
-
-	"github.com/zeromicro/go-zero/core/timex"
-)
-
-// WithDuration returns a Logger which logs the given duration.
-func WithDuration(d time.Duration) Logger {
-	return &durationLogger{
-		Duration: timex.ReprOfDuration(d),
-	}
-}
-
-type durationLogger logEntry
-
-func (l *durationLogger) Error(v ...interface{}) {
-	l.err(fmt.Sprint(v...))
-}
-
-func (l *durationLogger) Errorf(format string, v ...interface{}) {
-	l.err(fmt.Sprintf(format, v...))
-}
-
-func (l *durationLogger) Errorv(v interface{}) {
-	l.err(v)
-}
-
-func (l *durationLogger) Errorw(msg string, fields ...LogField) {
-	l.err(msg, fields...)
-}
-
-func (l *durationLogger) Info(v ...interface{}) {
-	l.info(fmt.Sprint(v...))
-}
-
-func (l *durationLogger) Infof(format string, v ...interface{}) {
-	l.info(fmt.Sprintf(format, v...))
-}
-
-func (l *durationLogger) Infov(v interface{}) {
-	l.info(v)
-}
-
-func (l *durationLogger) Infow(msg string, fields ...LogField) {
-	l.info(msg, fields...)
-}
-
-func (l *durationLogger) Slow(v ...interface{}) {
-	l.slow(fmt.Sprint(v...))
-}
-
-func (l *durationLogger) Slowf(format string, v ...interface{}) {
-	l.slow(fmt.Sprintf(format, v...))
-}
-
-func (l *durationLogger) Slowv(v interface{}) {
-	l.slow(v)
-}
-
-func (l *durationLogger) Sloww(msg string, fields ...LogField) {
-	l.slow(msg, fields...)
-}
-
-func (l *durationLogger) WithContext(ctx context.Context) Logger {
-	return &contextLogger{
-		ctx: ctx,
-		logEntry: logEntry{
-			Duration: l.Duration,
-		},
-	}
-}
-
-func (l *durationLogger) WithDuration(duration time.Duration) Logger {
-	l.Duration = timex.ReprOfDuration(duration)
-	return l
-}
-
-func (l *durationLogger) err(v interface{}, fields ...LogField) {
-	if shallLog(ErrorLevel) {
-		fields = append(fields, Field(durationKey, l.Duration))
-		getWriter().Error(v, fields...)
-	}
-}
-
-func (l *durationLogger) info(v interface{}, fields ...LogField) {
-	if shallLog(InfoLevel) {
-		fields = append(fields, Field(durationKey, l.Duration))
-		getWriter().Info(v, fields...)
-	}
-}
-
-func (l *durationLogger) slow(v interface{}, fields ...LogField) {
-	if shallLog(ErrorLevel) {
-		fields = append(fields, Field(durationKey, l.Duration))
-		getWriter().Slow(v, fields...)
-	}
-}
diff --git a/core/logx/fields.go b/core/logx/fields.go
index 91958720..08bcf636 100644
--- a/core/logx/fields.go
+++ b/core/logx/fields.go
@@ -6,8 +6,8 @@ var fieldsContextKey contextKey
 
 type contextKey struct{}
 
-// WithFields returns a new context with the given fields.
-func WithFields(ctx context.Context, fields ...LogField) context.Context {
+// ContextWithFields returns a new context with the given fields.
+func ContextWithFields(ctx context.Context, fields ...LogField) context.Context {
 	if val := ctx.Value(fieldsContextKey); val != nil {
 		if arr, ok := val.([]LogField); ok {
 			return context.WithValue(ctx, fieldsContextKey, append(arr, fields...))
@@ -16,3 +16,9 @@ func WithFields(ctx context.Context, fields ...LogField) context.Context {
 
 	return context.WithValue(ctx, fieldsContextKey, fields)
 }
+
+// WithFields returns a new logger with the given fields.
+// deprecated: use ContextWithFields instead.
+func WithFields(ctx context.Context, fields ...LogField) context.Context {
+	return ContextWithFields(ctx, fields...)
+}
diff --git a/core/logx/logger.go b/core/logx/logger.go
index 2d48af47..ca690766 100644
--- a/core/logx/logger.go
+++ b/core/logx/logger.go
@@ -31,8 +31,10 @@ type Logger interface {
 	Slowv(interface{})
 	// Sloww logs a message at slow level.
 	Sloww(string, ...LogField)
+	// WithCallerSkip returns a new logger with the given caller skip.
+	WithCallerSkip(skip int) Logger
 	// WithContext returns a new logger with the given context.
-	WithContext(context.Context) Logger
+	WithContext(ctx context.Context) Logger
 	// WithDuration returns a new logger with the given duration.
-	WithDuration(time.Duration) Logger
+	WithDuration(d time.Duration) Logger
 }
diff --git a/core/logx/logs.go b/core/logx/logs.go
index 325c9370..28fd5c06 100644
--- a/core/logx/logs.go
+++ b/core/logx/logs.go
@@ -14,7 +14,7 @@ import (
 	"github.com/zeromicro/go-zero/core/sysx"
 )
 
-const callerDepth = 5
+const callerDepth = 4
 
 var (
 	timeFormat = "2006-01-02T15:04:05.000Z07:00"
@@ -29,15 +29,16 @@ var (
 )
 
 type (
-	logEntry struct {
-		Timestamp string      `json:"@timestamp"`
-		Level     string      `json:"level"`
-		Duration  string      `json:"duration,omitempty"`
-		Caller    string      `json:"caller,omitempty"`
-		Content   interface{} `json:"content"`
+	// LogField is a key-value pair that will be added to the log entry.
+	LogField struct {
+		Key   string
+		Value interface{}
 	}
 
-	logEntryWithFields map[string]interface{}
+	// LogOption defines the method to customize the logging.
+	LogOption func(options *logOptions)
+
+	logEntry map[string]interface{}
 
 	logOptions struct {
 		gzipEnabled           bool
@@ -47,15 +48,6 @@ type (
 		maxSize               int
 		rotationRule          string
 	}
-
-	// LogField is a key-value pair that will be added to the log entry.
-	LogField struct {
-		Key   string
-		Value interface{}
-	}
-
-	// LogOption defines the method to customize the logging.
-	LogOption func(options *logOptions)
 )
 
 // Alert alerts v in alert level, and the message is written to error log.
@@ -85,35 +77,35 @@ func DisableStat() {
 
 // Error writes v into error log.
 func Error(v ...interface{}) {
-	errorTextSync(fmt.Sprint(v...))
+	writeError(fmt.Sprint(v...))
 }
 
 // Errorf writes v with format into error log.
 func Errorf(format string, v ...interface{}) {
-	errorTextSync(fmt.Errorf(format, v...).Error())
+	writeError(fmt.Errorf(format, v...).Error())
 }
 
 // ErrorStack writes v along with call stack into error log.
 func ErrorStack(v ...interface{}) {
 	// there is newline in stack string
-	stackSync(fmt.Sprint(v...))
+	writeStack(fmt.Sprint(v...))
 }
 
 // ErrorStackf writes v along with call stack in format into error log.
 func ErrorStackf(format string, v ...interface{}) {
 	// there is newline in stack string
-	stackSync(fmt.Sprintf(format, v...))
+	writeStack(fmt.Sprintf(format, v...))
 }
 
 // Errorv writes v into error log with json content.
 // No call stack attached, because not elegant to pack the messages.
 func Errorv(v interface{}) {
-	errorAnySync(v)
+	writeError(v)
 }
 
 // Errorw writes msg along with fields into error log.
 func Errorw(msg string, fields ...LogField) {
-	errorFieldsSync(msg, fields...)
+	writeError(msg, fields...)
 }
 
 // Field returns a LogField for the given key and value.
@@ -156,22 +148,22 @@ func Field(key string, value interface{}) LogField {
 
 // Info writes v into access log.
 func Info(v ...interface{}) {
-	infoTextSync(fmt.Sprint(v...))
+	writeInfo(fmt.Sprint(v...))
 }
 
 // Infof writes v with format into access log.
 func Infof(format string, v ...interface{}) {
-	infoTextSync(fmt.Sprintf(format, v...))
+	writeInfo(fmt.Sprintf(format, v...))
 }
 
 // Infov writes v into access log with json content.
 func Infov(v interface{}) {
-	infoAnySync(v)
+	writeInfo(v)
 }
 
 // Infow writes msg along with fields into access log.
 func Infow(msg string, fields ...LogField) {
-	infoFieldsSync(msg, fields...)
+	writeInfo(msg, fields...)
 }
 
 // Must checks if err is nil, otherwise logs the error and exits.
@@ -244,42 +236,42 @@ func SetUp(c LogConf) (err error) {
 
 // Severe writes v into severe log.
 func Severe(v ...interface{}) {
-	severeSync(fmt.Sprint(v...))
+	writeSevere(fmt.Sprint(v...))
 }
 
 // Severef writes v with format into severe log.
 func Severef(format string, v ...interface{}) {
-	severeSync(fmt.Sprintf(format, v...))
+	writeSevere(fmt.Sprintf(format, v...))
 }
 
 // Slow writes v into slow log.
 func Slow(v ...interface{}) {
-	slowTextSync(fmt.Sprint(v...))
+	writeSlow(fmt.Sprint(v...))
 }
 
 // Slowf writes v with format into slow log.
 func Slowf(format string, v ...interface{}) {
-	slowTextSync(fmt.Sprintf(format, v...))
+	writeSlow(fmt.Sprintf(format, v...))
 }
 
 // Slowv writes v into slow log with json content.
 func Slowv(v interface{}) {
-	slowAnySync(v)
+	writeSlow(v)
 }
 
 // Sloww writes msg along with fields into slow log.
 func Sloww(msg string, fields ...LogField) {
-	slowFieldsSync(msg, fields...)
+	writeSlow(msg, fields...)
 }
 
 // Stat writes v into stat log.
 func Stat(v ...interface{}) {
-	statSync(fmt.Sprint(v...))
+	writeStat(fmt.Sprint(v...))
 }
 
 // Statf writes v with format into stat log.
 func Statf(format string, v ...interface{}) {
-	statSync(fmt.Sprintf(format, v...))
+	writeStat(fmt.Sprintf(format, v...))
 }
 
 // WithCooldownMillis customizes logging on writing call stack interval.
@@ -324,6 +316,10 @@ func WithRotation(r string) LogOption {
 	}
 }
 
+func addCaller(fields ...LogField) []LogField {
+	return append(fields, Field(callerKey, getCaller(callerDepth)))
+}
+
 func createOutput(path string) (io.WriteCloser, error) {
 	if len(path) == 0 {
 		return nil, ErrLogPathNotSet
@@ -339,24 +335,6 @@ func createOutput(path string) (io.WriteCloser, error) {
 	}
 }
 
-func errorAnySync(v interface{}) {
-	if shallLog(ErrorLevel) {
-		getWriter().Error(v)
-	}
-}
-
-func errorFieldsSync(content string, fields ...LogField) {
-	if shallLog(ErrorLevel) {
-		getWriter().Error(content, fields...)
-	}
-}
-
-func errorTextSync(msg string) {
-	if shallLog(ErrorLevel) {
-		getWriter().Error(msg)
-	}
-}
-
 func getWriter() Writer {
 	w := writer.Load()
 	if w == nil {
@@ -372,24 +350,6 @@ func handleOptions(opts []LogOption) {
 	}
 }
 
-func infoAnySync(val interface{}) {
-	if shallLog(InfoLevel) {
-		getWriter().Info(val)
-	}
-}
-
-func infoFieldsSync(content string, fields ...LogField) {
-	if shallLog(InfoLevel) {
-		getWriter().Info(content, fields...)
-	}
-}
-
-func infoTextSync(msg string) {
-	if shallLog(InfoLevel) {
-		getWriter().Info(msg)
-	}
-}
-
 func setupLogLevel(c LogConf) {
 	switch c.Level {
 	case levelInfo:
@@ -424,12 +384,6 @@ func setupWithVolume(c LogConf) error {
 	return setupWithFiles(c)
 }
 
-func severeSync(msg string) {
-	if shallLog(SevereLevel) {
-		getWriter().Severe(fmt.Sprintf("%s\n%s", msg, string(debug.Stack())))
-	}
-}
-
 func shallLog(level uint32) bool {
 	return atomic.LoadUint32(&logLevel) <= level
 }
@@ -438,32 +392,38 @@ func shallLogStat() bool {
 	return atomic.LoadUint32(&disableStat) == 0
 }
 
-func slowAnySync(v interface{}) {
+func writeError(val interface{}, fields ...LogField) {
 	if shallLog(ErrorLevel) {
-		getWriter().Slow(v)
+		getWriter().Error(val, addCaller(fields...)...)
 	}
 }
 
-func slowFieldsSync(content string, fields ...LogField) {
-	if shallLog(ErrorLevel) {
-		getWriter().Slow(content, fields...)
+func writeInfo(val interface{}, fields ...LogField) {
+	if shallLog(InfoLevel) {
+		getWriter().Info(val, addCaller(fields...)...)
+	}
+}
+
+func writeSevere(msg string) {
+	if shallLog(SevereLevel) {
+		getWriter().Severe(fmt.Sprintf("%s\n%s", msg, string(debug.Stack())))
 	}
 }
 
-func slowTextSync(msg string) {
+func writeSlow(val interface{}, fields ...LogField) {
 	if shallLog(ErrorLevel) {
-		getWriter().Slow(msg)
+		getWriter().Slow(val, addCaller(fields...)...)
 	}
 }
 
-func stackSync(msg string) {
+func writeStack(msg string) {
 	if shallLog(ErrorLevel) {
 		getWriter().Stack(fmt.Sprintf("%s\n%s", msg, string(debug.Stack())))
 	}
 }
 
-func statSync(msg string) {
+func writeStat(msg string) {
 	if shallLogStat() && shallLog(InfoLevel) {
-		getWriter().Stat(msg)
+		getWriter().Stat(msg, addCaller()...)
 	}
 }
diff --git a/core/logx/richlogger.go b/core/logx/richlogger.go
new file mode 100644
index 00000000..de52a80e
--- /dev/null
+++ b/core/logx/richlogger.go
@@ -0,0 +1,172 @@
+package logx
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"github.com/zeromicro/go-zero/core/timex"
+	"go.opentelemetry.io/otel/trace"
+)
+
+// WithCallerSkip returns a Logger with given caller skip.
+func WithCallerSkip(skip int) Logger {
+	if skip <= 0 {
+		return new(richLogger)
+	}
+
+	return &richLogger{
+		callerSkip: skip,
+	}
+}
+
+// WithContext sets ctx to log, for keeping tracing information.
+func WithContext(ctx context.Context) Logger {
+	return &richLogger{
+		ctx: ctx,
+	}
+}
+
+// WithDuration returns a Logger with given duration.
+func WithDuration(d time.Duration) Logger {
+	return &richLogger{
+		fields: []LogField{Field(durationKey, timex.ReprOfDuration(d))},
+	}
+}
+
+type richLogger struct {
+	ctx        context.Context
+	callerSkip int
+	fields     []LogField
+}
+
+func (l *richLogger) Error(v ...interface{}) {
+	l.err(fmt.Sprint(v...))
+}
+
+func (l *richLogger) Errorf(format string, v ...interface{}) {
+	l.err(fmt.Sprintf(format, v...))
+}
+
+func (l *richLogger) Errorv(v interface{}) {
+	l.err(fmt.Sprint(v))
+}
+
+func (l *richLogger) Errorw(msg string, fields ...LogField) {
+	l.err(msg, fields...)
+}
+
+func (l *richLogger) Info(v ...interface{}) {
+	l.info(fmt.Sprint(v...))
+}
+
+func (l *richLogger) Infof(format string, v ...interface{}) {
+	l.info(fmt.Sprintf(format, v...))
+}
+
+func (l *richLogger) Infov(v interface{}) {
+	l.info(v)
+}
+
+func (l *richLogger) Infow(msg string, fields ...LogField) {
+	l.info(msg, fields...)
+}
+
+func (l *richLogger) Slow(v ...interface{}) {
+	l.slow(fmt.Sprint(v...))
+}
+
+func (l *richLogger) Slowf(format string, v ...interface{}) {
+	l.slow(fmt.Sprintf(format, v...))
+}
+
+func (l *richLogger) Slowv(v interface{}) {
+	l.slow(v)
+}
+
+func (l *richLogger) Sloww(msg string, fields ...LogField) {
+	l.slow(msg, fields...)
+}
+
+func (l *richLogger) WithCallerSkip(skip int) Logger {
+	if skip <= 0 {
+		return l
+	}
+
+	l.callerSkip = skip
+	return l
+}
+
+func (l *richLogger) WithContext(ctx context.Context) Logger {
+	l.ctx = ctx
+	return l
+}
+
+func (l *richLogger) WithDuration(duration time.Duration) Logger {
+	l.fields = append(l.fields, Field(durationKey, timex.ReprOfDuration(duration)))
+	return l
+}
+
+func (l *richLogger) buildFields(fields ...LogField) []LogField {
+	fields = append(l.fields, fields...)
+	fields = append(fields, Field(callerKey, getCaller(callerDepth+l.callerSkip)))
+
+	if l.ctx == nil {
+		return fields
+	}
+
+	traceID := traceIdFromContext(l.ctx)
+	if len(traceID) > 0 {
+		fields = append(fields, Field(traceKey, traceID))
+	}
+
+	spanID := spanIdFromContext(l.ctx)
+	if len(spanID) > 0 {
+		fields = append(fields, Field(spanKey, spanID))
+	}
+
+	val := l.ctx.Value(fieldsContextKey)
+	if val != nil {
+		if arr, ok := val.([]LogField); ok {
+			fields = append(fields, arr...)
+		}
+	}
+
+	return fields
+}
+
+func (l *richLogger) err(v interface{}, fields ...LogField) {
+	if shallLog(ErrorLevel) {
+		getWriter().Error(v, l.buildFields(fields...)...)
+	}
+}
+
+func (l *richLogger) info(v interface{}, fields ...LogField) {
+	if shallLog(InfoLevel) {
+		getWriter().Info(v, l.buildFields(fields...)...)
+	}
+}
+
+func (l *richLogger) slow(v interface{}, fields ...LogField) {
+	if shallLog(ErrorLevel) {
+		getWriter().Slow(v, l.buildFields(fields...)...)
+	}
+}
+
+func spanIdFromContext(ctx context.Context) string {
+	spanCtx := trace.SpanContextFromContext(ctx)
+	if spanCtx.HasSpanID() {
+		return spanCtx.SpanID().String()
+	}
+
+	return ""
+}
+
+func traceIdFromContext(ctx context.Context) string {
+	spanCtx := trace.SpanContextFromContext(ctx)
+	if spanCtx.HasTraceID() {
+		return spanCtx.TraceID().String()
+	}
+
+	return ""
+}
diff --git a/core/logx/writer.go b/core/logx/writer.go
index 08873c09..5f372566 100644
--- a/core/logx/writer.go
+++ b/core/logx/writer.go
@@ -256,13 +256,11 @@ func buildFields(fields ...LogField) []string {
 }
 
 func output(writer io.Writer, level string, val interface{}, fields ...LogField) {
-	fields = append(fields, Field(callerKey, getCaller(callerDepth)))
-
 	switch atomic.LoadUint32(&encoding) {
 	case plainEncodingType:
 		writePlainAny(writer, level, val, buildFields(fields...)...)
 	default:
-		entry := make(logEntryWithFields)
+		entry := make(logEntry)
 		for _, field := range fields {
 			entry[field.Key] = field.Value
 		}
diff --git a/core/metric/counter.go b/core/metric/counter.go
index 5344773c..c95412f7 100644
--- a/core/metric/counter.go
+++ b/core/metric/counter.go
@@ -3,6 +3,7 @@ package metric
 import (
 	prom "github.com/prometheus/client_golang/prometheus"
 	"github.com/zeromicro/go-zero/core/proc"
+	"github.com/zeromicro/go-zero/core/prometheus"
 )
 
 type (
@@ -47,10 +48,18 @@ func NewCounterVec(cfg *CounterVecOpts) CounterVec {
 }
 
 func (cv *promCounterVec) Inc(labels ...string) {
+	if !prometheus.Enabled() {
+		return
+	}
+
 	cv.counter.WithLabelValues(labels...).Inc()
 }
 
 func (cv *promCounterVec) Add(v float64, labels ...string) {
+	if !prometheus.Enabled() {
+		return
+	}
+
 	cv.counter.WithLabelValues(labels...).Add(v)
 }
 
diff --git a/core/metric/gauge.go b/core/metric/gauge.go
index b4452983..52ff841d 100644
--- a/core/metric/gauge.go
+++ b/core/metric/gauge.go
@@ -3,6 +3,7 @@ package metric
 import (
 	prom "github.com/prometheus/client_golang/prometheus"
 	"github.com/zeromicro/go-zero/core/proc"
+	"github.com/zeromicro/go-zero/core/prometheus"
 )
 
 type (
@@ -50,14 +51,26 @@ func NewGaugeVec(cfg *GaugeVecOpts) GaugeVec {
 }
 
 func (gv *promGaugeVec) Inc(labels ...string) {
+	if !prometheus.Enabled() {
+		return
+	}
+
 	gv.gauge.WithLabelValues(labels...).Inc()
 }
 
 func (gv *promGaugeVec) Add(v float64, labels ...string) {
+	if !prometheus.Enabled() {
+		return
+	}
+
 	gv.gauge.WithLabelValues(labels...).Add(v)
 }
 
 func (gv *promGaugeVec) Set(v float64, labels ...string) {
+	if !prometheus.Enabled() {
+		return
+	}
+
 	gv.gauge.WithLabelValues(labels...).Set(v)
 }
 
diff --git a/core/metric/histogram.go b/core/metric/histogram.go
index 2053f6bb..4f0a1058 100644
--- a/core/metric/histogram.go
+++ b/core/metric/histogram.go
@@ -3,6 +3,7 @@ package metric
 import (
 	prom "github.com/prometheus/client_golang/prometheus"
 	"github.com/zeromicro/go-zero/core/proc"
+	"github.com/zeromicro/go-zero/core/prometheus"
 )
 
 type (
@@ -53,6 +54,10 @@ func NewHistogramVec(cfg *HistogramVecOpts) HistogramVec {
 }
 
 func (hv *promHistogramVec) Observe(v int64, labels ...string) {
+	if !prometheus.Enabled() {
+		return
+	}
+
 	hv.histogram.WithLabelValues(labels...).Observe(float64(v))
 }
 
diff --git a/core/stores/redis/hook.go b/core/stores/redis/hook.go
index 6cd0ab42..5229bd27 100644
--- a/core/stores/redis/hook.go
+++ b/core/stores/redis/hook.go
@@ -56,6 +56,11 @@ func (h hook) AfterProcess(ctx context.Context, cmd red.Cmder) error {
 		logDuration(ctx, cmd, duration)
 	}
 
+	metricReqDur.Observe(int64(duration/time.Millisecond), cmd.Name())
+	if msg := formatError(err); len(msg) > 0 {
+		metricReqErr.Inc(cmd.Name(), msg)
+	}
+
 	return nil
 }
 
@@ -98,9 +103,43 @@ func (h hook) AfterProcessPipeline(ctx context.Context, cmds []red.Cmder) error
 		logDuration(ctx, cmds[0], duration)
 	}
 
+	metricReqDur.Observe(int64(duration/time.Millisecond), "Pipeline")
+	if msg := formatError(batchError.Err()); len(msg) > 0 {
+		metricReqErr.Inc("Pipeline", msg)
+	}
+
 	return nil
 }
 
+func formatError(err error) string {
+	if err == nil || err == red.Nil {
+		return ""
+	}
+
+	es := err.Error()
+	switch {
+	case strings.HasPrefix(es, "read"):
+		return "read timeout"
+	case strings.HasPrefix(es, "dial"):
+		if strings.Contains(es, "connection refused") {
+			return "connection refused"
+		}
+		return "dial timeout"
+	case strings.HasPrefix(es, "write"):
+		return "write timeout"
+	case strings.Contains(es, "EOF"):
+		return "eof"
+	case strings.Contains(es, "reset"):
+		return "reset"
+	case strings.Contains(es, "broken"):
+		return "broken pipe"
+	case strings.Contains(es, "breaker"):
+		return "breaker"
+	default:
+		return "unexpected error"
+	}
+}
+
 func logDuration(ctx context.Context, cmd red.Cmder, duration time.Duration) {
 	var buf strings.Builder
 	for i, arg := range cmd.Args() {
diff --git a/core/stores/redis/metrics.go b/core/stores/redis/metrics.go
new file mode 100644
index 00000000..2cb381f4
--- /dev/null
+++ b/core/stores/redis/metrics.go
@@ -0,0 +1,23 @@
+package redis
+
+import "github.com/zeromicro/go-zero/core/metric"
+
+const namespace = "redis_client"
+
+var (
+	metricReqDur = metric.NewHistogramVec(&metric.HistogramVecOpts{
+		Namespace: namespace,
+		Subsystem: "requests",
+		Name:      "duration_ms",
+		Help:      "redis client requests duration(ms).",
+		Labels:    []string{"command"},
+		Buckets:   []float64{5, 10, 25, 50, 100, 250, 500, 1000, 2500},
+	})
+	metricReqErr = metric.NewCounterVec(&metric.CounterVecOpts{
+		Namespace: namespace,
+		Subsystem: "requests",
+		Name:      "error_total",
+		Help:      "redis client requests error count.",
+		Labels:    []string{"command", "error"},
+	})
+)
diff --git a/core/stores/sqlx/metrics.go b/core/stores/sqlx/metrics.go
new file mode 100644
index 00000000..7c478012
--- /dev/null
+++ b/core/stores/sqlx/metrics.go
@@ -0,0 +1,23 @@
+package sqlx
+
+import "github.com/zeromicro/go-zero/core/metric"
+
+const namespace = "sql_client"
+
+var (
+	metricReqDur = metric.NewHistogramVec(&metric.HistogramVecOpts{
+		Namespace: namespace,
+		Subsystem: "requests",
+		Name:      "durations_ms",
+		Help:      "mysql client requests duration(ms).",
+		Labels:    []string{"command"},
+		Buckets:   []float64{5, 10, 25, 50, 100, 250, 500, 1000, 2500},
+	})
+	metricReqErr = metric.NewCounterVec(&metric.CounterVecOpts{
+		Namespace: namespace,
+		Subsystem: "requests",
+		Name:      "error_total",
+		Help:      "mysql client requests error count.",
+		Labels:    []string{"command", "error"},
+	})
+)
diff --git a/core/stores/sqlx/sqlconn.go b/core/stores/sqlx/sqlconn.go
index c3e0269d..0118a38d 100644
--- a/core/stores/sqlx/sqlconn.go
+++ b/core/stores/sqlx/sqlconn.go
@@ -153,6 +153,9 @@ func (db *commonSqlConn) ExecCtx(ctx context.Context, q string, args ...interfac
 		result, err = exec(ctx, conn, q, args...)
 		return err
 	}, db.acceptable)
+	if err == breaker.ErrServiceUnavailable {
+		metricReqErr.Inc("Exec", "breaker")
+	}
 
 	return
 }
@@ -186,6 +189,9 @@ func (db *commonSqlConn) PrepareCtx(ctx context.Context, query string) (stmt Stm
 		}
 		return nil
 	}, db.acceptable)
+	if err == breaker.ErrServiceUnavailable {
+		metricReqErr.Inc("Prepare", "breaker")
+	}
 
 	return
 }
@@ -270,9 +276,14 @@ func (db *commonSqlConn) TransactCtx(ctx context.Context, fn func(context.Contex
 		endSpan(span, err)
 	}()
 
-	return db.brk.DoWithAcceptable(func() error {
+	err = db.brk.DoWithAcceptable(func() error {
 		return transact(ctx, db, db.beginTx, fn)
 	}, db.acceptable)
+	if err == breaker.ErrServiceUnavailable {
+		metricReqErr.Inc("Transact", "breaker")
+	}
+
+	return
 }
 
 func (db *commonSqlConn) acceptable(err error) bool {
@@ -287,7 +298,7 @@ func (db *commonSqlConn) acceptable(err error) bool {
 func (db *commonSqlConn) queryRows(ctx context.Context, scanner func(*sql.Rows) error,
 	q string, args ...interface{}) (err error) {
 	var qerr error
-	return db.brk.DoWithAcceptable(func() error {
+	err = db.brk.DoWithAcceptable(func() error {
 		conn, err := db.connProv()
 		if err != nil {
 			db.onError(err)
@@ -301,6 +312,11 @@ func (db *commonSqlConn) queryRows(ctx context.Context, scanner func(*sql.Rows)
 	}, func(err error) bool {
 		return qerr == err || db.acceptable(err)
 	})
+	if err == breaker.ErrServiceUnavailable {
+		metricReqErr.Inc("queryRows", "breaker")
+	}
+
+	return
 }
 
 func (s statement) Close() error {
diff --git a/core/stores/sqlx/stmt.go b/core/stores/sqlx/stmt.go
index 7b6ad26b..35796b1b 100644
--- a/core/stores/sqlx/stmt.go
+++ b/core/stores/sqlx/stmt.go
@@ -135,6 +135,8 @@ func (e *realSqlGuard) finish(ctx context.Context, err error) {
 	if err != nil {
 		logSqlError(ctx, e.stmt, err)
 	}
+
+	metricReqDur.Observe(int64(duration/time.Millisecond), e.command)
 }
 
 func (e *realSqlGuard) start(q string, args ...interface{}) error {
diff --git a/go.mod b/go.mod
index 2698b7ae..b8eaf4b9 100644
--- a/go.mod
+++ b/go.mod
@@ -27,7 +27,7 @@ require (
 	go.mongodb.org/mongo-driver v1.10.2
 	go.opentelemetry.io/otel v1.10.0
 	go.opentelemetry.io/otel/exporters/jaeger v1.10.0
-	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.9.0
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.10.0
 	go.opentelemetry.io/otel/exporters/zipkin v1.10.0
 	go.opentelemetry.io/otel/sdk v1.10.0
 	go.opentelemetry.io/otel/trace v1.10.0
diff --git a/go.sum b/go.sum
index 24fd2f00..7f1d7eeb 100644
--- a/go.sum
+++ b/go.sum
@@ -484,29 +484,26 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opentelemetry.io/otel v1.7.0/go.mod h1:5BdUoMIz5WEs0vt0CUEMtSSaTSHBBVwrhnz7+nrD5xk=
-go.opentelemetry.io/otel v1.9.0/go.mod h1:np4EoPGzoPs3O67xUVNoPPcmSvsfOxNlNA4F4AC+0Eo=
 go.opentelemetry.io/otel v1.10.0 h1:Y7DTJMR6zs1xkS/upamJYk0SxxN4C9AqRd77jmZnyY4=
 go.opentelemetry.io/otel v1.10.0/go.mod h1:NbvWjCthWHKBEUMpf0/v8ZRZlni86PpGFEMA9pnQSnQ=
 go.opentelemetry.io/otel/exporters/jaeger v1.10.0 h1:7W3aVVjEYayu/GOqOVF4mbTvnCuxF1wWu3eRxFGQXvw=
 go.opentelemetry.io/otel/exporters/jaeger v1.10.0/go.mod h1:n9IGyx0fgyXXZ/i0foLHNxtET9CzXHzZeKCucvRBFgA=
-go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.9.0 h1:ggqApEjDKczicksfvZUCxuvoyDmR6Sbm56LwiK8DVR0=
-go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.9.0/go.mod h1:78XhIg8Ht9vR4tbLNUhXsiOnE2HOuSeKAiAcoVQEpOY=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.9.0 h1:NN90Cuna0CnBg8YNu1Q0V35i2E8LDByFOwHRCq/ZP9I=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.9.0/go.mod h1:0EsCXjZAiiZGnLdEUXM9YjCKuuLZMYyglh2QDXcYKVA=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.9.0 h1:M0/hqGuJBLeIEu20f89H74RGtqV2dn+SFWEz9ATAAwY=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.9.0/go.mod h1:K5G92gbtCrYJ0mn6zj9Pst7YFsDFuvSYEhYKRMcufnM=
+go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.10.0 h1:TaB+1rQhddO1sF71MpZOZAuSPW1klK2M8XxfrBMfK7Y=
+go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.10.0/go.mod h1:78XhIg8Ht9vR4tbLNUhXsiOnE2HOuSeKAiAcoVQEpOY=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.10.0 h1:pDDYmo0QadUPal5fwXoY1pmMpFcdyhXOmL5drCrI3vU=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.10.0/go.mod h1:Krqnjl22jUJ0HgMzw5eveuCvFDXY4nSYb4F8t5gdrag=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.10.0 h1:KtiUEhQmj/Pa874bVYKGNVdq8NPKiacPbaRRtgXi+t4=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.10.0/go.mod h1:OfUCyyIiDvNXHWpcWgbF+MWvqPZiNa3YDEnivcnYsV0=
 go.opentelemetry.io/otel/exporters/zipkin v1.10.0 h1:HcPAFsFpEBKF+G5NIOA+gBsxifd3Ej+wb+KsdBLa15E=
 go.opentelemetry.io/otel/exporters/zipkin v1.10.0/go.mod h1:HdfvgwcOoCB0+zzrTHycW6btjK0zNpkz2oTGO815SCI=
-go.opentelemetry.io/otel/sdk v1.9.0/go.mod h1:AEZc8nt5bd2F7BC24J5R0mrjYnpEgYHyTcM/vrSple4=
 go.opentelemetry.io/otel/sdk v1.10.0 h1:jZ6K7sVn04kk/3DNUdJ4mqRlGDiXAVuIG+MMENpTNdY=
 go.opentelemetry.io/otel/sdk v1.10.0/go.mod h1:vO06iKzD5baltJz1zarxMCNHFpUlUiOy4s65ECtn6kE=
 go.opentelemetry.io/otel/trace v1.7.0/go.mod h1:fzLSB9nqR2eXzxPXb2JW9IKE+ScyXA48yyE4TNvoHqU=
-go.opentelemetry.io/otel/trace v1.9.0/go.mod h1:2737Q0MuG8q1uILYm2YYVkAyLtOofiTNGg6VODnOiPo=
 go.opentelemetry.io/otel/trace v1.10.0 h1:npQMbR8o7mum8uF95yFbOEJffhs1sbCOfDh8zAJiH5E=
 go.opentelemetry.io/otel/trace v1.10.0/go.mod h1:Sij3YYczqAdz+EhmGhE6TpTxUO5/F/AzrK+kxfGqySM=
 go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
-go.opentelemetry.io/proto/otlp v0.18.0 h1:W5hyXNComRa23tGpKwG+FRAc4rfF6ZUg1JReK+QHS80=
-go.opentelemetry.io/proto/otlp v0.18.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
+go.opentelemetry.io/proto/otlp v0.19.0 h1:IVN6GR+mhC4s5yfcTbmzHYODqvWAp3ZedA2SJPI1Nnw=
+go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
 go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
 go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
diff --git a/rest/handler/prometheushandler.go b/rest/handler/prometheushandler.go
index 639d59c0..fcafa0a1 100644
--- a/rest/handler/prometheushandler.go
+++ b/rest/handler/prometheushandler.go
@@ -6,7 +6,6 @@ import (
 	"time"
 
 	"github.com/zeromicro/go-zero/core/metric"
-	"github.com/zeromicro/go-zero/core/prometheus"
 	"github.com/zeromicro/go-zero/core/timex"
 	"github.com/zeromicro/go-zero/rest/internal/response"
 )
@@ -35,10 +34,6 @@ var (
 // PrometheusHandler returns a middleware that reports stats to prometheus.
 func PrometheusHandler(path string) func(http.Handler) http.Handler {
 	return func(next http.Handler) http.Handler {
-		if !prometheus.Enabled() {
-			return next
-		}
-
 		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 			startTime := timex.Now()
 			cw := &response.WithCodeResponseWriter{Writer: w}
diff --git a/zrpc/internal/clientinterceptors/prometheusinterceptor.go b/zrpc/internal/clientinterceptors/prometheusinterceptor.go
index b713f09f..7b50144a 100644
--- a/zrpc/internal/clientinterceptors/prometheusinterceptor.go
+++ b/zrpc/internal/clientinterceptors/prometheusinterceptor.go
@@ -6,7 +6,6 @@ import (
 	"time"
 
 	"github.com/zeromicro/go-zero/core/metric"
-	"github.com/zeromicro/go-zero/core/prometheus"
 	"github.com/zeromicro/go-zero/core/timex"
 	"google.golang.org/grpc"
 	"google.golang.org/grpc/status"
@@ -36,10 +35,6 @@ var (
 // PrometheusInterceptor is an interceptor that reports to prometheus server.
 func PrometheusInterceptor(ctx context.Context, method string, req, reply interface{},
 	cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
-	if !prometheus.Enabled() {
-		return invoker(ctx, method, req, reply, cc, opts...)
-	}
-
 	startTime := timex.Now()
 	err := invoker(ctx, method, req, reply, cc, opts...)
 	metricClientReqDur.Observe(int64(timex.Since(startTime)/time.Millisecond), method)
diff --git a/zrpc/internal/serverinterceptors/prometheusinterceptor.go b/zrpc/internal/serverinterceptors/prometheusinterceptor.go
index 86070e38..e6cce1a4 100644
--- a/zrpc/internal/serverinterceptors/prometheusinterceptor.go
+++ b/zrpc/internal/serverinterceptors/prometheusinterceptor.go
@@ -6,7 +6,6 @@ import (
 	"time"
 
 	"github.com/zeromicro/go-zero/core/metric"
-	"github.com/zeromicro/go-zero/core/prometheus"
 	"github.com/zeromicro/go-zero/core/timex"
 	"google.golang.org/grpc"
 	"google.golang.org/grpc/status"
@@ -36,10 +35,6 @@ var (
 // UnaryPrometheusInterceptor reports the statistics to the prometheus server.
 func UnaryPrometheusInterceptor(ctx context.Context, req interface{},
 	info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
-	if !prometheus.Enabled() {
-		return handler(ctx, req)
-	}
-
 	startTime := timex.Now()
 	resp, err := handler(ctx, req)
 	metricServerReqDur.Observe(int64(timex.Since(startTime)/time.Millisecond), info.FullMethod)


Test patch:
diff --git a/core/logx/durationlogger_test.go b/core/logx/durationlogger_test.go
deleted file mode 100644
index b1f1af0d..00000000
--- a/core/logx/durationlogger_test.go
+++ /dev/null
@@ -1,161 +0,0 @@
-package logx
-
-import (
-	"context"
-	"strings"
-	"sync/atomic"
-	"testing"
-	"time"
-
-	"github.com/stretchr/testify/assert"
-	"go.opentelemetry.io/otel"
-	sdktrace "go.opentelemetry.io/otel/sdk/trace"
-)
-
-func TestWithDurationError(t *testing.T) {
-	w := new(mockWriter)
-	old := writer.Swap(w)
-	defer writer.Store(old)
-
-	WithDuration(time.Second).Error("foo")
-	assert.True(t, strings.Contains(w.String(), "duration"), w.String())
-}
-
-func TestWithDurationErrorf(t *testing.T) {
-	w := new(mockWriter)
-	old := writer.Swap(w)
-	defer writer.Store(old)
-
-	WithDuration(time.Second).Errorf("foo")
-	assert.True(t, strings.Contains(w.String(), "duration"), w.String())
-}
-
-func TestWithDurationErrorv(t *testing.T) {
-	w := new(mockWriter)
-	old := writer.Swap(w)
-	defer writer.Store(old)
-
-	WithDuration(time.Second).Errorv("foo")
-	assert.True(t, strings.Contains(w.String(), "duration"), w.String())
-}
-
-func TestWithDurationErrorw(t *testing.T) {
-	w := new(mockWriter)
-	old := writer.Swap(w)
-	defer writer.Store(old)
-
-	WithDuration(time.Second).Errorw("foo", Field("foo", "bar"))
-	assert.True(t, strings.Contains(w.String(), "duration"), w.String())
-	assert.True(t, strings.Contains(w.String(), "foo"), w.String())
-	assert.True(t, strings.Contains(w.String(), "bar"), w.String())
-}
-
-func TestWithDurationInfo(t *testing.T) {
-	w := new(mockWriter)
-	old := writer.Swap(w)
-	defer writer.Store(old)
-
-	WithDuration(time.Second).Info("foo")
-	assert.True(t, strings.Contains(w.String(), "duration"), w.String())
-}
-
-func TestWithDurationInfoConsole(t *testing.T) {
-	old := atomic.LoadUint32(&encoding)
-	atomic.StoreUint32(&encoding, plainEncodingType)
-	defer func() {
-		atomic.StoreUint32(&encoding, old)
-	}()
-
-	w := new(mockWriter)
-	o := writer.Swap(w)
-	defer writer.Store(o)
-
-	WithDuration(time.Second).Info("foo")
-	assert.True(t, strings.Contains(w.String(), "ms"), w.String())
-}
-
-func TestWithDurationInfof(t *testing.T) {
-	w := new(mockWriter)
-	old := writer.Swap(w)
-	defer writer.Store(old)
-
-	WithDuration(time.Second).Infof("foo")
-	assert.True(t, strings.Contains(w.String(), "duration"), w.String())
-}
-
-func TestWithDurationInfov(t *testing.T) {
-	w := new(mockWriter)
-	old := writer.Swap(w)
-	defer writer.Store(old)
-
-	WithDuration(time.Second).Infov("foo")
-	assert.True(t, strings.Contains(w.String(), "duration"), w.String())
-}
-
-func TestWithDurationInfow(t *testing.T) {
-	w := new(mockWriter)
-	old := writer.Swap(w)
-	defer writer.Store(old)
-
-	WithDuration(time.Second).Infow("foo", Field("foo", "bar"))
-	assert.True(t, strings.Contains(w.String(), "duration"), w.String())
-	assert.True(t, strings.Contains(w.String(), "foo"), w.String())
-	assert.True(t, strings.Contains(w.String(), "bar"), w.String())
-}
-
-func TestWithDurationWithContextInfow(t *testing.T) {
-	w := new(mockWriter)
-	old := writer.Swap(w)
-	defer writer.Store(old)
-
-	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")
-	WithDuration(time.Second).WithContext(ctx).Infow("foo", Field("foo", "bar"))
-	assert.True(t, strings.Contains(w.String(), "duration"), w.String())
-	assert.True(t, strings.Contains(w.String(), "foo"), w.String())
-	assert.True(t, strings.Contains(w.String(), "bar"), w.String())
-	assert.True(t, strings.Contains(w.String(), "trace"), w.String())
-	assert.True(t, strings.Contains(w.String(), "span"), w.String())
-}
-
-func TestWithDurationSlow(t *testing.T) {
-	w := new(mockWriter)
-	old := writer.Swap(w)
-	defer writer.Store(old)
-
-	WithDuration(time.Second).Slow("foo")
-	assert.True(t, strings.Contains(w.String(), "duration"), w.String())
-}
-
-func TestWithDurationSlowf(t *testing.T) {
-	w := new(mockWriter)
-	old := writer.Swap(w)
-	defer writer.Store(old)
-
-	WithDuration(time.Second).WithDuration(time.Hour).Slowf("foo")
-	assert.True(t, strings.Contains(w.String(), "duration"), w.String())
-}
-
-func TestWithDurationSlowv(t *testing.T) {
-	w := new(mockWriter)
-	old := writer.Swap(w)
-	defer writer.Store(old)
-
-	WithDuration(time.Second).WithDuration(time.Hour).Slowv("foo")
-	assert.True(t, strings.Contains(w.String(), "duration"), w.String())
-}
-
-func TestWithDurationSloww(t *testing.T) {
-	w := new(mockWriter)
-	old := writer.Swap(w)
-	defer writer.Store(old)
-
-	WithDuration(time.Second).WithDuration(time.Hour).Sloww("foo", Field("foo", "bar"))
-	assert.True(t, strings.Contains(w.String(), "duration"), w.String())
-	assert.True(t, strings.Contains(w.String(), "foo"), w.String())
-	assert.True(t, strings.Contains(w.String(), "bar"), w.String())
-}
diff --git a/core/logx/fields_test.go b/core/logx/fields_test.go
index c447526f..91f137b9 100644
--- a/core/logx/fields_test.go
+++ b/core/logx/fields_test.go
@@ -7,8 +7,8 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
-func TestWithFields(t *testing.T) {
-	ctx := WithFields(context.Background(), Field("a", 1), Field("b", 2))
+func TestContextWithFields(t *testing.T) {
+	ctx := ContextWithFields(context.Background(), Field("a", 1), Field("b", 2))
 	vals := ctx.Value(fieldsContextKey)
 	assert.NotNil(t, vals)
 	fields, ok := vals.([]LogField)
@@ -19,8 +19,8 @@ func TestWithFields(t *testing.T) {
 func TestWithFieldsAppend(t *testing.T) {
 	var dummyKey struct{}
 	ctx := context.WithValue(context.Background(), dummyKey, "dummy")
-	ctx = WithFields(ctx, Field("a", 1), Field("b", 2))
-	ctx = WithFields(ctx, Field("c", 3), Field("d", 4))
+	ctx = ContextWithFields(ctx, Field("a", 1), Field("b", 2))
+	ctx = ContextWithFields(ctx, Field("c", 3), Field("d", 4))
 	vals := ctx.Value(fieldsContextKey)
 	assert.NotNil(t, vals)
 	fields, ok := vals.([]LogField)
diff --git a/core/logx/logs_test.go b/core/logx/logs_test.go
index 718e7438..3be37648 100644
--- a/core/logx/logs_test.go
+++ b/core/logx/logs_test.go
@@ -461,13 +461,13 @@ func TestStructedLogWithDuration(t *testing.T) {
 	defer writer.Store(old)
 
 	WithDuration(time.Second).Info(message)
-	var entry logEntry
+	var entry map[string]interface{}
 	if err := json.Unmarshal([]byte(w.String()), &entry); err != nil {
 		t.Error(err)
 	}
-	assert.Equal(t, levelInfo, entry.Level)
-	assert.Equal(t, message, entry.Content)
-	assert.Equal(t, "1000.0ms", entry.Duration)
+	assert.Equal(t, levelInfo, entry[levelKey])
+	assert.Equal(t, message, entry[contentKey])
+	assert.Equal(t, "1000.0ms", entry[durationKey])
 }
 
 func TestSetLevel(t *testing.T) {
@@ -531,6 +531,7 @@ func TestSetup(t *testing.T) {
 	MustSetup(LogConf{
 		ServiceName: "any",
 		Mode:        "console",
+		TimeFormat:  timeFormat,
 	})
 	MustSetup(LogConf{
 		ServiceName: "any",
@@ -553,7 +554,15 @@ func TestSetup(t *testing.T) {
 		Encoding:    plainEncoding,
 	})
 
+	defer os.RemoveAll("CD01CB7D-2705-4F3F-889E-86219BF56F10")
 	assert.NotNil(t, setupWithVolume(LogConf{}))
+	assert.Nil(t, setupWithVolume(LogConf{
+		ServiceName: "CD01CB7D-2705-4F3F-889E-86219BF56F10",
+	}))
+	assert.Nil(t, setupWithVolume(LogConf{
+		ServiceName: "CD01CB7D-2705-4F3F-889E-86219BF56F10",
+		Rotation:    sizeRotationRule,
+	}))
 	assert.NotNil(t, setupWithFiles(LogConf{}))
 	assert.Nil(t, setupWithFiles(LogConf{
 		ServiceName: "any",
@@ -583,6 +592,8 @@ func TestDisable(t *testing.T) {
 	var opt logOptions
 	WithKeepDays(1)(&opt)
 	WithGzip()(&opt)
+	WithMaxBackups(1)(&opt)
+	WithMaxSize(1024)(&opt)
 	assert.Nil(t, Close())
 	assert.Nil(t, Close())
 }
@@ -711,14 +722,16 @@ func put(b []byte) {
 func doTestStructedLog(t *testing.T, level string, w *mockWriter, write func(...interface{})) {
 	const message = "hello there"
 	write(message)
-	var entry logEntry
+
+	var entry map[string]interface{}
 	if err := json.Unmarshal([]byte(w.String()), &entry); err != nil {
 		t.Error(err)
 	}
-	assert.Equal(t, level, entry.Level)
-	val, ok := entry.Content.(string)
+
+	assert.Equal(t, level, entry[levelKey])
+	val, ok := entry[contentKey]
 	assert.True(t, ok)
-	assert.True(t, strings.Contains(val, message))
+	assert.True(t, strings.Contains(val.(string), message))
 }
 
 func doTestStructedLogConsole(t *testing.T, w *mockWriter, write func(...interface{})) {
diff --git a/core/logx/contextlogger_test.go b/core/logx/richlogger_test.go
similarity index 91%
rename from core/logx/contextlogger_test.go
rename to core/logx/richlogger_test.go
index 8f931481..08777752 100644
--- a/core/logx/contextlogger_test.go
+++ b/core/logx/richlogger_test.go
@@ -3,6 +3,7 @@ package logx
 import (
 	"context"
 	"encoding/json"
+	"fmt"
 	"io"
 	"strings"
 	"sync/atomic"
@@ -201,7 +202,7 @@ func TestLogWithFields(t *testing.T) {
 		writer.Store(old)
 	}()
 
-	ctx := WithFields(context.Background(), Field("foo", "bar"))
+	ctx := ContextWithFields(context.Background(), Field("foo", "bar"))
 	l := WithContext(ctx)
 	SetLevel(InfoLevel)
 	l.Info(testlog)
@@ -211,6 +212,31 @@ func TestLogWithFields(t *testing.T) {
 	assert.Equal(t, "bar", val.Foo)
 }
 
+func TestLogWithCallerSkip(t *testing.T) {
+	w := new(mockWriter)
+	old := writer.Swap(w)
+	writer.lock.RLock()
+	defer func() {
+		writer.lock.RUnlock()
+		writer.Store(old)
+	}()
+
+	l := WithCallerSkip(1).WithCallerSkip(0)
+	p := func(v string) {
+		l.Info(v)
+	}
+
+	file, line := getFileLine()
+	p(testlog)
+	assert.True(t, w.Contains(fmt.Sprintf("%s:%d", file, line+1)))
+
+	w.Reset()
+	l = WithCallerSkip(0).WithCallerSkip(1)
+	file, line = getFileLine()
+	p(testlog)
+	assert.True(t, w.Contains(fmt.Sprintf("%s:%d", file, line+1)))
+}
+
 func validate(t *testing.T, body string, expectedTrace, expectedSpan bool) {
 	var val mockValue
 	dec := json.NewDecoder(strings.NewReader(body))
diff --git a/core/logx/syslog_test.go b/core/logx/syslog_test.go
index 7e389093..2be0b51a 100644
--- a/core/logx/syslog_test.go
+++ b/core/logx/syslog_test.go
@@ -42,11 +42,18 @@ func captureOutput(f func()) string {
 }
 
 func getContent(jsonStr string) string {
-	var entry logEntry
+	var entry map[string]interface{}
 	json.Unmarshal([]byte(jsonStr), &entry)
-	val, ok := entry.Content.(string)
-	if ok {
-		return val
+
+	val, ok := entry[contentKey]
+	if !ok {
+		return ""
+	}
+
+	str, ok := val.(string)
+	if !ok {
+		return ""
 	}
-	return ""
+
+	return str
 }
diff --git a/core/metric/counter_test.go b/core/metric/counter_test.go
index d8612fd6..145d1b20 100644
--- a/core/metric/counter_test.go
+++ b/core/metric/counter_test.go
@@ -5,6 +5,7 @@ import (
 
 	"github.com/prometheus/client_golang/prometheus/testutil"
 	"github.com/stretchr/testify/assert"
+	"github.com/zeromicro/go-zero/core/prometheus"
 )
 
 func TestNewCounterVec(t *testing.T) {
@@ -21,6 +22,7 @@ func TestNewCounterVec(t *testing.T) {
 }
 
 func TestCounterIncr(t *testing.T) {
+	startAgent()
 	counterVec := NewCounterVec(&CounterVecOpts{
 		Namespace: "http_client",
 		Subsystem: "call",
@@ -37,6 +39,7 @@ func TestCounterIncr(t *testing.T) {
 }
 
 func TestCounterAdd(t *testing.T) {
+	startAgent()
 	counterVec := NewCounterVec(&CounterVecOpts{
 		Namespace: "rpc_server",
 		Subsystem: "requests",
@@ -51,3 +54,11 @@ func TestCounterAdd(t *testing.T) {
 	r := testutil.ToFloat64(cv.counter)
 	assert.Equal(t, float64(33), r)
 }
+
+func startAgent() {
+	prometheus.StartAgent(prometheus.Config{
+		Host: "127.0.0.1",
+		Port: 9101,
+		Path: "/metrics",
+	})
+}
diff --git a/core/metric/gauge_test.go b/core/metric/gauge_test.go
index 534889fb..ad3ea549 100644
--- a/core/metric/gauge_test.go
+++ b/core/metric/gauge_test.go
@@ -21,6 +21,7 @@ func TestNewGaugeVec(t *testing.T) {
 }
 
 func TestGaugeInc(t *testing.T) {
+	startAgent()
 	gaugeVec := NewGaugeVec(&GaugeVecOpts{
 		Namespace: "rpc_client2",
 		Subsystem: "requests",
@@ -37,6 +38,7 @@ func TestGaugeInc(t *testing.T) {
 }
 
 func TestGaugeAdd(t *testing.T) {
+	startAgent()
 	gaugeVec := NewGaugeVec(&GaugeVecOpts{
 		Namespace: "rpc_client",
 		Subsystem: "request",
@@ -53,6 +55,7 @@ func TestGaugeAdd(t *testing.T) {
 }
 
 func TestGaugeSet(t *testing.T) {
+	startAgent()
 	gaugeVec := NewGaugeVec(&GaugeVecOpts{
 		Namespace: "http_client",
 		Subsystem: "request",
diff --git a/core/metric/histogram_test.go b/core/metric/histogram_test.go
index e8328649..4874617b 100644
--- a/core/metric/histogram_test.go
+++ b/core/metric/histogram_test.go
@@ -21,6 +21,7 @@ func TestNewHistogramVec(t *testing.T) {
 }
 
 func TestHistogramObserve(t *testing.T) {
+	startAgent()
 	histogramVec := NewHistogramVec(&HistogramVecOpts{
 		Name:    "counts",
 		Help:    "rpc server requests duration(ms).",
diff --git a/core/stores/sqlx/sqlconn_test.go b/core/stores/sqlx/sqlconn_test.go
index 3f2723ae..8c68c5e6 100644
--- a/core/stores/sqlx/sqlconn_test.go
+++ b/core/stores/sqlx/sqlconn_test.go
@@ -17,7 +17,8 @@ func init() {
 }
 
 func TestSqlConn(t *testing.T) {
-	mock := buildConn()
+	mock, err := buildConn()
+	assert.Nil(t, err)
 	mock.ExpectExec("any")
 	mock.ExpectQuery("any").WillReturnRows(sqlmock.NewRows([]string{"foo"}))
 	conn := NewMysql(mockedDatasource)
@@ -50,8 +51,8 @@ func TestSqlConn(t *testing.T) {
 	}))
 }
 
-func buildConn() (mock sqlmock.Sqlmock) {
-	connManager.GetResource(mockedDatasource, func() (io.Closer, error) {
+func buildConn() (mock sqlmock.Sqlmock, err error) {
+	_, err = connManager.GetResource(mockedDatasource, func() (io.Closer, error) {
 		var db *sql.DB
 		var err error
 		db, mock, err = sqlmock.New()