file.go 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. // Copyright 2017 Unknwon
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License"): you may
  4. // not use this file except in compliance with the License. You may obtain
  5. // a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  11. // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  12. // License for the specific language governing permissions and limitations
  13. // under the License.
  14. package clog
  15. import (
  16. "bytes"
  17. "fmt"
  18. "io/ioutil"
  19. "log"
  20. "os"
  21. "path/filepath"
  22. "strings"
  23. "time"
  24. )
  25. const (
  26. FILE MODE = "file"
  27. SIMPLE_DATE_FORMAT = "2006-01-02"
  28. LOG_PREFIX_LENGTH = len("2017/02/06 21:20:08 ")
  29. )
  30. // FileRotationConfig represents rotation related configurations for file mode logger.
  31. // All the settings can take effect at the same time, remain zero values to disable them.
  32. type FileRotationConfig struct {
  33. // Do rotation for output files.
  34. Rotate bool
  35. // Rotate on daily basis.
  36. Daily bool
  37. // Maximum size in bytes of file for a rotation.
  38. MaxSize int64
  39. // Maximum number of lines for a rotation.
  40. MaxLines int64
  41. // Maximum lifetime of a output file in days.
  42. MaxDays int64
  43. }
  44. type FileConfig struct {
  45. // Minimum level of messages to be processed.
  46. Level LEVEL
  47. // Buffer size defines how many messages can be queued before hangs.
  48. BufferSize int64
  49. // File name to outout messages.
  50. Filename string
  51. // Rotation related configurations.
  52. FileRotationConfig
  53. }
  54. type file struct {
  55. *log.Logger
  56. Adapter
  57. file *os.File
  58. filename string
  59. openDay int
  60. currentSize int64
  61. currentLines int64
  62. rotate FileRotationConfig
  63. }
  64. func newFile() Logger {
  65. return &file{
  66. Adapter: Adapter{
  67. quitChan: make(chan struct{}),
  68. },
  69. }
  70. }
  71. func (f *file) Level() LEVEL { return f.level }
  72. var newLineBytes = []byte("\n")
  73. func (f *file) initFile() (err error) {
  74. f.file, err = os.OpenFile(f.filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0660)
  75. if err != nil {
  76. return fmt.Errorf("OpenFile '%s': %v", f.filename, err)
  77. }
  78. f.Logger = log.New(f.file, "", log.Ldate|log.Ltime)
  79. return nil
  80. }
  81. // isExist checks whether a file or directory exists.
  82. // It returns false when the file or directory does not exist.
  83. func isExist(path string) bool {
  84. _, err := os.Stat(path)
  85. return err == nil || os.IsExist(err)
  86. }
  87. // rotateFilename returns next available rotate filename with given date.
  88. func (f *file) rotateFilename(date string) string {
  89. filename := fmt.Sprintf("%s.%s", f.filename, date)
  90. if !isExist(filename) {
  91. return filename
  92. }
  93. format := filename + ".%03d"
  94. for i := 1; i < 1000; i++ {
  95. filename := fmt.Sprintf(format, i)
  96. if !isExist(filename) {
  97. return filename
  98. }
  99. }
  100. panic("too many log files for yesterday")
  101. }
  102. func (f *file) deleteOutdatedFiles() {
  103. filepath.Walk(filepath.Dir(f.filename), func(path string, info os.FileInfo, err error) error {
  104. if !info.IsDir() &&
  105. info.ModTime().Before(time.Now().Add(-24*time.Hour*time.Duration(f.rotate.MaxDays))) &&
  106. strings.HasPrefix(filepath.Base(path), filepath.Base(f.filename)) {
  107. os.Remove(path)
  108. }
  109. return nil
  110. })
  111. }
  112. func (f *file) initRotate() error {
  113. // Gather basic file info for rotation.
  114. fi, err := f.file.Stat()
  115. if err != nil {
  116. return fmt.Errorf("Stat: %v", err)
  117. }
  118. f.currentSize = fi.Size()
  119. // If there is any content in the file, count the number of lines.
  120. if f.rotate.MaxLines > 0 && f.currentSize > 0 {
  121. data, err := ioutil.ReadFile(f.filename)
  122. if err != nil {
  123. return fmt.Errorf("ReadFile '%s': %v", f.filename, err)
  124. }
  125. f.currentLines = int64(bytes.Count(data, newLineBytes)) + 1
  126. }
  127. if f.rotate.Daily {
  128. now := time.Now()
  129. f.openDay = now.Day()
  130. lastWriteTime := fi.ModTime()
  131. if lastWriteTime.Year() != now.Year() ||
  132. lastWriteTime.Month() != now.Month() ||
  133. lastWriteTime.Day() != now.Day() {
  134. if err = f.file.Close(); err != nil {
  135. return fmt.Errorf("Close: %v", err)
  136. }
  137. if err = os.Rename(f.filename, f.rotateFilename(lastWriteTime.Format(SIMPLE_DATE_FORMAT))); err != nil {
  138. return fmt.Errorf("Rename: %v", err)
  139. }
  140. if err = f.initFile(); err != nil {
  141. return fmt.Errorf("initFile: %v", err)
  142. }
  143. }
  144. }
  145. if f.rotate.MaxDays > 0 {
  146. f.deleteOutdatedFiles()
  147. }
  148. return nil
  149. }
  150. func (f *file) Init(v interface{}) (err error) {
  151. cfg, ok := v.(FileConfig)
  152. if !ok {
  153. return ErrConfigObject{"FileConfig", v}
  154. }
  155. if !isValidLevel(cfg.Level) {
  156. return ErrInvalidLevel{}
  157. }
  158. f.level = cfg.Level
  159. f.filename = cfg.Filename
  160. os.MkdirAll(filepath.Dir(f.filename), os.ModePerm)
  161. if err = f.initFile(); err != nil {
  162. return fmt.Errorf("initFile: %v", err)
  163. }
  164. f.rotate = cfg.FileRotationConfig
  165. if f.rotate.Rotate {
  166. f.initRotate()
  167. }
  168. f.msgChan = make(chan *Message, cfg.BufferSize)
  169. return nil
  170. }
  171. func (f *file) ExchangeChans(errorChan chan<- error) chan *Message {
  172. f.errorChan = errorChan
  173. return f.msgChan
  174. }
  175. func (f *file) write(msg *Message) {
  176. f.Logger.Print(msg.Body)
  177. if f.rotate.Rotate {
  178. f.currentSize += int64(LOG_PREFIX_LENGTH + len(msg.Body))
  179. f.currentLines++ // TODO: should I care if log message itself contains new lines?
  180. var (
  181. needsRotate = false
  182. rotateDate time.Time
  183. )
  184. now := time.Now()
  185. if f.rotate.Daily && now.Day() != f.openDay {
  186. needsRotate = true
  187. rotateDate = now.Add(-24 * time.Hour)
  188. } else if (f.rotate.MaxSize > 0 && f.currentSize >= f.rotate.MaxSize) ||
  189. (f.rotate.MaxLines > 0 && f.currentLines >= f.rotate.MaxLines) {
  190. needsRotate = true
  191. rotateDate = now
  192. }
  193. if needsRotate {
  194. f.file.Close()
  195. if err := os.Rename(f.filename, f.rotateFilename(rotateDate.Format(SIMPLE_DATE_FORMAT))); err != nil {
  196. f.errorChan <- fmt.Errorf("fail to rename rotate file '%s': %v", f.filename, err)
  197. }
  198. if err := f.initFile(); err != nil {
  199. f.errorChan <- fmt.Errorf("fail to init log file '%s': %v", f.filename, err)
  200. }
  201. f.openDay = now.Day()
  202. f.currentSize = 0
  203. f.currentLines = 0
  204. }
  205. }
  206. }
  207. func (f *file) Start() {
  208. LOOP:
  209. for {
  210. select {
  211. case msg := <-f.msgChan:
  212. f.write(msg)
  213. case <-f.quitChan:
  214. break LOOP
  215. }
  216. }
  217. for {
  218. if len(f.msgChan) == 0 {
  219. break
  220. }
  221. f.write(<-f.msgChan)
  222. }
  223. f.quitChan <- struct{}{} // Notify the cleanup is done.
  224. }
  225. func (f *file) Destroy() {
  226. f.quitChan <- struct{}{}
  227. <-f.quitChan
  228. close(f.msgChan)
  229. close(f.quitChan)
  230. f.file.Close()
  231. }
  232. func init() {
  233. Register(FILE, newFile)
  234. }