scrolls 0.3.7 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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"