scrolls 0.3.9 → 0.9.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 764cf377dd151093b60740032200866af5dfe033
4
- data.tar.gz: ad73c8e48a7baa90366ca0e1a05eea887f719fb3
2
+ SHA256:
3
+ metadata.gz: d70168aa379a644e25091bce26f7ce86e3ffb9d6564a97246d3e6251fbe353ad
4
+ data.tar.gz: 813b7fc23789ea3436e4f18f1be99f601ec034fbf737e70bfbf8705948676aed
5
5
  SHA512:
6
- metadata.gz: 43df84fb536e8f94aa15e99471def2587ad5a8abe73ba018392d4aa9deacfff21eb1d2fbc11458670f43d459fe7a3cedeb190436bd5d93a997f7f2374fd35383
7
- data.tar.gz: d565408e3abf2d46ce1f0561e9dbee59003f5041bbec07a0bdae3cd099295ec9cbf77c3bd8f2df97b1b15b797c1c7d452c1099df2d59ae53b444cb49e5c32424
6
+ metadata.gz: a419f6a9473eeaa2f7957db5d30ee5d1977149861542a7e28feb7e126f7413ee6a9876ff76e30414620fcf8a43eaa0abafe79408b23db0c4415a06f048ad5653
7
+ data.tar.gz: 3be6e7a630e21b35be17bf59ac480a40f2b98eec106c27dacfc245765574e317c680b5587a55064e5883b420c272ca1cdab687ac2052316fdbf53efdde7f9e45
data/Gemfile CHANGED
@@ -4,5 +4,7 @@ source 'https://rubygems.org'
4
4
  gemspec
5
5
 
6
6
  group :test do
7
+ gem "minitest", require: 'minitest/autorun'
8
+ gem "minitest-reporters"
7
9
  gem "rake"
8
10
  end
data/README.md CHANGED
@@ -20,26 +20,65 @@ Or install it yourself as:
20
20
 
21
21
  Scrolls follows the belief that logs should be treated as data. One way to think of them is the blood of your infrastructure. Logs are a realtime view of what is happening on your systems.
22
22
 
23
- ## Need to know!
23
+ ## Usage
24
+
25
+ ### 0.9.0 and later
24
26
 
