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.4KB

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