scrolls 0.3.7 → 0.9.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/Gemfile +2 -0
- data/README.md +52 -14
- data/Rakefile +4 -2
- data/docs/global-context.md +1 -1
- data/docs/syslog.md +14 -1
- data/lib/scrolls.rb +62 -83
- data/lib/scrolls/iologger.rb +14 -0
- data/lib/scrolls/logger.rb +318 -0
- data/lib/scrolls/parser.rb +3 -1
- data/lib/scrolls/sysloglogger.rb +17 -0
- data/lib/scrolls/utils.rb +55 -13
- data/lib/scrolls/version.rb +1 -1
- data/test/test_helper.rb +4 -1
- data/test/test_parser.rb +11 -2
- data/test/test_scrolls.rb +78 -41
- metadata +6 -9
- data/lib/scrolls/atomic.rb +0 -59
- data/lib/scrolls/log.rb +0 -274
- data/lib/scrolls/syslog.rb +0 -46
- data/test/test_atomic.rb +0 -33
@@ -0,0 +1,318 @@
|
|
1
|
+
require "syslog"
|
2
|
+
|
3
|
+
require "scrolls/parser"
|
4
|
+
require "scrolls/iologger"
|
5
|
+
require "scrolls/sysloglogger"
|
6
|
+
require "scrolls/utils"
|
7
|
+
|
8
|
+
module Scrolls
|
9
|
+
# Default log facility
|
10
|
+
LOG_FACILITY = ENV['LOG_FACILITY'] || Syslog::LOG_USER
|
11
|
+
|
12
|
+
# Default log level
|
13
|
+
LOG_LEVEL = (ENV['LOG_LEVEL'] || 6).to_i
|
14
|
+
|
15
|
+
# Default syslog options
|
16
|
+
SYSLOG_OPTIONS = Syslog::LOG_PID|Syslog::LOG_CONS
|
17
|
+
|
18
|
+
class TimeUnitError < RuntimeError; end
|
19
|
+
class LogLevelError < StandardError; end
|
20
|
+
|
21
|
+
# Top level class to hold our global context
|
22
|
+
#
|
23
|
+
# Global context is defined using Scrolls#init
|
24
|
+
class GlobalContext
|
25
|
+
def initialize(ctx)
|
26
|
+
@ctx = ctx || {}
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_h
|
30
|
+
@ctx
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class Logger
|
35
|
+
|
36
|
+
attr_reader :logger
|
37
|
+
attr_accessor :exceptions, :timestamp
|
38
|
+
|
39
|
+
def initialize(options={})
|
40
|
+
@stream = options.fetch(:stream, STDOUT)
|
41
|
+
@log_facility = options.fetch(:facility, LOG_FACILITY)
|
42
|
+
@time_unit = options.fetch(:time_unit, "seconds")
|
43
|
+
@timestamp = options.fetch(:timestamp, false)
|
44
|
+
@exceptions = options.fetch(:exceptions, "single")
|
45
|
+
@global_ctx = options.fetch(:global_context, {})
|
46
|
+
@syslog_opts = options.fetch(:syslog_options, SYSLOG_OPTIONS)
|
47
|
+
@escape_keys = options.fetch(:escape_keys, false)
|
48
|
+
|
49
|
+
# Our main entry point to ensure our options are setup properly
|
50
|
+
setup!
|
51
|
+
end
|
52
|
+
|
53
|
+
def context
|
54
|
+
if Thread.current.thread_variables.include?(:scrolls_context)
|
55
|
+
Thread.current.thread_variable_get(:scrolls_context)
|
56
|
+
else
|
57
|
+
Thread.current.thread_variable_set(:scrolls_context, {})
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def context=(h)
|
62
|
+
Thread.current.thread_variable_set(:scrolls_context, h || {})
|
63
|
+
end
|
64
|
+
|
65
|
+
def stream
|
66
|
+
@stream
|
67
|
+
end
|
68
|
+
|
69
|
+
def stream=(s)
|
70
|
+
# Return early to avoid setup
|
71
|
+
return if s == @stream
|
72
|
+
|
73
|
+
@stream = s
|
74
|
+
setup_stream
|
75
|
+
end
|
76
|
+
|
77
|
+
def escape_keys?
|
78
|
+
@escape_keys
|
79
|
+
end
|
80
|
+
|
81
|
+
def syslog_options
|
82
|
+
@syslog_opts
|
83
|
+
end
|
84
|
+
|
85
|
+
def facility
|
86
|
+
@facility
|
87
|
+
end
|
88
|
+
|
89
|
+
def facility=(f)
|
90
|
+
if f
|
91
|
+
setup_facility(f)
|
92
|
+
# If we are using syslog, we need to setup our connection again
|
93
|
+
if stream == "syslog"
|
94
|
+
@logger = Scrolls::SyslogLogger.new(
|
95
|
+
progname,
|
96
|
+
syslog_options,
|
97
|
+
facility
|
98
|
+
)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def time_unit
|
104
|
+
@time_unit
|
105
|
+
end
|
106
|
+
|
107
|
+
def time_unit=(u)
|
108
|
+
@time_unit = u
|
109
|
+
setup_time_unit
|
110
|
+
end
|
111
|
+
|
112
|
+
def global_context
|
113
|
+
@global_context.to_h
|
114
|
+
end
|
115
|
+
|
116
|
+
def log(data, &blk)
|
117
|
+
# If we get a string lets bring it into our structure.
|
118
|
+
if data.kind_of? String
|
119
|
+
rawhash = { "log_message" => data }
|
120
|
+
else
|
121
|
+
rawhash = data
|
122
|
+
end
|
123
|
+
|
124
|
+
if gc = @global_context.to_h
|
125
|
+
ctx = gc.merge(context)
|
126
|
+
logdata = ctx.merge(rawhash)
|
127
|
+
end
|
128
|
+
|
129
|
+
# By merging the logdata into the timestamp, rather than vice-versa, we
|
130
|
+
# ensure that the timestamp comes first in the Hash, and is placed first
|
131
|
+
# on the output, which helps with readability.
|
132
|
+
logdata = { :now => Time.now.utc }.merge(logdata) if prepend_timestamp?
|
133
|
+
|
134
|
+
unless blk
|
135
|
+
write(logdata)
|
136
|
+
else
|
137
|
+
start = Time.now
|
138
|
+
res = nil
|
139
|
+
log(logdata.merge(:at => "start"))
|
140
|
+
begin
|
141
|
+
res = yield
|
142
|
+
rescue StandardError => e
|
143
|
+
logdata.merge!({
|
144
|
+
at: "exception",
|
145
|
+
reraise: true,
|
146
|
+
class: e.class,
|
147
|
+
message: e.message,
|
148
|
+
exception_id: e.object_id.abs,
|
149
|
+
elapsed: calculate_time(start, Time.now)
|
150
|
+
})
|
151
|
+
logdata.delete_if { |k,v| k if v == "" }
|
152
|
+
log(logdata)
|
153
|
+
raise e
|
154
|
+
end
|
155
|
+
log(logdata.merge(:at => "finish", :elapsed => calculate_time(start, Time.now)))
|
156
|
+
res
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def log_exception(e, data=nil)
|
161
|
+
unless @defined
|
162
|
+
@stream = STDERR
|
163
|
+
setup_stream
|
164
|
+
end
|
165
|
+
|
166
|
+
# We check our arguments for type
|
167
|
+
case data
|
168
|
+
when String
|
169
|
+
rawhash = { "log_message" => data }
|
170
|
+
when Hash
|
171
|
+
rawhash = data
|
172
|
+
else
|
173
|
+
rawhash = {}
|
174
|
+
end
|
175
|
+
|
176
|
+
if gc = @global_context.to_h
|
177
|
+
logdata = gc.merge(rawhash)
|
178
|
+
end
|
179
|
+
|
180
|
+
excepdata = {
|
181
|
+
at: "exception",
|
182
|
+
class: e.class,
|
183
|
+
message: e.message,
|
184
|
+
exception_id: e.object_id.abs
|
185
|
+
}
|
186
|
+
|
187
|
+
excepdata.delete_if { |k,v| k if v == "" }
|
188
|
+
|
189
|
+
if e.backtrace
|
190
|
+
if single_line_exceptions?
|
191
|
+
lines = e.backtrace.map { |line| line.gsub(/[`'"]/, "") }
|
192
|
+
|
193
|
+
if lines.length > 0
|
194
|
+
excepdata[:site] = lines.join('\n')
|
195
|
+
log(logdata.merge(excepdata))
|
196
|
+
end
|
197
|
+
else
|
198
|
+
log(logdata.merge(excepdata))
|
199
|
+
|
200
|
+
e.backtrace.each do |line|
|
201
|
+
log(logdata.merge(excepdata).merge(
|
202
|
+
:at => "exception",
|
203
|
+
:class => e.class,
|
204
|
+
:exception_id => e.object_id.abs,
|
205
|
+
:site => line.gsub(/[`'"]/, "")
|
206
|
+
))
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
def with_context(prefix)
|
213
|
+
return unless block_given?
|
214
|
+
old = context
|
215
|
+
self.context = old.merge(prefix)
|
216
|
+
res = yield if block_given?
|
217
|
+
ensure
|
218
|
+
self.context = old
|
219
|
+
res
|
220
|
+
end
|
221
|
+
|
222
|
+
private
|
223
|
+
|
224
|
+
def setup!
|
225
|
+
setup_global_context
|
226
|
+
prepend_timestamp?
|
227
|
+
setup_facility
|
228
|
+
setup_stream
|
229
|
+
single_line_exceptions?
|
230
|
+
setup_time_unit
|
231
|
+
end
|
232
|
+
|
233
|
+
def setup_global_context
|
234
|
+
# Builds up an immutable object for our global_context
|
235
|
+
# This is not backwards compatiable and was introduced after 0.3.7.
|
236
|
+
# Removes ability to add to global context once we initialize our
|
237
|
+
# logging object. This also deprecates #add_global_context.
|
238
|
+
@global_context = GlobalContext.new(@global_ctx)
|
239
|
+
@global_context.freeze
|
240
|
+
end
|
241
|
+
|
242
|
+
def prepend_timestamp?
|
243
|
+
@timestamp
|
244
|
+
end
|
245
|
+
|
246
|
+
def setup_facility(f=nil)
|
247
|
+
if f
|
248
|
+
@facility = LOG_FACILITY_MAP.fetch(f, LOG_FACILITY)
|
249
|
+
else
|
250
|
+
@facility = LOG_FACILITY_MAP.fetch(@log_facility, LOG_FACILITY)
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
def setup_stream
|
255
|
+
unless @stream == STDOUT
|
256
|
+
# Set this so we know we aren't using our default stream
|
257
|
+
@defined = true
|
258
|
+
end
|
259
|
+
|
260
|
+
if @stream == "syslog"
|
261
|
+
@logger = Scrolls::SyslogLogger.new(
|
262
|
+
progname,
|
263
|
+
syslog_options,
|
264
|
+
facility
|
265
|
+
)
|
266
|
+
else
|
267
|
+
@logger = IOLogger.new(@stream)
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
def single_line_exceptions?
|
272
|
+
return false if @exceptions == "multi"
|
273
|
+
true
|
274
|
+
end
|
275
|
+
|
276
|
+
def setup_time_unit
|
277
|
+
unless %w{s ms seconds milliseconds}.include? @time_unit
|
278
|
+
raise TimeUnitError, "Specify the following: s, ms, seconds, milliseconds"
|
279
|
+
end
|
280
|
+
|
281
|
+
case @time_unit
|
282
|
+
when %w{s seconds}
|
283
|
+
@t = 1.0
|
284
|
+
when %w{ms milliseconds}
|
285
|
+
@t = 1000.0
|
286
|
+
else
|
287
|
+
@t = 1.0
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
# We need this for our syslog setup
|
292
|
+
def progname
|
293
|
+
File.basename($0)
|
294
|
+
end
|
295
|
+
|
296
|
+
def calculate_time(start, finish)
|
297
|
+
translate_time_unit unless @t
|
298
|
+
((finish - start).to_f * @t)
|
299
|
+
end
|
300
|
+
|
301
|
+
def log_level_ok?(level)
|
302
|
+
if level
|
303
|
+
raise LogLevelError, "Log level unknown" unless LOG_LEVEL_MAP.key?(level)
|
304
|
+
LOG_LEVEL_MAP[level.to_s] <= LOG_LEVEL
|
305
|
+
else
|
306
|
+
true
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
def write(data)
|
311
|
+
if log_level_ok?(data[:level])
|
312
|
+
msg = Scrolls::Parser.unparse(data, escape_keys=escape_keys?)
|
313
|
+
@logger.log(msg)
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
end
|
318
|
+
end
|
data/lib/scrolls/parser.rb
CHANGED
@@ -0,0 +1,17 @@
|
|
1
|
+
module Scrolls
|
2
|
+
class SyslogLogger
|
3
|
+
def initialize(ident = 'scrolls',
|
4
|
+
options = Scrolls::SYSLOG_OPTIONS,
|
5
|
+
facility = Scrolls::LOG_FACILITY)
|
6
|
+
if Syslog.opened?
|
7
|
+
@syslog = Syslog.reopen(ident, options, facility)
|
8
|
+
else
|
9
|
+
@syslog = Syslog.open(ident, options, facility)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def log(data)
|
14
|
+
@syslog.log(Syslog::LOG_INFO, "%s", data)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/scrolls/utils.rb
CHANGED
@@ -1,19 +1,61 @@
|
|
1
1
|
module Scrolls
|
2
|
-
module Utils
|
3
2
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
3
|
+
# Helpful map of syslog facilities
|
4
|
+
LOG_FACILITY_MAP = {
|
5
|
+
"auth" => Syslog::LOG_AUTH,
|
6
|
+
"authpriv" => Syslog::LOG_AUTHPRIV,
|
7
|
+
"cron" => Syslog::LOG_CRON,
|
8
|
+
"daemon" => Syslog::LOG_DAEMON,
|
9
|
+
"ftp" => Syslog::LOG_FTP,
|
10
|
+
"kern" => Syslog::LOG_KERN,
|
11
|
+
"mail" => Syslog::LOG_MAIL,
|
12
|
+
"news" => Syslog::LOG_NEWS,
|
13
|
+
"syslog" => Syslog::LOG_SYSLOG,
|
14
|
+
"user" => Syslog::LOG_USER,
|
15
|
+
"uucp" => Syslog::LOG_UUCP,
|
16
|
+
"local0" => Syslog::LOG_LOCAL0,
|
17
|
+
"local1" => Syslog::LOG_LOCAL1,
|
18
|
+
"local2" => Syslog::LOG_LOCAL2,
|
19
|
+
"local3" => Syslog::LOG_LOCAL3,
|
20
|
+
"local4" => Syslog::LOG_LOCAL4,
|
21
|
+
"local5" => Syslog::LOG_LOCAL5,
|
22
|
+
"local6" => Syslog::LOG_LOCAL6,
|
23
|
+
"local7" => Syslog::LOG_LOCAL7,
|
24
|
+
}
|
25
|
+
|
26
|
+
# Helpful map of syslog log levels
|
27
|
+
LOG_LEVEL_MAP = {
|
28
|
+
"emerg" => 0, # Syslog::LOG_EMERG
|
29
|
+
"emergency" => 0, # Syslog::LOG_EMERG
|
30
|
+
"alert" => 1, # Syslog::LOG_ALERT
|
31
|
+
"crit" => 2, # Syslog::LOG_CRIT
|
32
|
+
"critical" => 2, # Syslog::LOG_CRIT
|
33
|
+
"error" => 3, # Syslog::LOG_ERR
|
34
|
+
"warn" => 4, # Syslog::LOG_WARNING
|
35
|
+
"warning" => 4, # Syslog::LOG_WARNING
|
36
|
+
"notice" => 5, # Syslog::LOG_NOTICE
|
37
|
+
"info" => 6, # Syslog::LOG_INFO
|
38
|
+
"debug" => 7 # Syslog::LOG_DEBUG
|
39
|
+
}
|
40
|
+
|
41
|
+
ESCAPE_CHAR = {
|
42
|
+
"&" => "&",
|
43
|
+
"<" => "<",
|
44
|
+
">" => ">",
|
45
|
+
"'" => "'",
|
46
|
+
'"' => """,
|
47
|
+
"/" => "/"
|
48
|
+
}
|
49
|
+
|
50
|
+
ESCAPE_CHAR_PATTERN = Regexp.union(*ESCAPE_CHAR.keys)
|
51
|
+
|
52
|
+
module Utils
|
11
53
|
|
12
|
-
def
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
54
|
+
def self.escape_chars(d)
|
55
|
+
if d.is_a?(String) and d =~ ESCAPE_CHAR_PATTERN
|
56
|
+
esc = d.to_s.gsub(ESCAPE_CHAR_PATTERN) {|c| ESCAPE_CHAR[c] }
|
57
|
+
else
|
58
|
+
esc = d
|
17
59
|
end
|
18
60
|
end
|
19
61
|
|
data/lib/scrolls/version.rb
CHANGED
data/test/test_helper.rb
CHANGED