add structure logging handler for package glog (#2919)

This commit is contained in:
John Guo 2023-09-04 21:23:46 +08:00 committed by GitHub
parent eb11061bd2
commit 887803e495
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 334 additions and 43 deletions

View File

@ -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
}

View File

@ -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,

View File

@ -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,12 +116,35 @@ 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)
} else {
in.addStringToBuffer(buffer, in.Content)
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 {
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")

View File

@ -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)

View File

@ -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
}

View File

@ -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()

View File

@ -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 {