mirror of
https://gitee.com/johng/gf.git
synced 2024-12-01 19:57:40 +08:00
add structure logging handler for package glog (#2919)
This commit is contained in:
parent
eb11061bd2
commit
887803e495
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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")
|
||||
|
@ -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)
|
||||
|
245
os/glog/glog_logger_handler_structure.go
Normal file
245
os/glog/glog_logger_handler_structure.go
Normal 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
|
||||
}
|
@ -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()
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user