UNPKG

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