hook.go 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. package log
  2. import (
  3. "bufio"
  4. log "github.com/sirupsen/logrus"
  5. "io"
  6. "io/ioutil"
  7. "os"
  8. "strings"
  9. "sync"
  10. )
  11. // custom logrus hook
  12. // hookMu ensures all io operations are synced. Always on exported functions
  13. var hookMu sync.Mutex
  14. // LoggerHook extends the log.Hook interface by adding Reopen() and Rename()
  15. type LoggerHook interface {
  16. log.Hook
  17. Reopen() error
  18. }
  19. type LogrusHook struct {
  20. w io.Writer
  21. // file descriptor, can be re-opened
  22. fd *os.File
  23. // filename to the file descriptor
  24. fname string
  25. // txtFormatter that doesn't use colors
  26. plainTxtFormatter *log.TextFormatter
  27. mu sync.Mutex
  28. }
  29. // newLogrusHook creates a new hook. dest can be a file name or one of the following strings:
  30. // "stderr" - log to stderr, lines will be written to os.Stdout
  31. // "stdout" - log to stdout, lines will be written to os.Stdout
  32. // "off" - no log, lines will be written to ioutil.Discard
  33. func NewLogrusHook(dest string) (LoggerHook, error) {
  34. hookMu.Lock()
  35. defer hookMu.Unlock()
  36. hook := LogrusHook{fname: dest}
  37. err := hook.setup(dest)
  38. return &hook, err
  39. }
  40. type OutputOption int
  41. const (
  42. OutputStderr OutputOption = 1 + iota
  43. OutputStdout
  44. OutputOff
  45. OutputNull
  46. OutputFile
  47. )
  48. var outputOptions = [...]string{
  49. "stderr",
  50. "stdout",
  51. "off",
  52. "",
  53. "file",
  54. }
  55. func (o OutputOption) String() string {
  56. return outputOptions[o-1]
  57. }
  58. func parseOutputOption(str string) OutputOption {
  59. switch str {
  60. case "stderr":
  61. return OutputStderr
  62. case "stdout":
  63. return OutputStdout
  64. case "off":
  65. return OutputOff
  66. case "":
  67. return OutputNull
  68. }
  69. return OutputFile
  70. }
  71. // Setup sets the hook's writer w and file descriptor fd
  72. // assumes the hook.fd is closed and nil
  73. func (hook *LogrusHook) setup(dest string) error {
  74. out := parseOutputOption(dest)
  75. if out == OutputNull || out == OutputStderr {
  76. hook.w = os.Stderr
  77. } else if out == OutputStdout {
  78. hook.w = os.Stdout
  79. } else if out == OutputOff {
  80. hook.w = ioutil.Discard
  81. } else {
  82. if _, err := os.Stat(dest); err == nil {
  83. // file exists open the file for appending
  84. if err := hook.openAppend(dest); err != nil {
  85. return err
  86. }
  87. } else {
  88. // create the file
  89. if err := hook.openCreate(dest); err != nil {
  90. return err
  91. }
  92. }
  93. }
  94. // disable colors when writing to file
  95. if hook.fd != nil {
  96. hook.plainTxtFormatter = &log.TextFormatter{DisableColors: true}
  97. }
  98. return nil
  99. }
  100. // openAppend opens the dest file for appending. Default to os.Stderr if it can't open dest
  101. func (hook *LogrusHook) openAppend(dest string) (err error) {
  102. fd, err := os.OpenFile(dest, os.O_APPEND|os.O_WRONLY, 0644)
  103. if err != nil {
  104. log.WithError(err).Error("Could not open log file for appending")
  105. hook.w = os.Stderr
  106. hook.fd = nil
  107. return
  108. }
  109. hook.w = bufio.NewWriter(fd)
  110. hook.fd = fd
  111. return
  112. }
  113. // openCreate creates a new dest file for appending. Default to os.Stderr if it can't open dest
  114. func (hook *LogrusHook) openCreate(dest string) (err error) {
  115. fd, err := os.OpenFile(dest, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
  116. if err != nil {
  117. log.WithError(err).Error("Could not create log file")
  118. hook.w = os.Stderr
  119. hook.fd = nil
  120. return
  121. }
  122. hook.w = bufio.NewWriter(fd)
  123. hook.fd = fd
  124. return
  125. }
  126. // Fire implements the logrus Hook interface. It disables color text formatting if writing to a file
  127. func (hook *LogrusHook) Fire(entry *log.Entry) error {
  128. hookMu.Lock()
  129. defer hookMu.Unlock()
  130. if line, err := entry.String(); err == nil {
  131. r := strings.NewReader(line)
  132. if _, err = io.Copy(hook.w, r); err != nil {
  133. return err
  134. }
  135. if wb, ok := hook.w.(*bufio.Writer); ok {
  136. if err := wb.Flush(); err != nil {
  137. return err
  138. }
  139. if hook.fd != nil {
  140. err = hook.fd.Sync()
  141. }
  142. }
  143. return err
  144. } else {
  145. return err
  146. }
  147. }
  148. // Levels implements the logrus Hook interface
  149. func (hook *LogrusHook) Levels() []log.Level {
  150. return log.AllLevels
  151. }
  152. // Reopen closes and re-open log file descriptor, which is a special feature of this hook
  153. func (hook *LogrusHook) Reopen() error {
  154. hookMu.Lock()
  155. defer hookMu.Unlock()
  156. var err error
  157. if hook.fd != nil {
  158. if err = hook.fd.Close(); err != nil {
  159. return err
  160. }
  161. // The file could have been re-named by an external program such as logrotate(8)
  162. if _, err := os.Stat(hook.fname); err != nil {
  163. // The file doesn't exist, create a new one.
  164. return hook.openCreate(hook.fname)
  165. } else {
  166. return hook.openAppend(hook.fname)
  167. }
  168. }
  169. return err
  170. }