You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

logger.go 7.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. // Copyright (c) 2017 Daniel Oaks <daniel@danieloaks.net>
  2. // released under the MIT license
  3. package logger
  4. import (
  5. "bufio"
  6. "fmt"
  7. "os"
  8. "time"
  9. "strings"
  10. "sync"
  11. colorable "github.com/mattn/go-colorable"
  12. "github.com/mgutz/ansi"
  13. )
  14. // Level represents the level to log messages at.
  15. type Level int
  16. const (
  17. // LogDebug represents debug messages.
  18. LogDebug Level = iota
  19. // LogInfo represents informational messages.
  20. LogInfo
  21. // LogWarning represents warnings.
  22. LogWarning
  23. // LogError represents errors.
  24. LogError
  25. )
  26. var (
  27. // LogLevelNames takes a config name and gives the real log level.
  28. LogLevelNames = map[string]Level{
  29. "debug": LogDebug,
  30. "info": LogInfo,
  31. "warn": LogWarning,
  32. "warning": LogWarning,
  33. "warnings": LogWarning,
  34. "error": LogError,
  35. "errors": LogError,
  36. }
  37. // LogLevelDisplayNames gives the display name to use for our log levels.
  38. LogLevelDisplayNames = map[Level]string{
  39. LogDebug: "debug",
  40. LogInfo: "info",
  41. LogWarning: "warning",
  42. LogError: "error",
  43. }
  44. )
  45. // Manager is the main interface used to log debug/info/error messages.
  46. type Manager struct {
  47. configMutex sync.RWMutex
  48. loggers []singleLogger
  49. stdoutWriteLock sync.Mutex // use one lock for both stdout and stderr
  50. fileWriteLock sync.Mutex
  51. loggingRawIO bool
  52. }
  53. // LoggingConfig represents the configuration of a single logger.
  54. type LoggingConfig struct {
  55. Method string
  56. MethodStdout bool
  57. MethodStderr bool
  58. MethodFile bool
  59. Filename string
  60. TypeString string `yaml:"type"`
  61. Types []string `yaml:"real-types"`
  62. ExcludedTypes []string `yaml:"real-excluded-types"`
  63. LevelString string `yaml:"level"`
  64. Level Level `yaml:"level-real"`
  65. }
  66. // NewManager returns a new log manager.
  67. func NewManager(config []LoggingConfig) (*Manager, error) {
  68. var logger Manager
  69. if err := logger.ApplyConfig(config); err != nil {
  70. return nil, err
  71. }
  72. return &logger, nil
  73. }
  74. // ApplyConfig applies the given config to this logger (rehashes the config, in other words).
  75. func (logger *Manager) ApplyConfig(config []LoggingConfig) error {
  76. logger.configMutex.Lock()
  77. defer logger.configMutex.Unlock()
  78. for _, logger := range logger.loggers {
  79. logger.Close()
  80. }
  81. logger.loggers = nil
  82. logger.loggingRawIO = false
  83. // for safety, this deep-copies all mutable data in `config`
  84. // XXX let's keep it that way
  85. var lastErr error
  86. for _, logConfig := range config {
  87. typeMap := make(map[string]bool)
  88. for _, name := range logConfig.Types {
  89. typeMap[name] = true
  90. }
  91. excludedTypeMap := make(map[string]bool)
  92. for _, name := range logConfig.ExcludedTypes {
  93. excludedTypeMap[name] = true
  94. }
  95. sLogger := singleLogger{
  96. MethodSTDOUT: logConfig.MethodStdout,
  97. MethodSTDERR: logConfig.MethodStderr,
  98. MethodFile: fileMethod{
  99. Enabled: logConfig.MethodFile,
  100. Filename: logConfig.Filename,
  101. },
  102. Level: logConfig.Level,
  103. Types: typeMap,
  104. ExcludedTypes: excludedTypeMap,
  105. stdoutWriteLock: &logger.stdoutWriteLock,
  106. fileWriteLock: &logger.fileWriteLock,
  107. }
  108. if typeMap["userinput"] || typeMap["useroutput"] || (typeMap["*"] && !(excludedTypeMap["userinput"] && excludedTypeMap["useroutput"])) {
  109. logger.loggingRawIO = true
  110. }
  111. if sLogger.MethodFile.Enabled {
  112. file, err := os.OpenFile(sLogger.MethodFile.Filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0666)
  113. if err != nil {
  114. lastErr = fmt.Errorf("Could not open log file %s [%s]", sLogger.MethodFile.Filename, err.Error())
  115. }
  116. writer := bufio.NewWriter(file)
  117. sLogger.MethodFile.File = file
  118. sLogger.MethodFile.Writer = writer
  119. }
  120. logger.loggers = append(logger.loggers, sLogger)
  121. }
  122. return lastErr
  123. }
  124. // IsLoggingRawIO returns true if raw user input and output is being logged.
  125. func (logger *Manager) IsLoggingRawIO() bool {
  126. logger.configMutex.RLock()
  127. defer logger.configMutex.RUnlock()
  128. return logger.loggingRawIO
  129. }
  130. // Log logs the given message with the given details.
  131. func (logger *Manager) Log(level Level, logType string, messageParts ...string) {
  132. logger.configMutex.RLock()
  133. defer logger.configMutex.RUnlock()
  134. for _, singleLogger := range logger.loggers {
  135. singleLogger.Log(level, logType, messageParts...)
  136. }
  137. }
  138. // Debug logs the given message as a debug message.
  139. func (logger *Manager) Debug(logType string, messageParts ...string) {
  140. logger.Log(LogDebug, logType, messageParts...)
  141. }
  142. // Info logs the given message as an info message.
  143. func (logger *Manager) Info(logType string, messageParts ...string) {
  144. logger.Log(LogInfo, logType, messageParts...)
  145. }
  146. // Warning logs the given message as a warning message.
  147. func (logger *Manager) Warning(logType string, messageParts ...string) {
  148. logger.Log(LogWarning, logType, messageParts...)
  149. }
  150. // Error logs the given message as an error message.
  151. func (logger *Manager) Error(logType string, messageParts ...string) {
  152. logger.Log(LogError, logType, messageParts...)
  153. }
  154. // Fatal logs the given message as an error message, then exits.
  155. func (logger *Manager) Fatal(logType string, messageParts ...string) {
  156. logger.Error(logType, messageParts...)
  157. logger.Error("FATAL", "Fatal error encountered, application exiting")
  158. os.Exit(1)
  159. }
  160. type fileMethod struct {
  161. Enabled bool
  162. Filename string
  163. File *os.File
  164. Writer *bufio.Writer
  165. }
  166. // singleLogger represents a single logger instance.
  167. type singleLogger struct {
  168. stdoutWriteLock *sync.Mutex
  169. fileWriteLock *sync.Mutex
  170. MethodSTDOUT bool
  171. MethodSTDERR bool
  172. MethodFile fileMethod
  173. Level Level
  174. Types map[string]bool
  175. ExcludedTypes map[string]bool
  176. }
  177. func (logger *singleLogger) Close() error {
  178. if logger.MethodFile.Enabled {
  179. flushErr := logger.MethodFile.Writer.Flush()
  180. closeErr := logger.MethodFile.File.Close()
  181. if flushErr != nil {
  182. return flushErr
  183. }
  184. return closeErr
  185. }
  186. return nil
  187. }
  188. // Log logs the given message with the given details.
  189. func (logger *singleLogger) Log(level Level, logType string, messageParts ...string) {
  190. // no logging enabled
  191. if !(logger.MethodSTDOUT || logger.MethodSTDERR || logger.MethodFile.Enabled) {
  192. return
  193. }
  194. // ensure we're logging to the given level
  195. if level < logger.Level {
  196. return
  197. }
  198. // ensure we're capturing this logType
  199. logTypeCleaned := strings.ToLower(strings.TrimSpace(logType))
  200. capturing := (logger.Types["*"] || logger.Types[logTypeCleaned]) && !logger.ExcludedTypes["*"] && !logger.ExcludedTypes[logTypeCleaned]
  201. if !capturing {
  202. return
  203. }
  204. // assemble full line
  205. timeGrey := ansi.ColorFunc("243")
  206. grey := ansi.ColorFunc("8")
  207. alert := ansi.ColorFunc("232+b:red")
  208. warn := ansi.ColorFunc("black:214")
  209. info := ansi.ColorFunc("117")
  210. debug := ansi.ColorFunc("78")
  211. section := ansi.ColorFunc("229")
  212. levelDisplay := LogLevelDisplayNames[level]
  213. if level == LogError {
  214. levelDisplay = alert(levelDisplay)
  215. } else if level == LogWarning {
  216. levelDisplay = warn(levelDisplay)
  217. } else if level == LogInfo {
  218. levelDisplay = info(levelDisplay)
  219. } else if level == LogDebug {
  220. levelDisplay = debug(levelDisplay)
  221. }
  222. sep := grey(":")
  223. fullStringFormatted := fmt.Sprintf("%s %s %s %s %s %s ", timeGrey(time.Now().UTC().Format("2006-01-02T15:04:05Z")), sep, levelDisplay, sep, section(logType), sep)
  224. fullStringRaw := fmt.Sprintf("%s : %s : %s : ", time.Now().UTC().Format("2006-01-02T15:04:05Z"), LogLevelDisplayNames[level], logType)
  225. for i, p := range messageParts {
  226. fullStringFormatted += p
  227. fullStringRaw += p
  228. if i != len(messageParts)-1 {
  229. fullStringFormatted += " " + sep + " "
  230. fullStringRaw += " : "
  231. }
  232. }
  233. // output
  234. if logger.MethodSTDOUT {
  235. logger.stdoutWriteLock.Lock()
  236. fmt.Fprintln(colorable.NewColorableStdout(), fullStringFormatted)
  237. logger.stdoutWriteLock.Unlock()
  238. }
  239. if logger.MethodSTDERR {
  240. logger.stdoutWriteLock.Lock()
  241. fmt.Fprintln(colorable.NewColorableStderr(), fullStringFormatted)
  242. logger.stdoutWriteLock.Unlock()
  243. }
  244. if logger.MethodFile.Enabled {
  245. logger.fileWriteLock.Lock()
  246. logger.MethodFile.Writer.WriteString(fullStringRaw + "\n")
  247. logger.MethodFile.Writer.Flush()
  248. logger.fileWriteLock.Unlock()
  249. }
  250. }