scrolls 0.3.7 → 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,14 @@
1
+ module Scrolls
2
+ class IOLogger
3
+ def initialize(stream)
4
+ if stream.respond_to?(:sync)
5
+ stream.sync = true
6
+ end
7
+ @stream = stream
8
+ end
9
+
10
+ def log(data)
11
+ @stream.write("#{data}\n")
12
+ end
13
+ end
14
+ end
@@ -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
@@ -4,8 +4,10 @@ module Scrolls
4
4
  module Parser
5
5
  extend self
6
6
 
7
- def unparse(data)
7
+ def unparse(data, escape_keys=false)
8
8
  data.map do |(k,v)|
9
+ k = Scrolls::Utils.escape_chars(k) if escape_keys
10
+
9
11
  if (v == true)
10
12
  "#{k}=true"
11
13
  elsif (v == false)
@@ -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
- def hashify(d)
5
- last = d.pop
6
- return {} unless last
7
- return hashified_list(d).merge(last) if last.is_a?(Hash)
8
- d.push(last)
9
- hashified_list(d)
10
- end
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
+ "&" => "&amp;",
43
+ "<" => "&lt;",
44
+ ">" => "&gt;",
45
+ "'" => "&#x27;",
46
+ '"' => "&quot;",
47
+ "/" => "&#x2F;"
48
+ }
49
+
50
+ ESCAPE_CHAR_PATTERN = Regexp.union(*ESCAPE_CHAR.keys)
51
+
52
+ module Utils
11
53
 
12
- def hashified_list(l)
13
- return {} if l.empty?
14
- l.inject({}) do |h, i|
15
- h[i.to_sym] = true
16
- h
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
 
@@ -1,3 +1,3 @@
1
1
  module Scrolls
2
- VERSION = "0.3.7"
2
+ VERSION = "0.9.1"
3
3
  end
data/test/test_helper.rb CHANGED
@@ -1,6 +1,9 @@
1
- require "test/unit"
1
+ require "minitest/autorun"
2
+ require "minitest/reporters"
2
3
  require "stringio"
3
4
 
5
+ Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new
6
+
4
7
  $: << File.expand_path("../../lib", __FILE__)
5
8
 
6
9
  require "scrolls"