diff --git a/database/gdb/gdb_result.go b/database/gdb/gdb_result.go index f7c91013b9e..f86714203ba 100644 --- a/database/gdb/gdb_result.go +++ b/database/gdb/gdb_result.go @@ -44,9 +44,6 @@ func (r *SqlResult) MustGetInsertId() int64 { // driver may support this. // Also, See sql.Result. func (r *SqlResult) RowsAffected() (int64, error) { - if r.Result == nil { - return 0, nil - } if r.Affected > 0 { return r.Affected, nil } diff --git a/os/glog/glog_logger.go b/os/glog/glog_logger.go index ff06fe7aae3..f67e8a253c3 100644 --- a/os/glog/glog_logger.go +++ b/os/glog/glog_logger.go @@ -93,7 +93,7 @@ func (l *Logger) getFilePath(now time.Time) string { } // print prints `s` to defined writer, logging file or passed `std`. -func (l *Logger) print(ctx context.Context, level int, stack string, values ...interface{}) { +func (l *Logger) print(ctx context.Context, level int, stack string, values ...any) { // Lazy initialize for rotation feature. // It uses atomic reading operation to enhance the performance checking. // It here uses CAP for performance and concurrent safety. @@ -117,6 +117,7 @@ func (l *Logger) print(ctx context.Context, level int, stack string, values ...i Color: defaultLevelColor[level], Level: level, Stack: stack, + Values: values, } ) @@ -126,7 +127,7 @@ func (l *Logger) print(ctx context.Context, level int, stack string, values ...i } else if defaultHandler != nil { input.handlers = []Handler{defaultHandler} } - input.handlers = append(input.handlers, defaultPrintHandler) + input.handlers = append(input.handlers, doFinalPrint) // Time. timeFormat := "" @@ -205,24 +206,6 @@ func (l *Logger) print(ctx context.Context, level int, stack string, values ...i } } } - var tempStr string - for _, v := range values { - tempStr = gconv.String(v) - if len(input.Content) > 0 { - if input.Content[len(input.Content)-1] == '\n' { - // Remove one blank line(\n\n). - if len(tempStr) > 0 && tempStr[0] == '\n' { - input.Content += tempStr[1:] - } else { - input.Content += tempStr - } - } else { - input.Content += " " + tempStr - } - } else { - input.Content = tempStr - } - } if l.config.Flags&F_ASYNC > 0 { input.IsAsync = true err := asyncPool.Add(ctx, func(ctx context.Context) { @@ -265,9 +248,7 @@ func (l *Logger) doDefaultPrint(ctx context.Context, input *HandlerInput) *bytes // printToWriter writes buffer to writer. func (l *Logger) printToWriter(ctx context.Context, input *HandlerInput) *bytes.Buffer { if l.config.Writer != nil { - var ( - buffer = input.getRealBuffer(l.config.WriterColorEnable) - ) + var buffer = input.getRealBuffer(l.config.WriterColorEnable) if _, err := l.config.Writer.Write(buffer.Bytes()); err != nil { intlog.Errorf(ctx, `%+v`, err) } @@ -283,7 +264,7 @@ func (l *Logger) printToStdout(ctx context.Context, input *HandlerInput) *bytes. err error buffer = input.getRealBuffer(!l.config.StdoutColorDisabled) ) - // This will lose color in Windows os system. + // This will lose color in Windows os system. DO NOT USE. // if _, err := os.Stdout.Write(input.getRealBuffer(true).Bytes()); err != nil { // This will print color in Windows os system. @@ -372,23 +353,23 @@ func (l *Logger) getOpenedFilePointer(ctx context.Context, path string) *gfpool. } // printStd prints content `s` without stack. -func (l *Logger) printStd(ctx context.Context, level int, value ...interface{}) { - l.print(ctx, level, "", value...) +func (l *Logger) printStd(ctx context.Context, level int, values ...interface{}) { + l.print(ctx, level, "", values...) } // printStd prints content `s` with stack check. -func (l *Logger) printErr(ctx context.Context, level int, value ...interface{}) { +func (l *Logger) printErr(ctx context.Context, level int, values ...interface{}) { var stack string if l.config.StStatus == 1 { stack = l.GetStack() } // In matter of sequence, do not use stderr here, but use the same stdout. - l.print(ctx, level, stack, value...) + l.print(ctx, level, stack, values...) } // format formats `values` using fmt.Sprintf. -func (l *Logger) format(format string, value ...interface{}) string { - return fmt.Sprintf(format, value...) +func (l *Logger) format(format string, values ...interface{}) string { + return fmt.Sprintf(format, values...) } // PrintStack prints the caller stack, diff --git a/os/glog/glog_logger_handler.go b/os/glog/glog_logger_handler.go index 4929fb51520..9b632beaa79 100644 --- a/os/glog/glog_logger_handler.go +++ b/os/glog/glog_logger_handler.go @@ -10,6 +10,8 @@ import ( "bytes" "context" "time" + + "github.com/gogf/gf/v2/util/gconv" ) // Handler is function handler for custom logging content outputs. @@ -31,6 +33,7 @@ type HandlerInput struct { TraceId string // Trace id, only available if OpenTelemetry is enabled. Prefix string // Custom prefix string for logging content. Content string // Content is the main logging content without error stack string produced by logger. + Values []any // The passed un-formatted values array to logger. Stack string // Stack string produced by logger, only available if Config.StStatus configured. IsAsync bool // IsAsync marks it is in asynchronous logging. } @@ -43,9 +46,9 @@ type internalHandlerInfo struct { // defaultHandler is the default handler for package. var defaultHandler Handler -// defaultPrintHandler is a handler for logging content printing. +// doFinalPrint is a handler for logging content printing. // This handler outputs logging content to file/stdout/write if any of them configured. -func defaultPrintHandler(ctx context.Context, in *HandlerInput) { +func doFinalPrint(ctx context.Context, in *HandlerInput) { buffer := in.Logger.doDefaultPrint(ctx, in) if in.Buffer.Len() == 0 { in.Buffer = buffer @@ -113,13 +116,36 @@ func (in *HandlerInput) getDefaultBuffer(withColor bool) *bytes.Buffer { in.addStringToBuffer(buffer, in.CallerPath) } } + if in.Content != "" { - if in.Stack != "" { - in.addStringToBuffer(buffer, in.Content+"\nStack:\n"+in.Stack) + in.addStringToBuffer(buffer, in.Content) + } + + // Convert values string content. + var valueContent string + for _, v := range in.Values { + valueContent = gconv.String(v) + if len(valueContent) == 0 { + continue + } + if buffer.Len() > 0 { + if buffer.Bytes()[buffer.Len()-1] == '\n' { + // Remove one blank line(\n\n). + if valueContent[0] == '\n' { + valueContent = valueContent[1:] + } + buffer.WriteString(valueContent) + } else { + buffer.WriteString(" " + valueContent) + } } else { - in.addStringToBuffer(buffer, in.Content) + buffer.WriteString(valueContent) } } + + if in.Stack != "" { + in.addStringToBuffer(buffer, "\nStack:\n"+in.Stack) + } // avoid a single space at the end of a line. buffer.WriteString("\n") return buffer diff --git a/os/glog/glog_logger_handler_json.go b/os/glog/glog_logger_handler_json.go index 20b82c74e4d..3ef2c400733 100644 --- a/os/glog/glog_logger_handler_json.go +++ b/os/glog/glog_logger_handler_json.go @@ -10,6 +10,7 @@ import ( "context" "github.com/gogf/gf/v2/internal/json" + "github.com/gogf/gf/v2/util/gconv" ) // HandlerOutputJson is the structure outputting logging content as single json. @@ -18,8 +19,8 @@ type HandlerOutputJson struct { TraceId string `json:",omitempty"` // Trace id, only available if tracing is enabled. CtxStr string `json:",omitempty"` // The retrieved context value string from context, only available if Config.CtxKeys configured. Level string `json:""` // Formatted level string, like "DEBU", "ERRO", etc. Eg: ERRO - CallerFunc string `json:",omitempty"` // The source function name that calls logging, only available if F_CALLER_FN set. CallerPath string `json:",omitempty"` // The source file path and its line number that calls logging, only available if F_FILE_SHORT or F_FILE_LONG set. + CallerFunc string `json:",omitempty"` // The source function name that calls logging, only available if F_CALLER_FN set. Prefix string `json:",omitempty"` // Custom prefix string for logging content. Content string `json:""` // Content is the main logging content, containing error stack string produced by logger. Stack string `json:",omitempty"` // Stack string produced by logger, only available if Config.StStatus configured. @@ -38,6 +39,28 @@ func HandlerJson(ctx context.Context, in *HandlerInput) { Content: in.Content, Stack: in.Stack, } + // Convert values string content. + var valueContent string + for _, v := range in.Values { + valueContent = gconv.String(v) + if len(valueContent) == 0 { + continue + } + if len(output.Content) > 0 { + if output.Content[len(output.Content)-1] == '\n' { + // Remove one blank line(\n\n). + if valueContent[0] == '\n' { + valueContent = valueContent[1:] + } + output.Content += valueContent + } else { + output.Content += " " + valueContent + } + } else { + output.Content += valueContent + } + } + // Output json content. jsonBytes, err := json.Marshal(output) if err != nil { panic(err) diff --git a/os/glog/glog_logger_handler_structure.go b/os/glog/glog_logger_handler_structure.go new file mode 100644 index 00000000000..0b0846942b6 --- /dev/null +++ b/os/glog/glog_logger_handler_structure.go @@ -0,0 +1,245 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package glog + +import ( + "bytes" + "context" + "strconv" + "unicode" + "unicode/utf8" + + "github.com/gogf/gf/v2/util/gconv" +) + +type structuredBuffer struct { + in *HandlerInput + buffer *bytes.Buffer +} + +const ( + structureKeyTime = "Time" + structureKeyLevel = "Level" + structureKeyPrefix = "Prefix" + structureKeyContent = "Content" + structureKeyTraceId = "TraceId" + structureKeyCallerFunc = "CallerFunc" + structureKeyCallerPath = "CallerPath" + structureKeyCtxStr = "CtxStr" + structureKeyStack = "Stack" +) + +// Copied from encoding/json/tables.go. +// +// safeSet holds the value true if the ASCII character with the given array +// position can be represented inside a JSON string without any further +// escaping. +// +// All values are true except for the ASCII control characters (0-31), the +// double quote ("), and the backslash character ("\"). +var safeSet = [utf8.RuneSelf]bool{ + ' ': true, + '!': true, + '"': false, + '#': true, + '$': true, + '%': true, + '&': true, + '\'': true, + '(': true, + ')': true, + '*': true, + '+': true, + ',': true, + '-': true, + '.': true, + '/': true, + '0': true, + '1': true, + '2': true, + '3': true, + '4': true, + '5': true, + '6': true, + '7': true, + '8': true, + '9': true, + ':': true, + ';': true, + '<': true, + '=': true, + '>': true, + '?': true, + '@': true, + 'A': true, + 'B': true, + 'C': true, + 'D': true, + 'E': true, + 'F': true, + 'G': true, + 'H': true, + 'I': true, + 'J': true, + 'K': true, + 'L': true, + 'M': true, + 'N': true, + 'O': true, + 'P': true, + 'Q': true, + 'R': true, + 'S': true, + 'T': true, + 'U': true, + 'V': true, + 'W': true, + 'X': true, + 'Y': true, + 'Z': true, + '[': true, + '\\': false, + ']': true, + '^': true, + '_': true, + '`': true, + 'a': true, + 'b': true, + 'c': true, + 'd': true, + 'e': true, + 'f': true, + 'g': true, + 'h': true, + 'i': true, + 'j': true, + 'k': true, + 'l': true, + 'm': true, + 'n': true, + 'o': true, + 'p': true, + 'q': true, + 'r': true, + 's': true, + 't': true, + 'u': true, + 'v': true, + 'w': true, + 'x': true, + 'y': true, + 'z': true, + '{': true, + '|': true, + '}': true, + '~': true, + '\u007f': true, +} + +// HandlerStructure is a handler for output logging content as a structured string. +func HandlerStructure(ctx context.Context, in *HandlerInput) { + s := newStructuredBuffer(in) + in.Buffer.Write(s.Bytes()) + in.Buffer.Write([]byte("\n")) + in.Next(ctx) +} + +func newStructuredBuffer(in *HandlerInput) *structuredBuffer { + return &structuredBuffer{ + in: in, + buffer: bytes.NewBuffer(nil), + } +} + +func (buf *structuredBuffer) Bytes() []byte { + buf.addValue(structureKeyTime, buf.in.TimeFormat) + if buf.in.TraceId != "" { + buf.addValue(structureKeyTraceId, buf.in.TraceId) + } + if buf.in.CtxStr != "" { + buf.addValue(structureKeyCtxStr, buf.in.CtxStr) + } + if buf.in.LevelFormat != "" { + buf.addValue(structureKeyLevel, buf.in.LevelFormat) + } + if buf.in.CallerPath != "" { + buf.addValue(structureKeyCallerPath, buf.in.CallerPath) + } + if buf.in.CallerFunc != "" { + buf.addValue(structureKeyCallerFunc, buf.in.CallerFunc) + } + if buf.in.Prefix != "" { + buf.addValue(structureKeyPrefix, buf.in.Prefix) + } + // If the values cannot be the pair, move the first one to content. + values := buf.in.Values + if len(values)%2 != 0 { + if buf.in.Content != "" { + buf.in.Content += " " + } + buf.in.Content += gconv.String(values[0]) + values = values[1:] + } + if buf.in.Content != "" { + buf.addValue(structureKeyContent, buf.in.Content) + } + // Values pairs. + for i := 0; i < len(values); i += 2 { + buf.addValue(values[i], values[i+1]) + } + if buf.in.Stack != "" { + buf.addValue(structureKeyStack, buf.in.Stack) + } + contentBytes := buf.buffer.Bytes() + buf.buffer.Reset() + contentBytes = bytes.ReplaceAll(contentBytes, []byte{'\n'}, []byte{' '}) + return contentBytes +} + +func (buf *structuredBuffer) addValue(k, v any) { + var ( + ks = gconv.String(k) + vs = gconv.String(v) + ) + if buf.buffer.Len() > 0 { + buf.buffer.WriteByte(' ') + } + buf.appendString(ks) + buf.buffer.WriteByte('=') + buf.appendString(vs) +} + +func (buf *structuredBuffer) appendString(s string) { + if buf.needsQuoting(s) { + s = strconv.Quote(s) + } + buf.buffer.WriteString(s) +} + +func (buf *structuredBuffer) needsQuoting(s string) bool { + if len(s) == 0 { + return true + } + for i := 0; i < len(s); { + b := s[i] + if b < utf8.RuneSelf { + // Quote anything except a backslash that would need quoting in a + // JSON string, as well as space and '=' + if b != '\\' && (b == ' ' || b == '=' || !safeSet[b]) { + return true + } + i++ + continue + } + r, size := utf8.DecodeRuneInString(s[i:]) + if r == utf8.RuneError || unicode.IsSpace(r) || !unicode.IsPrint(r) { + return true + } + i += size + } + return false +} diff --git a/os/glog/glog_z_unit_logger_handler_test.go b/os/glog/glog_z_unit_logger_handler_test.go index 1ba3f8dad20..7bbec9559e8 100644 --- a/os/glog/glog_z_unit_logger_handler_test.go +++ b/os/glog/glog_z_unit_logger_handler_test.go @@ -89,6 +89,25 @@ func TestLogger_SetHandlers_HandlerJson(t *testing.T) { }) } +func TestLogger_SetHandlers_HandlerStructure(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + w := bytes.NewBuffer(nil) + l := glog.NewWithWriter(w) + l.SetHandlers(glog.HandlerStructure) + l.SetCtxKeys("Trace-Id", "Span-Id", "Test") + ctx := context.WithValue(context.Background(), "Trace-Id", "1234567890") + ctx = context.WithValue(ctx, "Span-Id", "abcdefg") + + l.Debug(ctx, "debug", "uid", 1000) + l.Info(ctx, "info", "' '", `"\n`) + + t.Assert(gstr.Count(w.String(), "uid=1000"), 1) + t.Assert(gstr.Count(w.String(), "Content=debug"), 1) + t.Assert(gstr.Count(w.String(), `"' '"="\"\\n"`), 1) + t.Assert(gstr.Count(w.String(), `CtxStr="1234567890, abcdefg"`), 2) + }) +} + func Test_SetDefaultHandler(t *testing.T) { gtest.C(t, func(t *gtest.T) { oldHandler := glog.GetDefaultHandler() diff --git a/text/gstr/gstr_slashes.go b/text/gstr/gstr_slashes.go index 2fed181462c..5095be16532 100644 --- a/text/gstr/gstr_slashes.go +++ b/text/gstr/gstr_slashes.go @@ -12,7 +12,7 @@ import ( "github.com/gogf/gf/v2/internal/utils" ) -// AddSlashes quotes chars('"\) with slashes. +// AddSlashes quotes with slashes `\` for chars: '"\. func AddSlashes(str string) string { var buf bytes.Buffer for _, char := range str { @@ -30,8 +30,8 @@ func StripSlashes(str string) string { return utils.StripSlashes(str) } -// QuoteMeta returns a version of str with a backslash character (\) -// before every character that is among: .\+*?[^]($) +// QuoteMeta returns a version of `str` with a backslash character (`\`). +// If custom chars `chars` not given, it uses default chars: .\+*?[^]($) func QuoteMeta(str string, chars ...string) string { var buf bytes.Buffer for _, char := range str {