/** * Copyright (c) 2021 OceanBase * OceanBase CE is licensed under Mulan PubL v2. * You can use this software according to the terms and conditions of the Mulan PubL v2. * You may obtain a copy of Mulan PubL v2 at: * http://license.coscl.org.cn/MulanPubL-2.0 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. * See the Mulan PubL v2 for more details. */ package logger import ( "bytes" "fmt" "os" "runtime" "sort" "strconv" "strings" "sync" "time" "unicode/utf8" "github.com/mattn/go-isatty" "github.com/sirupsen/logrus" ) const ( red = 31 yellow = 33 blue = 36 gray = 37 ) var ( // procid pid = os.Getpid() // proc start timestamp startTimestamp time.Time ) func init() { startTimestamp = time.Now() } type TraceIdKey struct{} // field alias type FieldMap map[string]string func (f FieldMap) resolve(key string) string { if k, ok := f[key]; ok { return k } return string(key) } // TextFormatter formats logs into text type TextFormatter struct { // Set to true to bypass checking for a TTY before outputting colors. ForceColors bool // Force disabling colors. DisableColors bool // Force quoting of all values ForceQuote bool // DisableQuote disables quoting for all values. // DisableQuote will have a lower priority than ForceQuote. // If both of them are set to true, quote will be forced on all values. DisableQuote bool // Override coloring based on CLICOLOR and CLICOLOR_FORCE. - https://bixense.com/clicolors/ EnvironmentOverrideColors bool // Disable timestamp logging. useful when output is redirected to logging // system that already adds timestamps. DisableTimestamp bool // Enable logging the full timestamp when a TTY is attached instead of just // the time passed since beginning of execution. FullTimestamp bool // TimestampFormat to use for display when a full timestamp is printed TimestampFormat string // The fields are sorted by default for a consistent output. For applications // that log extremely frequently and don't use the JSON formatter this may not // be desired. DisableSorting bool // The keys sorting function, when uninitialized it uses sort.Strings. SortingFunc func([]string) // Disables the truncation of the level text to 4 characters. DisableLevelTruncation bool // PadLevelText Adds padding the level text so that all the levels output at the same length // PadLevelText is a superset of the DisableLevelTruncation option PadLevelText bool // QuoteEmptyFields will wrap empty fields in quotes if true QuoteEmptyFields bool // Whether the logger's out is to a terminal isTerminal bool // FieldMap allows users to customize the names of keys for default fields. // As an example: // formatter := &TextFormatter{ // FieldMap: FieldMap{ // FieldKeyTime: "@timestamp", // FieldKeyLevel: "@level", // FieldKeyMsg: "@message"}} FieldMap FieldMap // CallerPrettyfier can be set by the user to modify the content // of the function and file keys in the data when ReportCaller is // activated. If any of the returned value is the empty string the // corresponding key will be removed from fields. CallerPrettyfier func(*runtime.Frame) (function string, file string) terminalInitOnce sync.Once // The max length of the level text, generated dynamically on init levelTextMaxLength int } func (f *TextFormatter) init(entry *logrus.Entry) { if entry.Logger != nil { file, ok := (entry.Logger.Out).(*os.File) f.isTerminal = ok && isatty.IsTerminal(file.Fd()) } // Get the max length of the level text for _, level := range logrus.AllLevels { levelTextLength := utf8.RuneCount([]byte(level.String())) if levelTextLength > f.levelTextMaxLength { f.levelTextMaxLength = levelTextLength } } } func (f *TextFormatter) isColored() bool { isColored := f.ForceColors || (f.isTerminal && (runtime.GOOS != "windows")) if f.EnvironmentOverrideColors { switch force, ok := os.LookupEnv("CLICOLOR_FORCE"); { case ok && force != "0": isColored = true case ok && force == "0", os.Getenv("CLICOLOR") == "0": isColored = false } } return isColored && !f.DisableColors } func prefixFieldClashes(data logrus.Fields, fieldMap FieldMap, reportCaller bool) { timeKey := fieldMap.resolve(logrus.FieldKeyTime) if t, ok := data[timeKey]; ok { data["fields."+timeKey] = t delete(data, timeKey) } msgKey := fieldMap.resolve(logrus.FieldKeyMsg) if m, ok := data[msgKey]; ok { data["fields."+msgKey] = m delete(data, msgKey) } levelKey := fieldMap.resolve(logrus.FieldKeyLevel) if l, ok := data[levelKey]; ok { data["fields."+levelKey] = l delete(data, levelKey) } logrusErrKey := fieldMap.resolve(logrus.FieldKeyLogrusError) if l, ok := data[logrusErrKey]; ok { data["fields."+logrusErrKey] = l delete(data, logrusErrKey) } // If reportCaller is not set, 'func' will not conflict. if reportCaller { funcKey := fieldMap.resolve(logrus.FieldKeyFunc) if l, ok := data[funcKey]; ok { data["fields."+funcKey] = l } fileKey := fieldMap.resolve(logrus.FieldKeyFile) if l, ok := data[fileKey]; ok { data["fields."+fileKey] = l } } } // Format renders a single log entry func (f *TextFormatter) Format(entry *logrus.Entry) ([]byte, error) { data := make(logrus.Fields) for k, v := range entry.Data { data[k] = v } prefixFieldClashes(data, f.FieldMap, entry.HasCaller()) keys := make([]string, 0, len(data)) for k := range data { keys = append(keys, k) } var funcVal, fileVal string fixedKeys := make([]string, 0, 4+len(data)) if !f.DisableTimestamp { fixedKeys = append(fixedKeys, f.FieldMap.resolve(logrus.FieldKeyTime)) } fixedKeys = append(fixedKeys, f.FieldMap.resolve(logrus.FieldKeyLevel)) if entry.Message != "" { fixedKeys = append(fixedKeys, f.FieldMap.resolve(logrus.FieldKeyMsg)) } // if entry.err != "" { // fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyLogrusError)) // } if entry.HasCaller() { if f.CallerPrettyfier != nil { funcVal, fileVal = f.CallerPrettyfier(entry.Caller) } else { funcVal = entry.Caller.Function fileVal = fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line) } if funcVal != "" { fixedKeys = append(fixedKeys, f.FieldMap.resolve(logrus.FieldKeyFunc)) } if fileVal != "" { fixedKeys = append(fixedKeys, f.FieldMap.resolve(logrus.FieldKeyFile)) } } if !f.DisableSorting { if f.SortingFunc == nil { sort.Strings(keys) fixedKeys = append(fixedKeys, keys...) } else { if !f.isColored() { fixedKeys = append(fixedKeys, keys...) f.SortingFunc(fixedKeys) } else { f.SortingFunc(keys) } } } else { fixedKeys = append(fixedKeys, keys...) } var b *bytes.Buffer if entry.Buffer != nil { b = entry.Buffer } else { b = &bytes.Buffer{} } f.terminalInitOnce.Do(func() { f.init(entry) }) timestampFormat := f.TimestampFormat if timestampFormat == "" { timestampFormat = defaultTimestampFormat } f.printMessage(b, entry, keys, data, timestampFormat) b.WriteByte('\n') return b.Bytes(), nil } func (f *TextFormatter) printMessage(b *bytes.Buffer, entry *logrus.Entry, keys []string, data logrus.Fields, timestampFormat string) { levelText := strings.ToUpper(entry.Level.String()) levelText = f.FieldMap.resolve(levelText) if !f.DisableLevelTruncation && !f.PadLevelText { levelText = levelText[0:4] } if f.PadLevelText { // Generates the format string used in the next line, for example "%-6s" or "%-7s". // Based on the max level text length. formatString := "%-" + strconv.Itoa(f.levelTextMaxLength) + "s" // Formats the level text by appending spaces up to the max length, for example: // - "INFO " // - "WARNING" levelText = fmt.Sprintf(formatString, levelText) } var traceId string if entry.Context != nil { traceIdVal := entry.Context.Value(TraceIdKey{}) traceId, _ = traceIdVal.(string) } // Remove a single newline if it already exists in the message to keep // the behavior of logrus text_formatter the same as the stdlib log package entry.Message = strings.TrimSuffix(entry.Message, "\n") caller := "" if entry.HasCaller() { funcVal := fmt.Sprintf("%s", entry.Caller.Function) fileVal := fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line) if f.CallerPrettyfier != nil { funcVal, fileVal = f.CallerPrettyfier(entry.Caller) } if fileVal == "" { caller = funcVal } else if funcVal == "" { caller = fileVal } else { caller = fileVal + ":" + funcVal } } if f.isColored() { f.printColored(b, entry, keys, data, timestampFormat, levelText, caller, traceId) } else { f.printNoColored(b, entry, keys, data, timestampFormat, levelText, caller, traceId) } } func (f *TextFormatter) printColored(b *bytes.Buffer, entry *logrus.Entry, keys []string, data logrus.Fields, timestampFormat string, levelText string, caller string, traceId string) { var levelColor int switch entry.Level { case logrus.DebugLevel, logrus.TraceLevel: levelColor = gray case logrus.WarnLevel: levelColor = yellow case logrus.ErrorLevel, logrus.FatalLevel, logrus.PanicLevel: levelColor = red case logrus.InfoLevel: levelColor = blue default: levelColor = blue } switch { case f.DisableTimestamp: fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m [%d,%s] %s %s", levelColor, levelText, pid, traceId, caller, entry.Message) case !f.FullTimestamp: fmt.Fprintf(b, "%04d \x1b[%dm%s\x1b[0m [%d,%s] %s %s", int(entry.Time.Sub(startTimestamp)/time.Second), levelColor, levelText, pid, traceId, caller, entry.Message) default: fmt.Fprintf(b, "%s \x1b[%dm%s\x1b[0m [%d,%s] (%s) %s", entry.Time.Format(timestampFormat), levelColor, levelText, pid, traceId, caller, entry.Message) } if len(keys) > 0 { b.WriteString(" ") } for i, k := range keys { v := data[k] if i == 0 { fmt.Fprintf(b, " \x1b[%dm%s\x1b[0m=", levelColor, k) } else { fmt.Fprintf(b, ", \x1b[%dm%s\x1b[0m=", levelColor, k) } f.appendValue(b, v) } } func (f *TextFormatter) printNoColored(b *bytes.Buffer, entry *logrus.Entry, keys []string, data logrus.Fields, timestampFormat string, levelText string, caller string, traceId string) { switch { case f.DisableTimestamp: fmt.Fprintf(b, "%s [%d,%s] caller=%s %s", levelText, pid, traceId, caller, entry.Message) case !f.FullTimestamp: fmt.Fprintf(b, "%04d %s [%d,%s] caller=%s %s", int(entry.Time.Sub(startTimestamp)/time.Second), levelText, pid, traceId, caller, entry.Message) default: fmt.Fprintf(b, "%s %s [%d,%s] caller=%s: %s", entry.Time.Format(timestampFormat), levelText, pid, traceId, caller, entry.Message) } if len(keys) > 0 { b.WriteString(" fields:") } for i, k := range keys { v := data[k] if i == 0 { fmt.Fprintf(b, " %s=", k) } else { fmt.Fprintf(b, ", %s=", k) } f.appendValue(b, v) } } func (f *TextFormatter) needsQuoting(text string) bool { if f.ForceQuote { return true } if f.QuoteEmptyFields && len(text) == 0 { return true } if f.DisableQuote { return false } for _, ch := range text { if !((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '-' || ch == '.' || ch == '_' || ch == '/' || ch == '@' || ch == '^' || ch == '+') { return true } } return false } func (f *TextFormatter) appendKeyValue(b *bytes.Buffer, key string, value interface{}) { if b.Len() > 0 { b.WriteByte(' ') } b.WriteString(key) b.WriteByte('=') f.appendValue(b, value) } func (f *TextFormatter) appendValue(b *bytes.Buffer, value interface{}) { stringVal, ok := value.(string) if !ok { stringVal = fmt.Sprint(value) } if !f.needsQuoting(stringVal) { b.WriteString(stringVal) } else { b.WriteString(fmt.Sprintf("%q", stringVal)) } }