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 | 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 |
|
67 | @options.file = false if @options.file.level is 0
|
68 | @options.stdio = false if @options.stdio.level is 0
|
69 |
|
70 |
|
71 | @[k.toUpperCase()] = v for k, v of levels.levels
|
72 |
|
73 |
|
74 | @file =
|
75 | path : @options.file.path, stream : null, timer : null, opening : false
|
76 | watcher: null, ino: null
|
77 | @closed = false
|
78 |
|
79 |
|
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 |
|
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 |
|
95 | emit : (args...)->
|
96 | super args...
|
97 | super 'all', args...
|
98 | return
|
99 |
|
100 |
|
101 | _checkFileRenamed : (cb)->
|
102 |
|
103 | if @options.file is false or @file.stream is null or @file.opening is true
|
104 | cb null, false
|
105 | return
|
106 |
|
107 |
|
108 | fs.stat @file.path, (err, stat) =>
|
109 |
|
110 | if err
|
111 | if err.code is 'ENOENT'
|
112 | cb null, true
|
113 | else
|
114 | cb err
|
115 | return
|
116 |
|
117 | prev = @file.ino
|
118 | @file.ino = stat.ino
|
119 |
|
120 | if prev is null or prev is stat.ino
|
121 | cb null, false
|
122 | else
|
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()
|
132 | @_newStream()
|
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 |
|
145 | try
|
146 | mkdirp.sync path.dirname(filePath), @options.file.dir_mode
|
147 | catch err
|
148 | @emit 'error', err
|
149 |
|
150 | @file.opening = true
|
151 |
|
152 | stream = Stream filePath : filePath, bufferLength : @options.bufferLength
|
153 |
|
154 |
|
155 |
|
156 | stream.on 'error', @emit.bind @
|
157 | stream.on 'open', =>
|
158 | @file.ino = null
|
159 | @file.opening = false
|
160 | @file.stream = stream
|
161 |
|
162 |
|
163 | _closeStream : ->
|
164 | @file.stream.end()
|
165 |
|
166 | @file.stream = null
|
167 | return
|
168 |
|
169 |
|
170 | _initFile : ->
|
171 |
|
172 | @_setFilePath()
|
173 | @_newStream()
|
174 |
|
175 | @_rotateFile()
|
176 |
|
177 |
|
178 |
|
179 | _rotateFile : ->
|
180 | [ms] = timeout @options.file.path
|
181 | return if null is ms
|
182 |
|
183 |
|
184 | ms = MIN_ROTATE_MS if ms <= MIN_ROTATE_MS
|
185 |
|
186 | if @file.timer isnt null
|
187 |
|
188 | clearTimeout @timer
|
189 | @timer = null
|
190 |
|
191 |
|
192 |
|
193 | @file.timer = setTimeout @_rotateFile.bind(@), ms
|
194 | process.nextTick =>
|
195 | @emit 'timer', ms
|
196 |
|
197 |
|
198 | prev = @file.path
|
199 | @_setFilePath()
|
200 | if prev isnt @file.path
|
201 | @_closeStream()
|
202 | @_newStream()
|
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 |
|
212 | line = pattern.format @options.stdio.render, msg, level
|
213 |
|
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 |
|
228 |
|
229 |
|
230 |
|
231 | info : (msg...) -> @_log msg, info
|
232 | |
233 |
|
234 |
|
235 |
|
236 |
|
237 |
|
238 | debug : (msg...) -> @_log msg, debug
|
239 | |
240 |
|
241 |
|
242 |
|
243 |
|
244 |
|
245 | warn : (msg...) -> @_log msg, warn
|
246 | |
247 |
|
248 |
|
249 |
|
250 |
|
251 |
|
252 | error : (msg...) -> @_log msg, error
|
253 |
|
254 | |
255 |
|
256 |
|
257 |
|
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 |
|
267 |
|
268 |
|
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 |
|
282 | module.exports = (options)->
|
283 | new JustLog options
|