text_formatter.go 9.0 KB

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