UNPKG

7.86 kBtext/coffeescriptView Raw
1fs = require 'fs'
2path = require 'path'
3util = require 'util'
4events = require 'events'
5moment = require 'moment'
6mkdirp = require 'mkdirp'
7os = require 'options-stream'
8levels = require './levels'
9colors = require './colors'
10timeout = require './timeout'
11pattern = require './pattern'
12Stream = require './stream'
13
14# lazy levels
15{info, debug, warn, error} = levels
16
17cwd = process.cwd()
18
19defaultLogFile = "
20[#{cwd}/logs/#{path.basename (path.basename process.argv[1] , '.js'), '.coffee'}-]YYYY-MM-DD[.log]
21"
22
23# log rotate minimum ms
24MIN_ROTATE_MS = 100
25
26class JustLog extends events.EventEmitter
27
28 ###
29 /**
30 * @param {Object} options
31 * - {String} [encodeing='utf-8'], log text encoding
32 * - file :
33 * - {Number} [level=error|warn], file log levels
34 * - {String} [pattern='file'], log line pattern
35 * - {String} [mode='0664'], log file mode
36 * - {String} [dir_mode='2775'], log dir mode
37 * - {String} [path="[$CWD/logs/$MAIN_FILE_BASENAME-]YYYY-MM-DD[.log]"], log file path pattern
38 * - stdio:
39 * - {Number} [level=all], file log levels
40 * - {String} [pattern='color'], log line pattern
41 * - {WritableStream} [stdout=process.stdout], info & debug output stream
42 * - {WritableStream} [stderr=process.stderr], warn & error output stream
43 ###
44 constructor : (options)->
45 @options = os {
46 encoding : 'utf-8'
47 file : {
48 level : error | warn
49 pattern : 'file'
50 path : defaultLogFile
51 mode : '0664'
52 dir_mode : '2775'
53 _watcher_timeout : 5007
54 }
55 stdio : {
56 level : error | warn | debug | info
57 pattern : 'color'
58 stdout : process.stdout
59 stderr : process.stderr
60 }
61 duration : 1000
62 bufferLength : 0
63 }, options
64
65 # options fix
66 @options.file = false if @options.file.level is 0
67 @options.stdio = false if @options.stdio.level is 0
68
69 # set level define as properties
70 @[k.toUpperCase()] = v for k, v of levels.levels # levels const
71
72 # file info init
73 @file =
74 path : @options.file.path, stream : null, timer : null, opening : false
75 watcher: null, ino: null
76 @closed = false
77
78 # need stdio
79 if @options.stdio
80 @stdout = @options.stdio.stdout
81 @stderr = @options.stdio.stderr
82 @options.stdio.render = pattern.compile @options.stdio.pattern
83
84 # need file
85 if @options.file
86 @options.file.render = pattern.compile @options.file.pattern
87 @_initFile()
88
89 @[k] = @[k].bind @ for k in ['info', 'debug', 'warn', 'error']
90
91 @lastCheckTime = @lastFlushTime = new Date().getTime()
92
93 # overwrite emit
94 emit : (args...)->
95 super args...
96 super 'all', args...
97 return
98
99 # check log file renamed
100 _checkFileRenamed : (cb)->
101 # check need file has stream stream opened
102 if @options.file is false or @file.stream is null or @file.opening is true
103 cb null, false
104 return
105
106 # get file stat
107 fs.stat @file.path, (err, stat) =>
108 # stat error
109 if err
110 if err.code is 'ENOENT' # file not exists, renamed
111 cb null, true
112 else # other error
113 cb err
114 return
115
116 prev = @file.ino # save prev inode
117 @file.ino = stat.ino # set curr ino
118
119 if prev is null or prev is stat.ino # first stat or inode unchanged
120 cb null, false
121 else # inode changed
122 cb null, true
123 return
124
125 _checkFile : ->
126 @_checkFileRenamed (err, changed)=>
127 return @emit err if err
128
129 return if changed is false
130 @_closeStream() # close prev stream
131 @_newStream() # open new stream
132 @emit 'rename', @file.path
133 return
134 return
135
136 _setFilePath : ->
137 filePath = path.normalize moment().format @options.file.path
138 filePath = path.relative cwd, filePath if path[0] is '/'
139 @file.path = filePath
140
141 _newStream : ->
142 filePath = @file.path
143 # mkdir
144 try
145 mkdirp.sync path.dirname(filePath), @options.file.dir_mode
146 catch err
147 @emit 'error', err
148 # open flag
149 @file.opening = true
150
151 stream = Stream filePath : filePath, bufferLength : @options.bufferLength
152
153 # open new stream
154 # stream = fs.createWriteStream filePath, flags: 'a', mode: @options.file.mode
155 stream.on 'error', @emit.bind @ # on error
156 stream.on 'open', => # opened
157 @file.ino = null
158 @file.opening = false
159 @file.stream = stream
160
161
162 _closeStream : ->
163 @file.stream.end() # end stream
164 # @file.stream.destroySoon() # destory after drain
165 @file.stream = null # clear object
166 return
167
168
169 _initFile : ->
170 # set file path
171 @_setFilePath()
172 @_newStream()
173 # @file.watcher = setInterval @_checkFile.bind(@), @options.file._watcher_timeout
174 @_rotateFile()
175
176
177
178 _rotateFile : ->
179 [ms] = timeout @options.file.path # get next timeout (ms)
180 return if null is ms # return if log file has no rotate rules
181
182 # fix timeout <= MIN_ROTATE_MS
183 ms = MIN_ROTATE_MS if ms <= MIN_ROTATE_MS
184 # remove old timeout
185 if @file.timer isnt null
186
187 clearTimeout @timer
188 @timer = null
189
190
191 # set timeout
192 @file.timer = setTimeout @_rotateFile.bind(@), ms
193 process.nextTick =>
194 @emit 'timer', ms # async emit 'timer-start'
195
196 # check filepath changed
197 prev = @file.path
198 @_setFilePath()
199 if prev isnt @file.path
200 @_closeStream() # close old stream
201 @_newStream() # make new stream
202 @emit 'rotate', prev, @file.path
203 return
204
205 _fileLog : (msg, level) ->
206 line = pattern.format @options.file.render, msg, level
207 @file.stream.write line, @options.encoding
208
209 _stdioLog : (msg, level) ->
210 # console.log @options.stdio.render, @options.stdio.render.toString()
211 line = pattern.format @options.stdio.render, msg, level
212 # console.log line
213 (if level & (error|warn) then @stderr else @stdout).write line, @options.encoding
214
215 _log : (msg, level) ->
216 if msg.length isnt 1 or typeof msg[0] isnt 'object'
217 msg = util.format msg...
218 else
219 msg = msg[0]
220 @_fileLog msg, level if @options.file && (@options.file.level & level)
221 @_stdioLog msg, level if @options.stdio && (@options.stdio.level & level)
222 @
223
224 ###
225 /**
226 * send an info log
227 * @param {Mixed} msg... log info (run as console.log)
228 * @return {JustLog} return self object for chain call
229 ###
230 info : (msg...) -> @_log msg, info
231 ###
232 /**
233 * send an debug log
234 * @param {Mixed} msg... log info (run as console.log)
235 * @return {JustLog} return self object for chain call
236 ###
237 debug : (msg...) -> @_log msg, debug
238 ###
239 /**
240 * send an warn log
241 * @param {Mixed} msg... log info (run as console.log)
242 * @return {JustLog} return self object for chain call
243 ###
244 warn : (msg...) -> @_log msg, warn
245 ###
246 /**
247 * send an error log
248 * @param {Mixed} msg... log info (run as console.log)
249 * @return {JustLog} return self object for chain call
250 ###
251 error : (msg...) -> @_log msg, error
252
253 ###
254 /**
255 * close log
256 * @param {Function} cb after close callback
257 ###
258 close : (cb) ->
259 if @options.file is false or @closed
260 process.nextTick cb if cb
261 return
262 @closed = true
263 @file.stream.on 'close', cb if cb and @file.stream
264 @_closeStream()
265 # if @file.watcher
266 # clearInterval @file.watcher
267 # @file.watcher = null
268 if @file.timer
269 clearTimeout @file.timer
270 @file.timer = null
271 return
272
273 heartBeat : (now)->
274 if now - @lastFlushTime > @options.duration and @file.stream
275 @file.stream.flush()
276 @lastFlushTime = now
277 if now - @lastCheckTime > @options.file._watcher_timeout
278 @_checkFile()
279 @lastCheckTime = now
280
281module.exports = (options)->
282 new JustLog options