1package logrus 2 3import ( 4 "bytes" 5 "fmt" 6 "os" 7 "sort" 8 "strings" 9 "sync" 10 "time" 11) 12 13const ( 14 nocolor = 0 15 red = 31 16 green = 32 17 yellow = 33 18 blue = 36 19 gray = 37 20) 21 22var ( 23 baseTimestamp time.Time 24 emptyFieldMap FieldMap 25) 26 27func init() { 28 baseTimestamp = time.Now() 29} 30 31// TextFormatter formats logs into text 32type TextFormatter struct { 33 // Set to true to bypass checking for a TTY before outputting colors. 34 ForceColors bool 35 36 // Force disabling colors. 37 DisableColors bool 38 39 // Override coloring based on CLICOLOR and CLICOLOR_FORCE. - https://bixense.com/clicolors/ 40 EnvironmentOverrideColors bool 41 42 // Disable timestamp logging. useful when output is redirected to logging 43 // system that already adds timestamps. 44 DisableTimestamp bool 45 46 // Enable logging the full timestamp when a TTY is attached instead of just 47 // the time passed since beginning of execution. 48 FullTimestamp bool 49 50 // TimestampFormat to use for display when a full timestamp is printed 51 TimestampFormat string 52 53 // The fields are sorted by default for a consistent output. For applications 54 // that log extremely frequently and don't use the JSON formatter this may not 55 // be desired. 56 DisableSorting bool 57 58 // The keys sorting function, when uninitialized it uses sort.Strings. 59 SortingFunc func([]string) 60 61 // Disables the truncation of the level text to 4 characters. 62 DisableLevelTruncation bool 63 64 // QuoteEmptyFields will wrap empty fields in quotes if true 65 QuoteEmptyFields bool 66 67 // Whether the logger's out is to a terminal 68 isTerminal bool 69 70 // FieldMap allows users to customize the names of keys for default fields. 71 // As an example: 72 // formatter := &TextFormatter{ 73 // FieldMap: FieldMap{ 74 // FieldKeyTime: "@timestamp", 75 // FieldKeyLevel: "@level", 76 // FieldKeyMsg: "@message"}} 77 FieldMap FieldMap 78 79 terminalInitOnce sync.Once 80} 81 82func (f *TextFormatter) init(entry *Entry) { 83 if entry.Logger != nil { 84 f.isTerminal = checkIfTerminal(entry.Logger.Out) 85 86 if f.isTerminal { 87 initTerminal(entry.Logger.Out) 88 } 89 } 90} 91 92func (f *TextFormatter) isColored() bool { 93 isColored := f.ForceColors || f.isTerminal 94 95 if f.EnvironmentOverrideColors { 96 if force, ok := os.LookupEnv("CLICOLOR_FORCE"); ok && force != "0" { 97 isColored = true 98 } else if ok && force == "0" { 99 isColored = false 100 } else if os.Getenv("CLICOLOR") == "0" { 101 isColored = false 102 } 103 } 104 105 return isColored && !f.DisableColors 106} 107 108// Format renders a single log entry 109func (f *TextFormatter) Format(entry *Entry) ([]byte, error) { 110 prefixFieldClashes(entry.Data, f.FieldMap, entry.HasCaller()) 111 112 keys := make([]string, 0, len(entry.Data)) 113 for k := range entry.Data { 114 keys = append(keys, k) 115 } 116 117 fixedKeys := make([]string, 0, 4+len(entry.Data)) 118 if !f.DisableTimestamp { 119 fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyTime)) 120 } 121 fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyLevel)) 122 if entry.Message != "" { 123 fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyMsg)) 124 } 125 if entry.err != "" { 126 fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyLogrusError)) 127 } 128 if entry.HasCaller() { 129 fixedKeys = append(fixedKeys, 130 f.FieldMap.resolve(FieldKeyFunc), f.FieldMap.resolve(FieldKeyFile)) 131 } 132 133 if !f.DisableSorting { 134 if f.SortingFunc == nil { 135 sort.Strings(keys) 136 fixedKeys = append(fixedKeys, keys...) 137 } else { 138 if !f.isColored() { 139 fixedKeys = append(fixedKeys, keys...) 140 f.SortingFunc(fixedKeys) 141 } else { 142 f.SortingFunc(keys) 143 } 144 } 145 } else { 146 fixedKeys = append(fixedKeys, keys...) 147 } 148 149 var b *bytes.Buffer 150 if entry.Buffer != nil { 151 b = entry.Buffer 152 } else { 153 b = &bytes.Buffer{} 154 } 155 156 f.terminalInitOnce.Do(func() { f.init(entry) }) 157 158 timestampFormat := f.TimestampFormat 159 if timestampFormat == "" { 160 timestampFormat = defaultTimestampFormat 161 } 162 if f.isColored() { 163 f.printColored(b, entry, keys, timestampFormat) 164 } else { 165 for _, key := range fixedKeys { 166 var value interface{} 167 switch { 168 case key == f.FieldMap.resolve(FieldKeyTime): 169 value = entry.Time.Format(timestampFormat) 170 case key == f.FieldMap.resolve(FieldKeyLevel): 171 value = entry.Level.String() 172 case key == f.FieldMap.resolve(FieldKeyMsg): 173 value = entry.Message 174 case key == f.FieldMap.resolve(FieldKeyLogrusError): 175 value = entry.err 176 case key == f.FieldMap.resolve(FieldKeyFunc) && entry.HasCaller(): 177 value = entry.Caller.Function 178 case key == f.FieldMap.resolve(FieldKeyFile) && entry.HasCaller(): 179 value = fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line) 180 default: 181 value = entry.Data[key] 182 } 183 f.appendKeyValue(b, key, value) 184 } 185 } 186 187 b.WriteByte('\n') 188 return b.Bytes(), nil 189} 190 191func (f *TextFormatter) printColored(b *bytes.Buffer, entry *Entry, keys []string, timestampFormat string) { 192 var levelColor int 193 switch entry.Level { 194 case DebugLevel, TraceLevel: 195 levelColor = gray 196 case WarnLevel: 197 levelColor = yellow 198 case ErrorLevel, FatalLevel, PanicLevel: 199 levelColor = red 200 default: 201 levelColor = blue 202 } 203 204 levelText := strings.ToUpper(entry.Level.String()) 205 if !f.DisableLevelTruncation { 206 levelText = levelText[0:4] 207 } 208 209 // Remove a single newline if it already exists in the message to keep 210 // the behavior of logrus text_formatter the same as the stdlib log package 211 entry.Message = strings.TrimSuffix(entry.Message, "\n") 212 213 caller := "" 214 215 if entry.HasCaller() { 216 caller = fmt.Sprintf("%s:%d %s()", 217 entry.Caller.File, entry.Caller.Line, entry.Caller.Function) 218 } 219 220 if f.DisableTimestamp { 221 fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m%s %-44s ", levelColor, levelText, caller, entry.Message) 222 } else if !f.FullTimestamp { 223 fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%04d]%s %-44s ", levelColor, levelText, int(entry.Time.Sub(baseTimestamp)/time.Second), caller, entry.Message) 224 } else { 225 fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%s]%s %-44s ", levelColor, levelText, entry.Time.Format(timestampFormat), caller, entry.Message) 226 } 227 for _, k := range keys { 228 v := entry.Data[k] 229 fmt.Fprintf(b, " \x1b[%dm%s\x1b[0m=", levelColor, k) 230 f.appendValue(b, v) 231 } 232} 233 234func (f *TextFormatter) needsQuoting(text string) bool { 235 if f.QuoteEmptyFields && len(text) == 0 { 236 return true 237 } 238 for _, ch := range text { 239 if !((ch >= 'a' && ch <= 'z') || 240 (ch >= 'A' && ch <= 'Z') || 241 (ch >= '0' && ch <= '9') || 242 ch == '-' || ch == '.' || ch == '_' || ch == '/' || ch == '@' || ch == '^' || ch == '+') { 243 return true 244 } 245 } 246 return false 247} 248 249func (f *TextFormatter) appendKeyValue(b *bytes.Buffer, key string, value interface{}) { 250 if b.Len() > 0 { 251 b.WriteByte(' ') 252 } 253 b.WriteString(key) 254 b.WriteByte('=') 255 f.appendValue(b, value) 256} 257 258func (f *TextFormatter) appendValue(b *bytes.Buffer, value interface{}) { 259 stringVal, ok := value.(string) 260 if !ok { 261 stringVal = fmt.Sprint(value) 262 } 263 264 if !f.needsQuoting(stringVal) { 265 b.WriteString(stringVal) 266 } else { 267 b.WriteString(fmt.Sprintf("%q", stringVal)) 268 } 269} 270