Repository: zeromicro/go-zero (go)

Issue:
Feature Request: logx: support Debug level (or addtional logging levels)
**Is your feature request related to a problem? Please describe.**
Many of the widely used external logging libraries in go support multiple logging levels on top of the defaults (info, error, fatal), like debug, trace and others.
- https://pkg.go.dev/go.uber.org/zap#Logger.Debug
- https://github.com/sirupsen/logrus#level-logging
- https://github.com/rs/zerolog#leveled-logging

I think it would be useful to have at least one additional logging level for debugging purposes to hide "code smell".
Use case: 
- log information for regular users using logx.Info()
- log debug information in special cases for developers using logx.Debug() - and enable these logs using a logx_debug configuration/environment variable

**Describe the solution you'd like**
Provide support for Debug logging in logx.

**Describe alternatives you've considered**
Support general implementation of custom logging levels instead of specializing to the debug level. I think this would be a bigger effort and I'm not sure if there's a huge need for this.


Pull request description:
feat: add log debug level
fix #2135


Patch:
diff --git a/core/logx/config.go b/core/logx/config.go
index eb79f4ee..dd96b6eb 100644
--- a/core/logx/config.go
+++ b/core/logx/config.go
@@ -7,7 +7,7 @@ type LogConf struct {
 	Encoding            string `json:",default=json,options=[json,plain]"`
 	TimeFormat          string `json:",optional"`
 	Path                string `json:",default=logs"`
-	Level               string `json:",default=info,options=[info,error,severe]"`
+	Level               string `json:",default=info,options=[debug,info,error,severe]"`
 	Compress            bool   `json:",optional"`
 	KeepDays            int    `json:",optional"`
 	StackCooldownMillis int    `json:",default=100"`
diff --git a/core/logx/logger.go b/core/logx/logger.go
index ca690766..ddb4ada7 100644
--- a/core/logx/logger.go
+++ b/core/logx/logger.go
@@ -7,6 +7,14 @@ import (
 
 // A Logger represents a logger.
 type Logger interface {
+	// Debug logs a message at info level.
+	Debug(...interface{})
+	// Debugf logs a message at info level.
+	Debugf(string, ...interface{})
+	// Debugv logs a message at info level.
+	Debugv(interface{})
+	// Debugw logs a message at info level.
+	Debugw(string, ...LogField)
 	// Error logs a message at error level.
 	Error(...interface{})
 	// Errorf logs a message at error level.
diff --git a/core/logx/logs.go b/core/logx/logs.go
index 28fd5c06..621b1ee3 100644
--- a/core/logx/logs.go
+++ b/core/logx/logs.go
@@ -64,6 +64,26 @@ func Close() error {
 	return nil
 }
 
+// Debug writes v into access log.
+func Debug(v ...interface{}) {
+	writeDebug(fmt.Sprint(v...))
+}
+
+// Debugf writes v with format into access log.
+func Debugf(format string, v ...interface{}) {
+	writeDebug(fmt.Sprintf(format, v...))
+}
+
+// Debugv writes v into access log with json content.
+func Debugv(v interface{}) {
+	writeDebug(v)
+}
+
+// Debugw writes msg along with fields into access log.
+func Debugw(msg string, fields ...LogField) {
+	writeDebug(msg, fields...)
+}
+
 // Disable disables the logging.
 func Disable() {
 	atomic.StoreUint32(&disableLog, 1)
@@ -352,6 +372,8 @@ func handleOptions(opts []LogOption) {
 
 func setupLogLevel(c LogConf) {
 	switch c.Level {
+	case levelDebug:
+		SetLevel(DebugLevel)
 	case levelInfo:
 		SetLevel(InfoLevel)
 	case levelError:
@@ -392,6 +414,12 @@ func shallLogStat() bool {
 	return atomic.LoadUint32(&disableStat) == 0
 }
 
+func writeDebug(val interface{}, fields ...LogField) {
+	if shallLog(DebugLevel) {
+		getWriter().Debug(val, addCaller(fields...)...)
+	}
+}
+
 func writeError(val interface{}, fields ...LogField) {
 	if shallLog(ErrorLevel) {
 		getWriter().Error(val, addCaller(fields...)...)
diff --git a/core/logx/richlogger.go b/core/logx/richlogger.go
index de52a80e..f73fddf9 100644
--- a/core/logx/richlogger.go
+++ b/core/logx/richlogger.go
@@ -40,6 +40,22 @@ type richLogger struct {
 	fields     []LogField
 }
 
+func (l *richLogger) Debug(v ...interface{}) {
+	l.debug(fmt.Sprint(v...))
+}
+
+func (l *richLogger) Debugf(format string, v ...interface{}) {
+	l.debug(fmt.Sprintf(format, v...))
+}
+
+func (l *richLogger) Debugv(v interface{}) {
+	l.debug(v)
+}
+
+func (l *richLogger) Debugw(msg string, fields ...LogField) {
+	l.debug(msg, fields...)
+}
+
 func (l *richLogger) Error(v ...interface{}) {
 	l.err(fmt.Sprint(v...))
 }
@@ -135,6 +151,12 @@ func (l *richLogger) buildFields(fields ...LogField) []LogField {
 	return fields
 }
 
+func (l *richLogger) debug(v interface{}, fields ...LogField) {
+	if shallLog(DebugLevel) {
+		getWriter().Debug(v, l.buildFields(fields...)...)
+	}
+}
+
 func (l *richLogger) err(v interface{}, fields ...LogField) {
 	if shallLog(ErrorLevel) {
 		getWriter().Error(v, l.buildFields(fields...)...)
diff --git a/core/logx/vars.go b/core/logx/vars.go
index 26c4fd84..b95c3215 100644
--- a/core/logx/vars.go
+++ b/core/logx/vars.go
@@ -3,8 +3,10 @@ package logx
 import "errors"
 
 const (
-	// InfoLevel logs everything
-	InfoLevel uint32 = iota
+	// DebugLevel logs everything
+	DebugLevel uint32 = iota
+	// InfoLevel does not include debugs
+	InfoLevel
 	// ErrorLevel includes errors, slows, stacks
 	ErrorLevel
 	// SevereLevel only log severe messages
@@ -37,6 +39,7 @@ const (
 	levelFatal  = "fatal"
 	levelSlow   = "slow"
 	levelStat   = "stat"
+	levelDebug  = "debug"
 
 	backupFileDelimiter = "-"
 	flags               = 0x0
diff --git a/core/logx/writer.go b/core/logx/writer.go
index 5f372566..16e96976 100644
--- a/core/logx/writer.go
+++ b/core/logx/writer.go
@@ -18,6 +18,7 @@ type (
 	Writer interface {
 		Alert(v interface{})
 		Close() error
+		Debug(v interface{}, fields ...LogField)
 		Error(v interface{}, fields ...LogField)
 		Info(v interface{}, fields ...LogField)
 		Severe(v interface{})
@@ -194,6 +195,10 @@ func (w *concreteWriter) Close() error {
 	return w.statLog.Close()
 }
 
+func (w *concreteWriter) Debug(v interface{}, fields ...LogField) {
+	output(w.infoLog, levelDebug, v, fields...)
+}
+
 func (w *concreteWriter) Error(v interface{}, fields ...LogField) {
 	output(w.errorLog, levelError, v, fields...)
 }
@@ -227,6 +232,9 @@ func (n nopWriter) Close() error {
 	return nil
 }
 
+func (n nopWriter) Debug(_ interface{}, _ ...LogField) {
+}
+
 func (n nopWriter) Error(_ interface{}, _ ...LogField) {
 }
 
diff --git a/tools/goctl/api/cmd.go b/tools/goctl/api/cmd.go
index 8bd0e9a7..71667c1b 100644
--- a/tools/goctl/api/cmd.go
+++ b/tools/goctl/api/cmd.go
@@ -43,7 +43,7 @@ var (
 
 	goCmd = &cobra.Command{
 		Use:   "go",
-		Short: "Generate go files for provided api in yaml file",
+		Short: "Generate go files for provided api in api file",
 		RunE:  gogen.GoCommand,
 	}
 
diff --git a/tools/goctl/internal/version/version.go b/tools/goctl/internal/version/version.go
index abb8bb80..2fc7caaf 100644
--- a/tools/goctl/internal/version/version.go
+++ b/tools/goctl/internal/version/version.go
@@ -6,7 +6,7 @@ import (
 )
 
 // BuildVersion is the version of goctl.
-const BuildVersion = "1.4.1"
+const BuildVersion = "1.4.2"
 
 var tag = map[string]int{"pre-alpha": 0, "alpha": 1, "pre-bata": 2, "beta": 3, "released": 4, "": 5}
 
diff --git a/tools/goctl/model/cmd.go b/tools/goctl/model/cmd.go
index 131fe048..4b76cd2b 100644
--- a/tools/goctl/model/cmd.go
+++ b/tools/goctl/model/cmd.go
@@ -93,6 +93,7 @@ func init() {
 	mongoCmd.Flags().StringVar(&mongo.VarStringBranch, "branch", "", "The branch of the remote repo, it does work with --remote")
 
 	mysqlCmd.PersistentFlags().BoolVar(&command.VarBoolStrict, "strict", false, "Generate model in strict mode")
+	mysqlCmd.PersistentFlags().StringSliceVarP(&command.VarStringSliceIgnoreColumns, "ignore-columns", "i", []string{"create_at", "created_at", "create_time", "update_at", "updated_at", "update_time"}, "Ignore columns while creating or updating rows")
 
 	mysqlCmd.AddCommand(datasourceCmd)
 	mysqlCmd.AddCommand(ddlCmd)
diff --git a/tools/goctl/model/sql/command/command.go b/tools/goctl/model/sql/command/command.go
index a12f2a2b..f00b4229 100644
--- a/tools/goctl/model/sql/command/command.go
+++ b/tools/goctl/model/sql/command/command.go
@@ -7,10 +7,11 @@ import (
 
 	"github.com/go-sql-driver/mysql"
 	"github.com/spf13/cobra"
+
+	"github.com/zeromicro/go-zero/core/collection"
 	"github.com/zeromicro/go-zero/core/logx"
 	"github.com/zeromicro/go-zero/core/stores/postgres"
 	"github.com/zeromicro/go-zero/core/stores/sqlx"
-
 	"github.com/zeromicro/go-zero/tools/goctl/config"
 	"github.com/zeromicro/go-zero/tools/goctl/model/sql/command/migrationnotes"
 	"github.com/zeromicro/go-zero/tools/goctl/model/sql/gen"
@@ -50,6 +51,8 @@ var (
 	VarStringBranch string
 	// VarBoolStrict describes whether the strict mode is enabled.
 	VarBoolStrict bool
+	// VarStringSliceIgnoreColumns represents the columns which are ignored.
+	VarStringSliceIgnoreColumns []string
 )
 
 var errNotMatched = errors.New("sql not matched")
@@ -81,13 +84,14 @@ func MysqlDDL(_ *cobra.Command, _ []string) error {
 	}
 
 	arg := ddlArg{
-		src:      src,
-		dir:      dir,
-		cfg:      cfg,
-		cache:    cache,
-		idea:     idea,
-		database: database,
-		strict:   VarBoolStrict,
+		src:           src,
+		dir:           dir,
+		cfg:           cfg,
+		cache:         cache,
+		idea:          idea,
+		database:      database,
+		strict:        VarBoolStrict,
+		ignoreColumns: mergeColumns(VarStringSliceIgnoreColumns),
 	}
 	return fromDDL(arg)
 }
@@ -121,17 +125,29 @@ func MySqlDataSource(_ *cobra.Command, _ []string) error {
 	}
 
 	arg := dataSourceArg{
-		url:      url,
-		dir:      dir,
-		tablePat: patterns,
-		cfg:      cfg,
-		cache:    cache,
-		idea:     idea,
-		strict:   VarBoolStrict,
+		url:           url,
+		dir:           dir,
+		tablePat:      patterns,
+		cfg:           cfg,
+		cache:         cache,
+		idea:          idea,
+		strict:        VarBoolStrict,
+		ignoreColumns: mergeColumns(VarStringSliceIgnoreColumns),
 	}
 	return fromMysqlDataSource(arg)
 }
 
+func mergeColumns(columns []string) []string {
+	set := collection.NewSet()
+	for _, v := range columns {
+		fields := strings.FieldsFunc(v, func(r rune) bool {
+			return r == ','
+		})
+		set.AddStr(fields...)
+	}
+	return set.KeysStr()
+}
+
 type pattern map[string]struct{}
 
 func (p pattern) Match(s string) bool {
@@ -205,11 +221,12 @@ func PostgreSqlDataSource(_ *cobra.Command, _ []string) error {
 }
 
 type ddlArg struct {
-	src, dir    string
-	cfg         *config.Config
-	cache, idea bool
-	database    string
-	strict      bool
+	src, dir      string
+	cfg           *config.Config
+	cache, idea   bool
+	database      string
+	strict        bool
+	ignoreColumns []string
 }
 
 func fromDDL(arg ddlArg) error {
@@ -228,7 +245,8 @@ func fromDDL(arg ddlArg) error {
 		return errNotMatched
 	}
 
-	generator, err := gen.NewDefaultGenerator(arg.dir, arg.cfg, gen.WithConsoleOption(log))
+	generator, err := gen.NewDefaultGenerator(arg.dir, arg.cfg,
+		gen.WithConsoleOption(log), gen.WithIgnoreColumns(arg.ignoreColumns))
 	if err != nil {
 		return err
 	}
@@ -244,11 +262,12 @@ func fromDDL(arg ddlArg) error {
 }
 
 type dataSourceArg struct {
-	url, dir    string
-	tablePat    pattern
-	cfg         *config.Config
-	cache, idea bool
-	strict      bool
+	url, dir      string
+	tablePat      pattern
+	cfg           *config.Config
+	cache, idea   bool
+	strict        bool
+	ignoreColumns []string
 }
 
 func fromMysqlDataSource(arg dataSourceArg) error {
@@ -301,7 +320,8 @@ func fromMysqlDataSource(arg dataSourceArg) error {
 		return errors.New("no tables matched")
 	}
 
-	generator, err := gen.NewDefaultGenerator(arg.dir, arg.cfg, gen.WithConsoleOption(log))
+	generator, err := gen.NewDefaultGenerator(arg.dir, arg.cfg,
+		gen.WithConsoleOption(log), gen.WithIgnoreColumns(arg.ignoreColumns))
 	if err != nil {
 		return err
 	}
diff --git a/tools/goctl/model/sql/example/makefile b/tools/goctl/model/sql/example/makefile
index c08e09ee..99368309 100644
--- a/tools/goctl/model/sql/example/makefile
+++ b/tools/goctl/model/sql/example/makefile
@@ -5,6 +5,10 @@ fromDDLWithCache:
 	goctl template clean
 	goctl model mysql ddl -src="./sql/*.sql" -dir="./sql/model/cache" -cache
 
+fromDDLWithCacheAndIgnoreColumns:
+	goctl template clean
+	goctl model mysql ddl -src="./sql/*.sql" -dir="./sql/model/ignore_columns/cache" -cache -i 'gmt_create,create_at' -i 'gmt_modified,update_at'
+
 fromDDLWithCacheAndDb:
 	goctl template clean
 	goctl model mysql ddl -src="./sql/*.sql" -dir="./sql/model/cache_db" -database="1gozero" -cache
diff --git a/tools/goctl/model/sql/gen/gen.go b/tools/goctl/model/sql/gen/gen.go
index 4b7b3ed1..187653b6 100644
--- a/tools/goctl/model/sql/gen/gen.go
+++ b/tools/goctl/model/sql/gen/gen.go
@@ -26,10 +26,11 @@ type (
 	defaultGenerator struct {
 		console.Console
 		// source string
-		dir          string
-		pkg          string
-		cfg          *config.Config
-		isPostgreSql bool
+		dir           string
+		pkg           string
+		cfg           *config.Config
+		isPostgreSql  bool
+		ignoreColumns []string
 	}
 
 	// Option defines a function with argument defaultGenerator
@@ -82,14 +83,21 @@ func NewDefaultGenerator(dir string, cfg *config.Config, opt ...Option) (*defaul
 	return generator, nil
 }
 
-// WithConsoleOption creates a console option
+// WithConsoleOption creates a console option.
 func WithConsoleOption(c console.Console) Option {
 	return func(generator *defaultGenerator) {
 		generator.Console = c
 	}
 }
 
-// WithPostgreSql marks  defaultGenerator.isPostgreSql true
+// WithIgnoreColumns ignores the columns while insert or update rows.
+func WithIgnoreColumns(ignoreColumns []string) Option {
+	return func(generator *defaultGenerator) {
+		generator.ignoreColumns = ignoreColumns
+	}
+}
+
+// WithPostgreSql marks  defaultGenerator.isPostgreSql true.
 func WithPostgreSql() Option {
 	return func(generator *defaultGenerator) {
 		generator.isPostgreSql = true
@@ -235,6 +243,16 @@ type Table struct {
 	PrimaryCacheKey        Key
 	UniqueCacheKey         []Key
 	ContainsUniqueCacheKey bool
+	ignoreColumns          []string
+}
+
+func (t Table) isIgnoreColumns(columnName string) bool {
+	for _, v := range t.ignoreColumns {
+		if v == columnName {
+			return true
+		}
+	}
+	return false
 }
 
 func (g *defaultGenerator) genModel(in parser.Table, withCache bool) (string, error) {
@@ -249,6 +267,7 @@ func (g *defaultGenerator) genModel(in parser.Table, withCache bool) (string, er
 	table.PrimaryCacheKey = primaryKey
 	table.UniqueCacheKey = uniqueKey
 	table.ContainsUniqueCacheKey = len(uniqueKey) > 0
+	table.ignoreColumns = g.ignoreColumns
 
 	importsCode, err := genImports(table, withCache, in.ContainsTime())
 	if err != nil {
diff --git a/tools/goctl/model/sql/gen/insert.go b/tools/goctl/model/sql/gen/insert.go
index ad9a54ba..683d98f2 100644
--- a/tools/goctl/model/sql/gen/insert.go
+++ b/tools/goctl/model/sql/gen/insert.go
@@ -31,7 +31,7 @@ func genInsert(table Table, withCache, postgreSql bool) (string, string, error)
 	var count int
 	for _, field := range table.Fields {
 		camel := util.SafeString(field.Name.ToCamel())
-		if camel == "CreateTime" || camel == "UpdateTime" || camel == "CreateAt" || camel == "UpdateAt" {
+		if table.isIgnoreColumns(field.Name.Source()) {
 			continue
 		}
 
diff --git a/tools/goctl/model/sql/gen/update.go b/tools/goctl/model/sql/gen/update.go
index d209b577..09554aa3 100644
--- a/tools/goctl/model/sql/gen/update.go
+++ b/tools/goctl/model/sql/gen/update.go
@@ -21,7 +21,7 @@ func genUpdate(table Table, withCache, postgreSql bool) (
 	}
 	for _, field := range table.Fields {
 		camel := util.SafeString(field.Name.ToCamel())
-		if camel == "CreateTime" || camel == "UpdateTime" || camel == "CreateAt" || camel == "UpdateAt" {
+		if table.isIgnoreColumns(field.Name.Source()) {
 			continue
 		}
 
diff --git a/tools/goctl/model/sql/gen/vars.go b/tools/goctl/model/sql/gen/vars.go
index 49534095..9df8c32a 100644
--- a/tools/goctl/model/sql/gen/vars.go
+++ b/tools/goctl/model/sql/gen/vars.go
@@ -1,8 +1,10 @@
 package gen
 
 import (
+	"fmt"
 	"strings"
 
+	"github.com/zeromicro/go-zero/core/collection"
 	"github.com/zeromicro/go-zero/tools/goctl/model/sql/template"
 	"github.com/zeromicro/go-zero/tools/goctl/util"
 	"github.com/zeromicro/go-zero/tools/goctl/util/pathx"
@@ -32,6 +34,17 @@ func genVars(table Table, withCache, postgreSql bool) (string, error) {
 		"withCache":             withCache,
 		"postgreSql":            postgreSql,
 		"data":                  table,
+		"ignoreColumns": func() string {
+			var set = collection.NewSet()
+			for _, c := range table.ignoreColumns {
+				if postgreSql {
+					set.AddStr(fmt.Sprintf(`"%s"`, c))
+				} else {
+					set.AddStr(fmt.Sprintf("\"`%s`\"", c))
+				}
+			}
+			return strings.Join(set.KeysStr(), ", ")
+		}(),
 	})
 	if err != nil {
 		return "", err
diff --git a/tools/goctl/model/sql/template/vars.go b/tools/goctl/model/sql/template/vars.go
index da093d80..8db226ff 100644
--- a/tools/goctl/model/sql/template/vars.go
+++ b/tools/goctl/model/sql/template/vars.go
@@ -1,20 +1,7 @@
 package template
 
-import "fmt"
+import _ "embed"
 
 // Vars defines a template for var block in model
-var Vars = fmt.Sprintf(
-	`
-var (
-	{{.lowerStartCamelObject}}FieldNames          = builder.RawFieldNames(&{{.upperStartCamelObject}}{}{{if .postgreSql}},true{{end}})
-	{{.lowerStartCamelObject}}Rows                = strings.Join({{.lowerStartCamelObject}}FieldNames, ",")
-	{{.lowerStartCamelObject}}RowsExpectAutoSet   = {{if .postgreSql}}strings.Join(stringx.Remove({{.lowerStartCamelObject}}FieldNames, {{if .autoIncrement}}"{{.originalPrimaryKey}}",{{end}} "%screate_time%s", "%supdate_time%s", "%screate_at%s", "%supdate_at%s"), ","){{else}}strings.Join(stringx.Remove({{.lowerStartCamelObject}}FieldNames, {{if .autoIncrement}}"{{.originalPrimaryKey}}",{{end}} "%screate_time%s", "%supdate_time%s", "%screate_at%s", "%supdate_at%s"), ","){{end}}
-	{{.lowerStartCamelObject}}RowsWithPlaceHolder = {{if .postgreSql}}builder.PostgreSqlJoin(stringx.Remove({{.lowerStartCamelObject}}FieldNames, "{{.originalPrimaryKey}}", "%screate_time%s", "%supdate_time%s", "%screate_at%s", "%supdate_at%s")){{else}}strings.Join(stringx.Remove({{.lowerStartCamelObject}}FieldNames, "{{.originalPrimaryKey}}", "%screate_time%s", "%supdate_time%s", "%screate_at%s", "%supdate_at%s"), "=?,") + "=?"{{end}}
-
-	{{if .withCache}}{{.cacheKeys}}{{end}}
-)
-`, "", "", "", "", "", "", "", "", // postgreSql mode
-	"`", "`", "`", "`", "`", "`", "`", "`",
-	"", "", "", "", "", "", "", "", // postgreSql mode
-	"`", "`", "`", "`", "`", "`", "`", "`",
-)
+//go:embed vars.tpl
+var Vars string
diff --git a/tools/goctl/model/sql/template/vars.tpl b/tools/goctl/model/sql/template/vars.tpl
new file mode 100644
index 00000000..c11fe535
--- /dev/null
+++ b/tools/goctl/model/sql/template/vars.tpl
@@ -0,0 +1,8 @@
+var (
+{{.lowerStartCamelObject}}FieldNames = builder.RawFieldNames(&{{.upperStartCamelObject}}{}{{if .postgreSql}}, true{{end}})
+{{.lowerStartCamelObject}}Rows = strings.Join({{.lowerStartCamelObject}}FieldNames, ",")
+{{.lowerStartCamelObject}}RowsExpectAutoSet = {{if .postgreSql}}strings.Join(stringx.Remove({{.lowerStartCamelObject}}FieldNames, {{if .autoIncrement}}"{{.originalPrimaryKey}}", {{end}} {{.ignoreColumns}}), ","){{else}}strings.Join(stringx.Remove({{.lowerStartCamelObject}}FieldNames, {{if .autoIncrement}}"{{.originalPrimaryKey}}", {{end}} {{.ignoreColumns}}), ","){{end}}
+{{.lowerStartCamelObject}}RowsWithPlaceHolder = {{if .postgreSql}}builder.PostgreSqlJoin(stringx.Remove({{.lowerStartCamelObject}}FieldNames, "{{.originalPrimaryKey}}", {{.ignoreColumns}})){{else}}strings.Join(stringx.Remove({{.lowerStartCamelObject}}FieldNames, "{{.originalPrimaryKey}}", {{.ignoreColumns}}), "=?,") + "=?"{{end}}
+
+{{if .withCache}}{{.cacheKeys}}{{end}}
+)


Test patch:
diff --git a/core/logx/logs_test.go b/core/logx/logs_test.go
index 3be37648..70ec4c7f 100644
--- a/core/logx/logs_test.go
+++ b/core/logx/logs_test.go
@@ -35,6 +35,12 @@ func (mw *mockWriter) Alert(v interface{}) {
 	output(&mw.builder, levelAlert, v)
 }
 
+func (mw *mockWriter) Debug(v interface{}, fields ...LogField) {
+	mw.lock.Lock()
+	defer mw.lock.Unlock()
+	output(&mw.builder, levelDebug, v, fields...)
+}
+
 func (mw *mockWriter) Error(v interface{}, fields ...LogField) {
 	mw.lock.Lock()
 	defer mw.lock.Unlock()
@@ -212,6 +218,46 @@ func TestStructedLogAlert(t *testing.T) {
 	})
 }
 
+func TestStructedLogDebug(t *testing.T) {
+	w := new(mockWriter)
+	old := writer.Swap(w)
+	defer writer.Store(old)
+
+	doTestStructedLog(t, levelDebug, w, func(v ...interface{}) {
+		Debug(v...)
+	})
+}
+
+func TestStructedLogDebugf(t *testing.T) {
+	w := new(mockWriter)
+	old := writer.Swap(w)
+	defer writer.Store(old)
+
+	doTestStructedLog(t, levelDebug, w, func(v ...interface{}) {
+		Debugf(fmt.Sprint(v...))
+	})
+}
+
+func TestStructedLogDebugv(t *testing.T) {
+	w := new(mockWriter)
+	old := writer.Swap(w)
+	defer writer.Store(old)
+
+	doTestStructedLog(t, levelDebug, w, func(v ...interface{}) {
+		Debugv(fmt.Sprint(v...))
+	})
+}
+
+func TestStructedLogDebugw(t *testing.T) {
+	w := new(mockWriter)
+	old := writer.Swap(w)
+	defer writer.Store(old)
+
+	doTestStructedLog(t, levelDebug, w, func(v ...interface{}) {
+		Debugw(fmt.Sprint(v...), Field("foo", time.Second))
+	})
+}
+
 func TestStructedLogError(t *testing.T) {
 	w := new(mockWriter)
 	old := writer.Swap(w)
diff --git a/core/logx/richlogger_test.go b/core/logx/richlogger_test.go
index 08777752..7cf373a5 100644
--- a/core/logx/richlogger_test.go
+++ b/core/logx/richlogger_test.go
@@ -37,6 +37,41 @@ func TestTraceLog(t *testing.T) {
 	validate(t, w.String(), true, true)
 }
 
+func TestTraceDebug(t *testing.T) {
+	w := new(mockWriter)
+	old := writer.Swap(w)
+	writer.lock.RLock()
+	defer func() {
+		writer.lock.RUnlock()
+		writer.Store(old)
+	}()
+
+	otp := otel.GetTracerProvider()
+	tp := sdktrace.NewTracerProvider(sdktrace.WithSampler(sdktrace.AlwaysSample()))
+	otel.SetTracerProvider(tp)
+	defer otel.SetTracerProvider(otp)
+
+	ctx, span := tp.Tracer("foo").Start(context.Background(), "bar")
+	defer span.End()
+
+	l := WithContext(ctx)
+	SetLevel(DebugLevel)
+	l.WithDuration(time.Second).Debug(testlog)
+	assert.True(t, strings.Contains(w.String(), traceKey))
+	assert.True(t, strings.Contains(w.String(), spanKey))
+	w.Reset()
+	l.WithDuration(time.Second).Debugf(testlog)
+	validate(t, w.String(), true, true)
+	w.Reset()
+	l.WithDuration(time.Second).Debugv(testlog)
+	validate(t, w.String(), true, true)
+	w.Reset()
+	l.WithDuration(time.Second).Debugw(testlog, Field("foo", "bar"))
+	validate(t, w.String(), true, true)
+	assert.True(t, strings.Contains(w.String(), "foo"), w.String())
+	assert.True(t, strings.Contains(w.String(), "bar"), w.String())
+}
+
 func TestTraceError(t *testing.T) {
 	w := new(mockWriter)
 	old := writer.Swap(w)
diff --git a/gateway/internal/requestparser_test.go b/gateway/internal/requestparser_test.go
index ede871f0..c910d02a 100644
--- a/gateway/internal/requestparser_test.go
+++ b/gateway/internal/requestparser_test.go
@@ -53,3 +53,16 @@ func TestNewRequestParserWithForm(t *testing.T) {
 	assert.Nil(t, err)
 	assert.NotNil(t, parser)
 }
+
+func TestNewRequestParserWithBadForm(t *testing.T) {
+	req := httptest.NewRequest("GET", "/val?a%1=b", nil)
+	parser, err := NewRequestParser(req, nil)
+	assert.NotNil(t, err)
+	assert.Nil(t, parser)
+}
+
+func TestRequestParser_buildJsonRequestParser(t *testing.T) {
+	parser, err := buildJsonRequestParser(map[string]interface{}{"a": make(chan int)}, nil)
+	assert.NotNil(t, err)
+	assert.Nil(t, parser)
+}