1 | fs = require 'fs'
|
2 | path = require 'path'
|
3 | util = require 'util'
|
4 | events = require 'events'
|
5 | moment = require 'moment'
|
6 | mkdirp = require 'mkdirp'
|
7 | os = require 'options-stream'
|
8 | levels = require './levels'
|
9 | colors = require './colors'
|
10 | timeout = require './timeout'
|
11 | pattern = require './pattern'
|
12 | Stream = require './stream'
|
13 |
|
14 |
|
15 | {info, debug, warn, error} = levels
|
16 |
|
17 | cwd = process.cwd()
|
18 |
|
19 | defaultLogFile = "
|
20 | [#{cwd}/logs/#{path.basename (path.basename process.argv[1] , '.js'), '.coffee'}-]YYYY-MM-DD[.log]
|
21 | "
|
22 |
|
23 |
|
24 | MIN_ROTATE_MS = 100
|
25 |
|
26 | class JustLog extends events.EventEmitter
|
27 |
|
28 | |
29 |
|
30 |
|
31 |
|
32 |
|
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 |
|
41 |
|
42 |
|
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 |
|
66 | @options.file = false if @options.file.level is 0
|
67 | @options.stdio = false if @options.stdio.level is 0
|
68 |
|
69 |
|
70 | @[k.toUpperCase()] = v for k, v of levels.levels
|
71 |
|
72 |
|
73 | @file =
|
74 | path : @options.file.path, stream : null, timer : null, opening : false
|
75 | watcher: null, ino: null
|
76 | @closed = false
|
77 |
|
78 |
|
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 |
|
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 |
|
94 | emit : (args...)->
|
95 | super args...
|
96 | super 'all', args...
|
97 | return
|
98 |
|
99 |
|
100 | _checkFileRenamed : (cb)->
|
101 |
|
102 | if @options.file is false or @file.stream is null or @file.opening is true
|
103 | cb null, false
|
104 | return
|
105 |
|
106 |
|
107 | fs.stat @file.path, (err, stat) =>
|
108 |
|
109 | if err
|
110 | if err.code is 'ENOENT'
|
111 | cb null, true
|
112 | else
|
113 | cb err
|
114 | return
|
115 |
|
116 | prev = @file.ino
|
117 | @file.ino = stat.ino
|
118 |
|
119 | if prev is null or prev is stat.ino
|
120 | cb null, false
|
121 | else
|
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()
|
131 | @_newStream()
|
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 |
|
144 | try
|
145 | mkdirp.sync path.dirname(filePath), @options.file.dir_mode
|
146 | catch err
|
147 | @emit 'error', err
|
148 |
|
149 | @file.opening = true
|
150 |
|
151 | stream = Stream filePath : filePath, bufferLength : @options.bufferLength
|
152 |
|
153 |
|
154 |
|
155 | stream.on 'error', @emit.bind @
|
156 | stream.on 'open', =>
|
157 | @file.ino = null
|
158 | @file.opening = false
|
159 | @file.stream = stream
|
160 |
|
161 |
|
162 | _closeStream : ->
|
163 | @file.stream.end()
|
164 |
|
165 | @file.stream = null
|
166 | return
|
167 |
|
168 |
|
169 | _initFile : ->
|
170 |
|
171 | @_setFilePath()
|
172 | @_newStream()
|
173 |
|
174 | @_rotateFile()
|
175 |
|
176 |
|
177 |
|
178 | _rotateFile : ->
|
179 | [ms] = timeout @options.file.path
|
180 | return if null is ms
|
181 |
|
182 |
|
183 | ms = MIN_ROTATE_MS if ms <= MIN_ROTATE_MS
|
184 |
|
185 | if @file.timer isnt null
|
186 |
|
187 | clearTimeout @timer
|
188 | @timer = null
|
189 |
|
190 |
|
191 |
|
192 | @file.timer = setTimeout @_rotateFile.bind(@), ms
|
193 | process.nextTick =>
|
194 | @emit 'timer', ms
|
195 |
|
196 |
|
197 | prev = @file.path
|
198 | @_setFilePath()
|
199 | if prev isnt @file.path
|
200 | @_closeStream()
|
201 | @_newStream()
|
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 |
|
211 | line = pattern.format @options.stdio.render, msg, level
|
212 |
|
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 |
|
227 |
|
228 |
|
229 |
|
230 | info : (msg...) -> @_log msg, info
|
231 | |
232 |
|
233 |
|
234 |
|
235 |
|
236 |
|
237 | debug : (msg...) -> @_log msg, debug
|
238 | |
239 |
|
240 |
|
241 |
|
242 |
|
243 |
|
244 | warn : (msg...) -> @_log msg, warn
|
245 | |
246 |
|
247 |
|
248 |
|
249 |
|
250 |
|
251 | error : (msg...) -> @_log msg, error
|
252 |
|
253 | |
254 |
|
255 |
|
256 |
|
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 |
|
266 |
|
267 |
|
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 |
|
281 | module.exports = (options)->
|
282 | new JustLog options
|