github.com/kaydxh/golang@v0.0.131/pkg/logs/logrus/glog_formatter.go (about) 1 /* 2 *Copyright (c) 2022, kaydxh 3 * 4 *Permission is hereby granted, free of charge, to any person obtaining a copy 5 *of this software and associated documentation files (the "Software"), to deal 6 *in the Software without restriction, including without limitation the rights 7 *to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 *copies of the Software, and to permit persons to whom the Software is 9 *furnished to do so, subject to the following conditions: 10 * 11 *The above copyright notice and this permission notice shall be included in all 12 *copies or substantial portions of the Software. 13 * 14 *THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 *IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 *FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 *AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 *LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 *OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 *SOFTWARE. 21 */ 22 package logrus 23 24 import ( 25 "bytes" 26 "fmt" 27 "os" 28 "runtime" 29 "sort" 30 "strconv" 31 "strings" 32 "sync" 33 "time" 34 "unicode/utf8" 35 36 runtime_ "github.com/kaydxh/golang/go/runtime" 37 "github.com/sirupsen/logrus" 38 ) 39 40 const ( 41 red = 31 42 yellow = 33 43 blue = 36 44 gray = 37 45 ) 46 47 var ( 48 baseTimestamp time.Time 49 pid int 50 ) 51 52 func init() { 53 baseTimestamp = time.Now() 54 pid = os.Getpid() 55 } 56 57 /* 58 https://medium.com/technical-tips/google-log-glog-output-format-7eb31b3f0ce5 59 [IWEF]yyyymmdd hh:mm:ss.uuuuuu threadid file:line] msg 60 IWEF — Log Levels, I for INFO, W for WARNING, E for ERROR and `F` for FATAL. 61 yyyymmdd — Year, Month and Date. 62 hh:mm:ss.uuuuuu — Hours, Minutes, Seconds and Microseconds. 63 threadid — PID/TID of the process/thread. 64 file:line — File name and line number. 65 msg — Actual user-specified log message. 66 */ 67 68 // GlogFormatter formats logs into text 69 type GlogFormatter struct { 70 // Set to true to bypass checking for a TTY before outputting colors. 71 ForceColors bool 72 73 // Force disabling colors. 74 DisableColors bool 75 76 // Force quoting of all values 77 ForceQuote bool 78 79 // DisableQuote disables quoting for all values. 80 // DisableQuote will have a lower priority than ForceQuote. 81 // If both of them are set to true, quote will be forced on all values. 82 DisableQuote bool 83 84 // Override coloring based on CLICOLOR and CLICOLOR_FORCE. - https://bixense.com/clicolors/ 85 EnvironmentOverrideColors bool 86 87 // Disable timestamp logging. useful when output is redirected to logging 88 // system that already adds timestamps. 89 DisableTimestamp bool 90 91 // Enable logging the full timestamp when a TTY is attached instead of just 92 // the time passed since beginning of execution. 93 FullTimestamp bool 94 95 // TimestampFormat to use for display when a full timestamp is printed 96 TimestampFormat string 97 98 // The fields are sorted by default for a consistent output. For applications 99 // that log extremely frequently and don't use the JSON formatter this may not 100 // be desired. 101 DisableSorting bool 102 103 // The keys sorting function, when uninitialized it uses sort.Strings. 104 SortingFunc func([]string) 105 106 // Disables the truncation of the level text to 4 characters. 107 DisableLevelTruncation bool 108 109 // PadLevelText Adds padding the level text so that all the levels output at the same length 110 // PadLevelText is a superset of the DisableLevelTruncation option 111 PadLevelText bool 112 113 // QuoteEmptyFields will wrap empty fields in quotes if true 114 QuoteEmptyFields bool 115 116 // Whether the logger's out is to a terminal 117 isTerminal bool 118 119 // FieldMap allows users to customize the names of keys for default fields. 120 // As an example: 121 // formatter := &TextFormatter{ 122 // FieldMap: FieldMap{ 123 // FieldKeyTime: "@timestamp", 124 // FieldKeyLevel: "@level", 125 // FieldKeyMsg: "@message"}} 126 FieldMap FieldMap 127 128 // CallerPrettyfier can be set by the user to modify the content 129 // of the function and file keys in the data when ReportCaller is 130 // activated. If any of the returned value is the empty string the 131 // corresponding key will be removed from fields. 132 CallerPrettyfier func(*runtime.Frame) (function string, file string) 133 134 terminalInitOnce sync.Once 135 136 // The max length of the level text, generated dynamically on init 137 levelTextMaxLength int 138 139 // Disables the glog style :[IWEF]yyyymmdd hh:mm:ss.uuuuuu threadid file:line] msg msg... 140 // replace with :[IWEF] [yyyymmdd] [hh:mm:ss.uuuuuu] [threadid] [file:line] msg msg... 141 //EnablePrettyLog bool 142 EnableGoroutineId bool 143 } 144 145 func (f *GlogFormatter) init(entry *logrus.Entry) { 146 if entry.Logger != nil { 147 f.isTerminal = checkIfTerminal(entry.Logger.Out) 148 } 149 // Get the max length of the level text 150 for _, level := range logrus.AllLevels { 151 levelTextLength := utf8.RuneCount([]byte(level.String())) 152 if levelTextLength > f.levelTextMaxLength { 153 f.levelTextMaxLength = levelTextLength 154 } 155 } 156 } 157 158 func (f *GlogFormatter) isColored() bool { 159 isColored := f.ForceColors || (f.isTerminal && (runtime.GOOS != "windows")) 160 161 if f.EnvironmentOverrideColors { 162 switch force, ok := os.LookupEnv("CLICOLOR_FORCE"); { 163 case ok && force != "0": 164 isColored = true 165 case ok && force == "0", os.Getenv("CLICOLOR") == "0": 166 isColored = false 167 } 168 } 169 170 return isColored && !f.DisableColors 171 } 172 173 // Format renders a single log entry 174 func (f *GlogFormatter) Format(entry *logrus.Entry) ([]byte, error) { 175 data := make(logrus.Fields) 176 for k, v := range entry.Data { 177 data[k] = v 178 } 179 prefixFieldClashes(data, f.FieldMap, entry.HasCaller()) 180 keys := make([]string, 0, len(data)) 181 for k := range data { 182 keys = append(keys, k) 183 } 184 185 fixedKeys := make([]string, 0, 4+len(data)) 186 if entry.Message != "" { 187 fixedKeys = append(fixedKeys, f.FieldMap.resolve(logrus.FieldKeyMsg)) 188 } 189 190 if !f.DisableSorting { 191 if f.SortingFunc == nil { 192 sort.Strings(keys) 193 fixedKeys = append(fixedKeys, keys...) 194 } else { 195 if !f.isColored() { 196 fixedKeys = append(fixedKeys, keys...) 197 f.SortingFunc(fixedKeys) 198 } else { 199 f.SortingFunc(keys) 200 } 201 } 202 } else { 203 fixedKeys = append(fixedKeys, keys...) 204 } 205 206 var b *bytes.Buffer 207 if entry.Buffer != nil { 208 b = entry.Buffer 209 } else { 210 b = &bytes.Buffer{} 211 } 212 213 f.terminalInitOnce.Do(func() { f.init(entry) }) 214 215 timestampFormat := f.TimestampFormat 216 if timestampFormat == "" { 217 timestampFormat = defaultTimestampFormat 218 } 219 220 levelText := f.level(entry) 221 b.Write(f.formatHeader(entry, levelText)) 222 223 for _, key := range fixedKeys { 224 var value interface{} 225 switch { 226 case key == f.FieldMap.resolve(FieldKeyMsg): 227 value = entry.Message 228 f.appendValue(b, value) 229 //continue means msg not need call f.appendKeyValue(b, key, value) 230 //or will duplicate write message 231 continue 232 case key == f.FieldMap.resolve(fieldKey(logrus.ErrorKey)): 233 value = data[logrus.ErrorKey] 234 default: 235 value = data[key] 236 } 237 key = fmt.Sprintf("%s", key) 238 f.appendKeyValue(b, key, value) 239 } 240 241 b.WriteByte('\n') 242 return b.Bytes(), nil 243 } 244 245 func (f *GlogFormatter) level(entry *logrus.Entry) string { 246 levelText := strings.ToUpper(entry.Level.String()) 247 if !f.DisableLevelTruncation && !f.PadLevelText { 248 levelText = levelText[0:4] 249 } 250 if f.PadLevelText { 251 // Generates the format string used in the next line, for example "%-6s" or "%-7s". 252 // Based on the max level text length. 253 formatString := "%-" + strconv.Itoa(f.levelTextMaxLength) + "s" 254 // Formats the level text by appending spaces up to the max length, for example: 255 // - "INFO " 256 // - "WARNING" 257 levelText = fmt.Sprintf(formatString, levelText) 258 } 259 260 return levelText 261 } 262 263 func (f *GlogFormatter) needsQuoting(text string) bool { 264 if f.ForceQuote { 265 return true 266 } 267 if f.QuoteEmptyFields && len(text) == 0 { 268 return true 269 } 270 if f.DisableQuote { 271 return false 272 } 273 for _, ch := range text { 274 if !((ch >= 'a' && ch <= 'z') || 275 (ch >= 'A' && ch <= 'Z') || 276 (ch >= '0' && ch <= '9') || 277 ch == '-' || ch == '.' || ch == '_' || ch == '/' || ch == '@' || ch == '^' || ch == '+') { 278 return true 279 } 280 } 281 return false 282 } 283 284 func (f *GlogFormatter) appendKeyValue(b *bytes.Buffer, key string, value interface{}) { 285 if b.Len() > 0 { 286 b.WriteByte(' ') 287 } 288 b.WriteString(key) 289 b.WriteByte('=') 290 f.appendValue(b, value) 291 } 292 293 func (f *GlogFormatter) appendValue(b *bytes.Buffer, value interface{}) { 294 stringVal, ok := value.(string) 295 if !ok { 296 stringVal = fmt.Sprint(value) 297 } 298 299 if !f.needsQuoting(stringVal) { 300 b.WriteString(stringVal) 301 } else { 302 b.WriteString(fmt.Sprintf("%q", stringVal)) 303 } 304 } 305 306 // Log line format: [IWEF]yyyymmdd hh:mm:ss.uuuuuu threadid file:line] msg 307 func (f *GlogFormatter) formatHeader(entry *logrus.Entry, levelText string) []byte { 308 309 var buf bytes.Buffer 310 switch { 311 case f.DisableTimestamp: 312 buf.WriteString(fmt.Sprintf("[%s]", levelText)) 313 case !f.FullTimestamp: 314 buf.WriteString(fmt.Sprintf("[%s] [%04d]", levelText, int(entry.Time.Sub(baseTimestamp)/time.Second))) 315 default: 316 buf.WriteString(fmt.Sprintf("[%s] [%s]", levelText, 317 entry.Time.Format(f.TimestampFormat), 318 )) 319 } 320 321 if f.EnableGoroutineId { 322 buf.WriteString(fmt.Sprintf(" [%d]", runtime_.GoroutineID())) 323 } else { 324 //use pid instead of goroutine id 325 buf.WriteString(fmt.Sprintf(" [%d]", pid)) 326 } 327 328 var ( 329 function string 330 fileline string = "???:-1" 331 ) 332 333 if entry.HasCaller() { 334 if f.CallerPrettyfier != nil { 335 function, fileline = f.CallerPrettyfier(entry.Caller) 336 } else { 337 function = entry.Caller.Function 338 fileline = fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line) 339 } 340 341 buf.WriteString(fmt.Sprintf(" [%s](%s)", fileline, function)) 342 } 343 344 //split head and body 345 buf.WriteString(" ") 346 347 return buf.Bytes() 348 }