file.go 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  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"
  19. "io/ioutil"
  20. "log"
  21. "os"
  22. "path/filepath"
  23. "strings"
  24. "time"
  25. )
  26. const (
  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. // Indicates whether object is been used in standalone mode.
  56. standalone bool
  57. *log.Logger
  58. Adapter
  59. file *os.File
  60. filename string
  61. openDay int
  62. currentSize int64
  63. currentLines int64
  64. rotate FileRotationConfig
  65. }
  66. func newFile() Logger {
  67. return &file{
  68. Adapter: Adapter{
  69. quitChan: make(chan struct{}),
  70. },
  71. }
  72. }
  73. // NewFileWriter returns an io.Writer for synchronized file logger in standalone mode.
  74. func NewFileWriter(filename string, cfg FileRotationConfig) (io.Writer, error) {
  75. f := &file{
  76. standalone: true,
  77. }
  78. if err := f.Init(FileConfig{
  79. Filename: filename,
  80. FileRotationConfig: cfg,
  81. }); err != nil {
  82. return nil, err
  83. }
  84. return f, nil
  85. }
  86. func (f *file) Level() LEVEL { return f.level }
  87. var newLineBytes = []byte("\n")
  88. func (f *file) initFile() (err error) {
  89. f.file, err = os.OpenFile(f.filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0660)
  90. if err != nil {
  91. return fmt.Errorf("OpenFile '%s': %v", f.filename, err)
  92. }
  93. f.Logger = log.New(f.file, "", log.Ldate|log.Ltime)
  94. return nil
  95. }
  96. // isExist checks whether a file or directory exists.
  97. // It returns false when the file or directory does not exist.
  98. func isExist(path string) bool {
  99. _, err := os.Stat(path)
  100. return err == nil || os.IsExist(err)
  101. }
  102. // rotateFilename returns next available rotate filename with given date.
  103. func (f *file) rotateFilename(date string) string {
  104. filename := fmt.Sprintf("%s.%s", f.filename, date)
  105. if !isExist(filename) {
  106. return filename
  107. }
  108. format := filename + ".%03d"
  109. for i := 1; i < 1000; i++ {
  110. filename := fmt.Sprintf(format, i)
  111. if !isExist(filename) {
  112. return filename
  113. }
  114. }
  115. panic("too many log files for yesterday")
  116. }
  117. func (f *file) deleteOutdatedFiles() {
  118. filepath.Walk(filepath.Dir(f.filename), func(path string, info os.FileInfo, err error) error {
  119. if !info.IsDir() &&
  120. info.ModTime().Before(time.Now().Add(-24*time.Hour*time.Duration(f.rotate.MaxDays))) &&
  121. strings.HasPrefix(filepath.Base(path), filepath.Base(f.filename)) {
  122. os.Remove(path)
  123. }
  124. return nil
  125. })
  126. }
  127. func (f *file) initRotate() error {
  128. // Gather basic file info for rotation.
  129. fi, err := f.file.Stat()
  130. if err != nil {
  131. return fmt.Errorf("Stat: %v", err)
  132. }
  133. f.currentSize = fi.Size()
  134. // If there is any content in the file, count the number of lines.
  135. if f.rotate.MaxLines > 0 && f.currentSize > 0 {
  136. data, err := ioutil.ReadFile(f.filename)
  137. if err != nil {
  138. return fmt.Errorf("ReadFile '%s': %v", f.filename, err)
  139. }
  140. f.currentLines = int64(bytes.Count(data, newLineBytes)) + 1
  141. }
  142. if f.rotate.Daily {
  143. now := time.Now()
  144. f.openDay = now.Day()
  145. lastWriteTime := fi.ModTime()
  146. if lastWriteTime.Year() != now.Year() ||
  147. lastWriteTime.Month() != now.Month() ||
  148. lastWriteTime.Day() != now.Day() {
  149. if err = f.file.Close(); err != nil {
  150. return fmt.Errorf("Close: %v", err)
  151. }
  152. if err = os.Rename(f.filename, f.rotateFilename(lastWriteTime.Format(SIMPLE_DATE_FORMAT))); err != nil {
  153. return fmt.Errorf("Rename: %v", err)
  154. }
  155. if err = f.initFile(); err != nil {
  156. return fmt.Errorf("initFile: %v", err)
  157. }
  158. }
  159. }
  160. if f.rotate.MaxDays > 0 {
  161. f.deleteOutdatedFiles()
  162. }
  163. return nil
  164. }
  165. func (f *file) Init(v interface{}) (err error) {
  166. cfg, ok := v.(FileConfig)
  167. if !ok {
  168. return ErrConfigObject{"FileConfig", v}
  169. }
  170. if !isValidLevel(cfg.Level) {
  171. return ErrInvalidLevel{}
  172. }
  173. f.level = cfg.Level
  174. f.filename = cfg.Filename
  175. os.MkdirAll(filepath.Dir(f.filename), os.ModePerm)
  176. if err = f.initFile(); err != nil {
  177. return fmt.Errorf("initFile: %v", err)
  178. }
  179. f.rotate = cfg.FileRotationConfig
  180. if f.rotate.Rotate {
  181. f.initRotate()
  182. }
  183. if !f.standalone {
  184. f.msgChan = make(chan *Message, cfg.BufferSize)
  185. }
  186. return nil
  187. }
  188. func (f *file) ExchangeChans(errorChan chan<- error) chan *Message {
  189. f.errorChan = errorChan
  190. return f.msgChan
  191. }
  192. func (f *file) write(msg *Message) int {
  193. f.Logger.Print(msg.Body)
  194. bytesWrote := len(msg.Body)
  195. if !f.standalone {
  196. bytesWrote += LOG_PREFIX_LENGTH
  197. }
  198. if f.rotate.Rotate {
  199. f.currentSize += int64(bytesWrote)
  200. f.currentLines++ // TODO: should I care if log message itself contains new lines?
  201. var (
  202. needsRotate = false
  203. rotateDate time.Time
  204. )
  205. now := time.Now()
  206. if f.rotate.Daily && now.Day() != f.openDay {
  207. needsRotate = true
  208. rotateDate = now.Add(-24 * time.Hour)
  209. } else if (f.rotate.MaxSize > 0 && f.currentSize >= f.rotate.MaxSize) ||
  210. (f.rotate.MaxLines > 0 && f.currentLines >= f.rotate.MaxLines) {
  211. needsRotate = true
  212. rotateDate = now
  213. }
  214. if needsRotate {
  215. f.file.Close()
  216. if err := os.Rename(f.filename, f.rotateFilename(rotateDate.Format(SIMPLE_DATE_FORMAT))); err != nil {
  217. f.errorChan <- fmt.Errorf("fail to rename rotate file '%s': %v", f.filename, err)
  218. }
  219. if err := f.initFile(); err != nil {
  220. f.errorChan <- fmt.Errorf("fail to init log file '%s': %v", f.filename, err)
  221. }
  222. f.openDay = now.Day()
  223. f.currentSize = 0
  224. f.currentLines = 0
  225. }
  226. }
  227. return bytesWrote
  228. }
  229. var _ io.Writer = new(file)
  230. // Write implements method of io.Writer interface.
  231. func (f *file) Write(p []byte) (int, error) {
  232. return f.write(&Message{
  233. Body: string(p),
  234. }), nil
  235. }
  236. func (f *file) Start() {
  237. LOOP:
  238. for {
  239. select {
  240. case msg := <-f.msgChan:
  241. f.write(msg)
  242. case <-f.quitChan:
  243. break LOOP
  244. }
  245. }
  246. for {
  247. if len(f.msgChan) == 0 {
  248. break
  249. }
  250. f.write(<-f.msgChan)
  251. }
  252. f.quitChan <- struct{}{} // Notify the cleanup is done.
  253. }
  254. func (f *file) Destroy() {
  255. f.quitChan <- struct{}{}
  256. <-f.quitChan
  257. close(f.msgChan)
  258. close(f.quitChan)
  259. f.file.Close()
  260. }
  261. func init() {
  262. Register(FILE, newFile)
  263. }