25
- The way Scrolls handles "global_context" is changing after v0.3.8. Please see the [release notes](https://github.com/asenchi/scrolls/releases/tag/v0.3.8) and [this documentation](https://github.com/asenchi/scrolls/tree/master/docs/global-context.md) for more information. I apologize for any trouble this may cause.
27
+ ```ruby
28
+ require 'scrolls'
26
29
 
27
- ## Documentation:
30
+ Scrolls.init(
31
+ timestamp: true,
32
+ global_context: {app: "scrolls", deploy: "production"},
33
+ exceptions: "multi"
34
+ )
28
35
 
29
- I apologize, some of these are a WIP.
36
+ Scrolls.log(at: "test")
30
37
 
31
- * [Sending logs to syslog using Scrolls](https://github.com/asenchi/scrolls/tree/master/docs/syslog.md)
32
- * Logging contexts
33
- * Adding timestamps by default
34
- * Misc Features
38
+ Scrolls.context(context: "block") do
39
+ Scrolls.log(at: "exec")
40
+ end
35
41
 
36
- ## Usage
42
+ begin
43
+ raise
44
+ rescue Exception => e
45
+ Scrolls.log_exception(e, at: "raise")
46
+ end
47
+ ```
48
+
49
+ You can also use `Scrolls#log` and `Scrolls#log_exception` without initalizing:
50
+
51
+ ```ruby
52
+ require 'scrolls'
53
+
54
+ Scrolls.log(test: "test")
55
+ ```
56
+
57
+ ### Defaults
58
+
59
+ Here are the defaults `Scrolls#init`:
60
+
61
+ ```
62
+ stream: STDOUT
63
+ facility: Syslog::LOG_USER
64
+ time_unit: "seconds"
65
+ timestamp: false
66
+ exceptions: "single"
67
+ global_context: {}
68
+ syslog_options: Syslog::LOG_PID|Syslog::LOG_CONS
69
+ escape_keys: false
70
+ strict_logfmt: false
71
+ ```
72
+
73
+ ## Older Versions
74
+
75
+ ### Pre 0.9.0
37
76
 
38
77
  ```ruby
39
78
  require 'scrolls'
40
79
 
41
80
  Scrolls.add_timestamp = true
42
- Scrolls.global_context(:app => "scrolls", :deploy => ENV["DEPLOY"])
81
+ Scrolls.global_context(:app => "scrolls", :deploy => "production")
43
82
 
44
83
  Scrolls.log(:at => "test")
45
84
 
@@ -57,10 +96,10 @@ end
57
96
  Produces:
58
97
 
59
98
  ```
60
- now="2014-01-17T16:11:39Z" app=scrolls deploy=nil at=test
61
- now="2014-01-17T16:11:39Z" app=scrolls deploy=nil context=block at=exec
62
- now="2014-01-17T16:11:39Z" app=scrolls deploy=nil at=exception class=RuntimeError message= exception_id=70312608019740
63
- now="2014-01-17T16:11:39Z" app=scrolls deploy=nil at=exception class= exception_id=70312608019740 site="./test.rb:16:in <main>"
99
+ now="2017-09-01T00:37:13Z" app=scrolls deploy=production at=test
100
+ now="2017-09-01T00:37:13Z" app=scrolls deploy=production context=block at=exec
101
+ now="2017-09-01T00:37:13Z" app=scrolls deploy=production at=exception class=RuntimeError exception_id=70149797587080
102
+ now="2017-09-01T00:37:13Z" app=scrolls deploy=production at=exception class=RuntimeError exception_id=70149797587080 site="./test-scrolls.rb:16:in <main>"
64
103
  ```
65
104
 
66
105
  ## History
data/Rakefile CHANGED
@@ -1,11 +1,13 @@
1
1
  #!/usr/bin/env rake
2
+
2
3
  require "bundler/gem_tasks"
4
+ require "rake/testtask"
3
5
 
4
6
  ENV['TESTOPTS'] = "-v"
5
7
 
6
- require "rake/testtask"
7
8
  Rake::TestTask.new do |t|
8
- t.pattern = "test/test_*.rb"
9
+ t.test_files = FileList["test/**/test_*.rb"]
10
+ t.verbose = true
9
11
  end
10
12
 
11
13
  task :default => :test
@@ -38,7 +38,7 @@ Scrolls.init(
38
38
  Scrolls.log(:t => "t")
39
39
  ```
40
40
 
41
- Is the same as this currently:
41
+ Is the same as this in versions prior to 0.9.0:
42
42
 
43
43
  ```ruby
44
44
  Scrolls.global_context(:g => "g")
data/docs/syslog.md CHANGED
@@ -6,6 +6,14 @@ By default Scrolls writes log messages to `STDOUT`. With the release of [v0.2.8]
6
6
  Scrolls.stream = "syslog"
7
7
  ```
8
8
 
9
+ Or using `Scrolls#init` in versions 0.9.0 and after:
10
+
11
+ ```ruby
12
+ Scrolls.init(
13
+ stream: "syslog"
14
+ )
15
+ ```
16
+
9
17
  This defaults to syslog facility USER and log level ERROR. You can adjust the log facility like so:
10
18
 
11
19
  ```ruby
@@ -14,4 +22,9 @@ Scrolls.facility = "local7"
14
22
 
15
23
  Scrolls generally doesn't care about log levels. The library defaults to ERROR (or 3), but ultimately is of the opinion that levels are useless. The reasoning behind this is that applications should log useful data, all of the time. Debugging data is great for development, but should never be deployed. The richness of structured logging allows exceptions and error messages to sit along side the context of the data in which the error was thrown, there is no need to send to an "emergency" level.
16
24
 
17
- With that said, if one wanted to adjust the log level, you can set an environment variable `LOG_LEVEL`. This allows this particular feature to be rather fluid throughout your application.
25
+ With that said, if one wanted to adjust the log level, you can set an environment variable `LOG_LEVEL` or use one of the level methods. This allows this particular feature to be rather fluid throughout your application.
26
+
27
+ ```ruby
28
+ Scrolls.info(d: "data")
29
+ Scrolls.warn(d: "data")
30
+ ```
@@ -1,7 +1,9 @@
1
1
  module Scrolls
2
- class IOLog
2
+ class IOLogger
3
3
  def initialize(stream)
4
- stream.sync = true
4
+ if stream.respond_to?(:sync)
5
+ stream.sync = true
6
+ end
5
7
  @stream = stream
6
8
  end
7
9
 
@@ -0,0 +1,323 @@
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
+ @strict_logfmt = options.fetch(:strict_logfmt, false)
49
+
50
+ # Our main entry point to ensure our options are setup properly
51
+ setup!
52
+ end
53
+
54
+ def context
55
+ if Thread.current.thread_variables.include?(:scrolls_context)
56
+ Thread.current.thread_variable_get(:scrolls_context)
57
+ else
58
+ Thread.current.thread_variable_set(:scrolls_context, {})
59
+ end
60
+ end
61
+
62
+ def context=(h)
63
+ Thread.current.thread_variable_set(:scrolls_context, h || {})
64
+ end
65
+
66
+ def stream
67
+ @stream
68
+ end
69
+
70
+ def stream=(s)
71
+ # Return early to avoid setup
72
+ return if s == @stream
73
+
74
+ @stream = s
75
+ setup_stream
76
+ end
77
+
78
+ def escape_keys?
79
+ @escape_keys
80
+ end
81
+
82
+ def strict_logfmt?
83
+ @strict_logfmt
84
+ end
85
+
86
+ def syslog_options
87
+ @syslog_opts
88
+ end
89
+
90
+ def facility
91
+ @facility
92
+ end
93
+
94
+ def facility=(f)
95
+ if f
96
+ setup_facility(f)
97
+ # If we are using syslog, we need to setup our connection again
98
+ if stream == "syslog"
99
+ @logger = Scrolls::SyslogLogger.new(
100
+ progname,
101
+ syslog_options,
102
+ facility
103
+ )
104
+ end
105
+ end
106
+ end
107
+
108
+ def time_unit
109
+ @time_unit
110
+ end
111
+
112
+ def time_unit=(u)
113
+ @time_unit = u
114
+ setup_time_unit
115
+ end
116
+
117
+ def global_context
118
+ @global_context.to_h
119
+ end
120
+
121
+ def log(data, &blk)
122
+ # If we get a string lets bring it into our structure.
123
+ if data.kind_of? String
124
+ rawhash = { "log_message" => data }
125
+ else
126
+ rawhash = data
127
+ end
128
+
129
+ if gc = @global_context.to_h
130
+ ctx = gc.merge(context)
131
+ logdata = ctx.merge(rawhash)
132
+ end
133
+
134
+ # By merging the logdata into the timestamp, rather than vice-versa, we
135
+ # ensure that the timestamp comes first in the Hash, and is placed first
136
+ # on the output, which helps with readability.
137
+ logdata = { :now => Time.now.utc }.merge(logdata) if prepend_timestamp?
138
+
139
+ unless blk
140
+ write(logdata)
141
+ else
142
+ start = Time.now
143
+ res = nil
144
+ log(logdata.merge(:at => "start"))
145
+ begin
146
+ res = yield
147
+ rescue StandardError => e
148
+ logdata.merge!({
149
+ at: "exception",
150
+ reraise: true,
151
+ class: e.class,
152
+ message: e.message,
153
+ exception_id: e.object_id.abs,
154
+ elapsed: calculate_time(start, Time.now)
155
+ })
156
+ logdata.delete_if { |k,v| k if v == "" }
157
+ log(logdata)
158
+ raise e
159
+ end
160
+ log(logdata.merge(:at => "finish", :elapsed => calculate_time(start, Time.now)))
161
+ res
162
+ end
163
+ end
164
+
165
+ def log_exception(e, data=nil)
166
+ unless @defined
167
+ @stream = STDERR
168
+ setup_stream
169
+ end
170
+
171
+ # We check our arguments for type
172
+ case data
173
+ when String
174
+ rawhash = { "log_message" => data }
175
+ when Hash
176
+ rawhash = data
177
+ else
178
+ rawhash = {}
179
+ end
180
+
181
+ if gc = @global_context.to_h
182
+ logdata = gc.merge(rawhash)
183
+ end
184
+
185
+ excepdata = {
186
+ at: "exception",
187
+ class: e.class,
188
+ message: e.message,
189
+ exception_id: e.object_id.abs
190
+ }
191
+
192
+ excepdata.delete_if { |k,v| k if v == "" }
193
+
194
+ if e.backtrace
195
+ if single_line_exceptions?
196
+ lines = e.backtrace.map { |line| line.gsub(/[`'"]/, "") }
197
+
198
+ if lines.length > 0
199
+ excepdata[:site] = lines.join('\n')
200
+ log(logdata.merge(excepdata))
201
+ end
202
+ else
203
+ log(logdata.merge(excepdata))
204
+
205
+ e.backtrace.each do |line|
206
+ log(logdata.merge(excepdata).merge(
207
+ :at => "exception",
208
+ :class => e.class,
209
+ :exception_id => e.object_id.abs,
210
+ :site => line.gsub(/[`'"]/, "")
211
+ ))
212
+ end
213
+ end
214
+ end
215
+ end
216
+
217
+ def with_context(prefix)
218
+ return unless block_given?
219
+ old = context
220
+ self.context = old.merge(prefix)
221
+ res = yield if block_given?
222
+ ensure
223
+ self.context = old
224
+ res
225
+ end
226
+
227
+ private
228
+
229
+ def setup!
230
+ setup_global_context
231
+ prepend_timestamp?
232
+ setup_facility
233
+ setup_stream
234
+ single_line_exceptions?
235
+ setup_time_unit
236
+ end
237
+
238
+ def setup_global_context
239
+ # Builds up an immutable object for our global_context
240
+ # This is not backwards compatiable and was introduced after 0.3.7.
241
+ # Removes ability to add to global context once we initialize our
242
+ # logging object. This also deprecates #add_global_context.
243
+ @global_context = GlobalContext.new(@global_ctx)
244
+ @global_context.freeze
245
+ end
246
+
247
+ def prepend_timestamp?
248
+ @timestamp
249
+ end
250
+
251
+ def setup_facility(f=nil)
252
+ if f
253
+ @facility = LOG_FACILITY_MAP.fetch(f, LOG_FACILITY)
254
+ else
255
+ @facility = LOG_FACILITY_MAP.fetch(@log_facility, LOG_FACILITY)
256
+ end
257
+ end
258
+
259
+ def setup_stream
260
+ unless @stream == STDOUT
261
+ # Set this so we know we aren't using our default stream
262
+ @defined = true
263
+ end
264
+
265
+ if @stream == "syslog"
266
+ @logger = Scrolls::SyslogLogger.new(
267
+ progname,
268
+ syslog_options,
269
+ facility
270
+ )
271
+ else
272
+ @logger = IOLogger.new(@stream)
273
+ end
274
+ end
275
+
276
+ def single_line_exceptions?
277
+ return false if @exceptions == "multi"
278
+ true
279
+ end
280
+
281
+ def setup_time_unit
282
+ unless %w{s ms seconds milliseconds}.include? @time_unit
283
+ raise TimeUnitError, "Specify the following: s, ms, seconds, milliseconds"
284
+ end
285
+
286
+ case @time_unit
287
+ when %w{s seconds}
288
+ @t = 1.0
289
+ when %w{ms milliseconds}
290
+ @t = 1000.0
291
+ else
292
+ @t = 1.0
293
+ end
294
+ end
295
+
296
+ # We need this for our syslog setup
297
+ def progname
298
+ File.basename($0)
299
+ end
300
+
301
+ def calculate_time(start, finish)
302
+ translate_time_unit unless @t
303
+ ((finish - start).to_f * @t)
304
+ end
305
+
306
+ def log_level_ok?(level)
307
+ if level
308
+ raise LogLevelError, "Log level unknown" unless LOG_LEVEL_MAP.key?(level)
309
+ LOG_LEVEL_MAP[level.to_s] <= LOG_LEVEL
310
+ else
311
+ true
312
+ end
313
+ end
314
+
315
+ def write(data)
316
+ if log_level_ok?(data[:level])
317
+ msg = Scrolls::Parser.unparse(data, escape_keys=escape_keys?, strict_logfmt=strict_logfmt?)
318
+ @logger.log(msg)
319
+ end
320
+ end
321
+
322
+ end
323
+ 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, strict_logfmt=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)
@@ -21,7 +23,7 @@ module Scrolls
21
23
  has_single_quote = v.index("'")
22
24
  has_double_quote = v.index('"')
23
25
  if v =~ /[ =:,]/
24
- if has_single_quote && has_double_quote
26
+ if (has_single_quote || strict_logfmt) && has_double_quote
25
27
  v = '"' + v.gsub(/\\|"/) { |c| "\\#{c}" } + '"'
26
28
  elsif has_double_quote
27
29
  v = "'" + v.gsub('\\', '\\\\\\') + "'"
@@ -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.9"
2
+ VERSION = "0.9.3"
3
3
  end