scrolls 0.9.0.pre → 0.9.4

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: b78c2d850fe048ad6f192be59e56b6d04daee1c5
4
- data.tar.gz: 49934f36ea64c5fe6a8a1b44ecb5689ea4574a75
2
+ SHA256:
3
+ metadata.gz: 2c46a03de696acee9e14e71d9e2a373415e9fa60532a7ac51896f71b5e0286ca
4
+ data.tar.gz: 9d9af16e271f1ff468fb778c8d5e05d9f215909adf77cdbf144204d2b3d8836e
5
5
  SHA512:
6
- metadata.gz: 9d05994bb1dfdd62a76ad372a56ac83270fe8853e19f5f402d86031e90b22eec8ff09f26d3208efe91e392a6b2d6391a56c8236eb3b6d29390183cbd8abf58ef
7
- data.tar.gz: c45dca818ec283f03f238de1201d5ea70198375b55a458e5a1441f2f8fa069f85198894cd11827496541084cd403ae356b6b9ce40376a499876f72be44f183fc
6
+ metadata.gz: 9aa2a50b5c00379872e54384b8f0b6050f82ab5f86e66bb2eca544a7ea5845ac4672dcc758e948d9b3af84b6d61af7972ca937be21854010002325c89b176865
7
+ data.tar.gz: 992fc115e62d9a2ea39305a72db4bfd0868c0f64d2e1a47082136ecf01e2d7a16461bbfb69e5bb38da628a4e1331685c4f6da275277e9793dfe5c5a8a808c496
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/LICENSE CHANGED
@@ -1,7 +1,7 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright © Curt Micol <asenchi@asenchi.com> 2012,2013,2014
4
- Copyright © Heroku 2012
3
+ Copyright (c) 2013-2021 Curt Micol <asenchi@asenchi.com>
4
+ Copyright (c) 2012, Heroku
5
5
 
6
6
  Permission is hereby granted, free of charge, to any person obtaining
7
7
  a copy of this software and associated documentation files (the
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,5 +1,5 @@
1
1
  module Scrolls
2
- class IOLog
2
+ class IOLogger
3
3
  def initialize(stream)
4
4
  if stream.respond_to?(:sync)
5
5
  stream.sync = true
@@ -1,100 +1,107 @@
1
+ require "syslog"
2
+
1
3
  require "scrolls/parser"
2
- require "scrolls/iolog"
3
- require "scrolls/syslog"
4
+ require "scrolls/iologger"
5
+ require "scrolls/sysloglogger"
6
+ require "scrolls/utils"
4
7
 
5
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
+
6
18
  class TimeUnitError < RuntimeError; end
19
+ class LogLevelError < StandardError; end
7
20
 
21
+ # Top level class to hold our global context
22
+ #
23
+ # Global context is defined using Scrolls#init
8
24
  class GlobalContext
9
- attr_reader :context
10
- def initialize(context)
11
- @context = context || {}
25
+ def initialize(ctx)
26
+ @ctx = ctx || {}
12
27
  end
13
28
 
14
29
  def to_h
15
- @context
30
+ @ctx
16
31
  end
17
32
  end
18
33
 
19
- class Log
20
- LOG_FACILITY = ENV['LOG_FACILITY'] || Syslog::LOG_USER
21
- LOG_FACILITY_MAP = {
22
- "auth" => Syslog::LOG_AUTH,
23
- "authpriv" => Syslog::LOG_AUTHPRIV,
24
- "cron" => Syslog::LOG_CRON,
25
- "daemon" => Syslog::LOG_DAEMON,
26
- "ftp" => Syslog::LOG_FTP,
27
- "kern" => Syslog::LOG_KERN,
28
- "mail" => Syslog::LOG_MAIL,
29
- "news" => Syslog::LOG_NEWS,
30
- "syslog" => Syslog::LOG_SYSLOG,
31
- "user" => Syslog::LOG_USER,
32
- "uucp" => Syslog::LOG_UUCP,
33
- "local0" => Syslog::LOG_LOCAL0,
34
- "local1" => Syslog::LOG_LOCAL1,
35
- "local2" => Syslog::LOG_LOCAL2,
36
- "local3" => Syslog::LOG_LOCAL3,
37
- "local4" => Syslog::LOG_LOCAL4,
38
- "local5" => Syslog::LOG_LOCAL5,
39
- "local6" => Syslog::LOG_LOCAL6,
40
- "local7" => Syslog::LOG_LOCAL7,
41
- }
42
-
43
- LOG_LEVEL = (ENV['LOG_LEVEL'] || 6).to_i
44
- LOG_LEVEL_MAP = {
45
- "emergency" => 0,
46
- "alert" => 1,
47
- "critical" => 2,
48
- "error" => 3,
49
- "warning" => 4,
50
- "notice" => 5,
51
- "info" => 6,
52
- "debug" => 7
53
- }
34
+ class Logger
54
35
 
55
36
  attr_reader :logger
56
37
  attr_accessor :exceptions, :timestamp
57
38
 
58
39
  def initialize(options={})
59
- @stream = options.fetch(:stream, sync_stream)
60
- @facility = options.fetch(:facility, LOG_FACILITY)
61
- @time_unit = options.fetch(:time_unit, "seconds")
62
- @timestamp = options.fetch(:timestamp, false)
63
- @exceptions = options.fetch(:exceptions, "single")
64
- @global_ctx = options.fetch(:global_context, {})
65
-
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
66
51
  setup!
67
52
  end
68
53
 
69
54
  def context
70
- Thread.current[:scrolls_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
71
60
  end
72
61
 
73
62
  def context=(h)
74
- Thread.current[:scrolls_context] = h
63
+ Thread.current.thread_variable_set(:scrolls_context, h || {})
75
64
  end
76
65
 
77
66
  def stream
78
67
  @stream
79
68
  end
80
69
 
81
- def stream=(stream)
70
+ def stream=(s)
82
71
  # Return early to avoid setup
83
- return if stream == @stream
84
-
85
- @stream = stream
72
+ return if s == @stream
73
+
74
+ @stream = s
86
75
  setup_stream
87
76
  end
88
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
+
89
90
  def facility
90
- @facility ||= LOG_FACILITY
91
+ @facility
91
92
  end
92
93
 
93
94
  def facility=(f)
94
95
  if f
95
- @facility = LOG_FACILITY_MAP[f]
96
- # Assume we are using syslog and set it up again
97
- @logger = Scrolls::SyslogLogger.new(progname, facility)
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
98
105
  end
99
106
  end
100
107
 
@@ -104,7 +111,7 @@ module Scrolls
104
111
 
105
112
  def time_unit=(u)
106
113
  @time_unit = u
107
- translate_time_unit
114
+ setup_time_unit
108
115
  end
109
116
 
110
117
  def global_context
@@ -138,14 +145,16 @@ module Scrolls
138
145
  begin
139
146
  res = yield
140
147
  rescue StandardError => e
141
- log(logdata.merge(
142
- :at => "exception",
143
- :reraise => true,
144
- :class => e.class,
145
- :message => e.message,
146
- :exception_id => e.object_id.abs,
147
- :elapsed => calculate_time(start, Time.now)
148
- ))
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)
149
158
  raise e
150
159
  end
151
160
  log(logdata.merge(:at => "finish", :elapsed => calculate_time(start, Time.now)))
@@ -153,17 +162,20 @@ module Scrolls
153
162
  end
154
163
  end
155
164
 
156
- def log_exception(data, e)
165
+ def log_exception(e, data=nil)
157
166
  unless @defined
158
167
  @stream = STDERR
159
168
  setup_stream
160
169
  end
161
170
 
162
- # If we get a string lets bring it into our structure.
163
- if data.kind_of? String
171
+ # We check our arguments for type
172
+ case data
173
+ when String
164
174
  rawhash = { "log_message" => data }
165
- else
175
+ when Hash
166
176
  rawhash = data
177
+ else
178
+ rawhash = {}
167
179
  end
168
180
 
169
181
  if gc = @global_context.to_h
@@ -171,12 +183,14 @@ module Scrolls
171
183
  end
172
184
 
173
185
  excepdata = {
174
- :at => "exception",
175
- :class => e.class,
176
- :message => e.message,
177
- :exception_id => e.object_id.abs
186
+ at: "exception",
187
+ class: e.class,
188
+ message: e.message,
189
+ exception_id: e.object_id.abs
178
190
  }
179
191
 
192
+ excepdata.delete_if { |k,v| k if v == "" }
193
+
180
194
  if e.backtrace
181
195
  if single_line_exceptions?
182
196
  lines = e.backtrace.map { |line| line.gsub(/[`'"]/, "") }
@@ -213,14 +227,15 @@ module Scrolls
213
227
  private
214
228
 
215
229
  def setup!
216
- build_global_context
230
+ setup_global_context
217
231
  prepend_timestamp?
232
+ setup_facility
218
233
  setup_stream
219
234
  single_line_exceptions?
220
- translate_time_unit
235
+ setup_time_unit
221
236
  end
222
237
 
223
- def build_global_context
238
+ def setup_global_context
224
239
  # Builds up an immutable object for our global_context
225
240
  # This is not backwards compatiable and was introduced after 0.3.7.
226
241
  # Removes ability to add to global context once we initialize our
@@ -233,15 +248,28 @@ module Scrolls
233
248
  @timestamp
234
249
  end
235
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
+
236
259
  def setup_stream
237
260
  unless @stream == STDOUT
261
+ # Set this so we know we aren't using our default stream
238
262
  @defined = true
239
263
  end
240
264
 
241
265
  if @stream == "syslog"
242
- @logger = Scrolls::SyslogLogger.new(progname, facility)
266
+ @logger = Scrolls::SyslogLogger.new(
267
+ progname,
268
+ syslog_options,
269
+ facility
270
+ )
243
271
  else
244
- @logger = sync_stream(@stream)
272
+ @logger = IOLogger.new(@stream)
245
273
  end
246
274
  end
247
275
 
@@ -250,7 +278,7 @@ module Scrolls
250
278
  true
251
279
  end
252
280
 
253
- def translate_time_unit
281
+ def setup_time_unit
254
282
  unless %w{s ms seconds milliseconds}.include? @time_unit
255
283
  raise TimeUnitError, "Specify the following: s, ms, seconds, milliseconds"
256
284
  end
@@ -264,11 +292,8 @@ module Scrolls
264
292
  @t = 1.0
265
293
  end
266
294
  end
267
-
268
- def sync_stream(out = STDOUT)
269
- IOLog.new(out)
270
- end
271
295
 
296
+ # We need this for our syslog setup
272
297
  def progname
273
298
  File.basename($0)
274
299
  end
@@ -280,6 +305,7 @@ module Scrolls
280
305
 
281
306
  def log_level_ok?(level)
282
307
  if level
308
+ raise LogLevelError, "Log level unknown" unless LOG_LEVEL_MAP.key?(level)
283
309
  LOG_LEVEL_MAP[level.to_s] <= LOG_LEVEL
284
310
  else
285
311
  true
@@ -288,8 +314,8 @@ module Scrolls
288
314
 
289
315
  def write(data)
290
316
  if log_level_ok?(data[:level])
291
- msg = Scrolls::Parser.unparse(data)
292
- logger.log(msg)
317
+ msg = Scrolls::Parser.unparse(data, escape_keys=escape_keys?, strict_logfmt=strict_logfmt?)
318
+ @logger.log(msg)
293
319
  end
294
320
  end
295
321
 
@@ -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.9.0.pre"
2
+ VERSION = "0.9.4"
3
3
  end
data/lib/scrolls.rb CHANGED
@@ -1,4 +1,3 @@
1
- require "thread"
2
1
  require "scrolls/logger"
3
2
  require "scrolls/version"
4
3
 
@@ -8,9 +7,26 @@ module Scrolls
8
7
  # Public: Initialize a Scrolls logger
9
8
  #
10
9
  # options - A hash of key/values for configuring Scrolls
10
+ # stream - Stream to output data (default: STDOUT)
11
+ # log_facility - Syslog facility (default: Syslog::LOG_USER)
12
+ # time_unit - Unit of time (default: seconds)
13
+ # timestamp - Prepend logs with a timestamp (default: false)
14
+ # exceptions - Method for outputting exceptions (default: single line)
15
+ # global_context - Immutable context to prepend all messages with
16
+ # syslog_options - Syslog options (default: Syslog::LOG_PID|Syslog::LOG_CONS)
17
+ # escape_keys - Escape chars in keys
18
+ # strict_logfmt - Always use double quotes to quote values
11
19
  #
12
20
  def init(options={})
13
- @log = Log.new(options)
21
+ # Set a hint whether #init was called.
22
+ @initialized = true
23
+ @log = Logger.new(options)
24
+ end
25
+
26
+ # Public: Get the primary logger
27
+ #
28
+ def logger
29
+ @log.logger
14
30
  end
15
31
 
16
32
  # Public: Set a context in a block for logs
@@ -48,26 +64,32 @@ module Scrolls
48
64
  # => nil
49
65
  #
50
66
  def log(data, &blk)
67
+ # Allows us to call #log directly and initialize defaults
68
+ @log = Logger.new({}) unless @initialized
69
+
51
70
  @log.log(data, &blk)
52
71
  end
53
72
 
54
73
  # Public: Log an exception
55
74
  #
56
- # data - A hash of key/values to log
57
75
  # e - An exception to pass to the logger
76
+ # data - A hash of key/values to log
58
77
  #
59
78
  # Examples:
60
79
  #
61
80
  # begin
62
81
  # raise Exception
63
82
  # rescue Exception => e
64
- # Scrolls.log_exception({test: "test"}, e)
83
+ # Scrolls.log_exception(e, {test: "test"})
65
84
  # end
66
85
  # test=test at=exception class=Exception message=Exception exception_id=70321999017240
67
86
  # ...
68
87
  #
69
- def log_exception(data, e)
70
- @log.log_exception(data, e)
88
+ def log_exception(e, data)
89
+ # Allows us to call #log directly and initialize defaults
90
+ @log = Logger.new({}) unless @initialized
91
+
92
+ @log.log_exception(e, data)
71
93
  end
72
94
 
73
95
  # Public: Setup a logging facility (default: Syslog::LOG_USER)
@@ -200,6 +222,7 @@ module Scrolls
200
222
  # => nil
201
223
  #
202
224
  def debug(data, &blk)
225
+ data = coalesce_strings_to_hash(data)
203
226
  data = data.merge(:level => "debug") if data.is_a?(Hash)
204
227
  @log.log(data, &blk)
205
228
  end
@@ -218,6 +241,7 @@ module Scrolls
218
241
  # => nil
219
242
  #
220
243
  def error(data, &blk)
244
+ data = coalesce_strings_to_hash(data)
221
245
  data = data.merge(:level => "warning") if data.is_a?(Hash)
222
246
  @log.log(data, &blk)
223
247
  end
@@ -236,6 +260,7 @@ module Scrolls
236
260
  # => nil
237
261
  #
238
262
  def fatal(data, &blk)
263
+ data = coalesce_strings_to_hash(data)
239
264
  data = data.merge(:level => "error") if data.is_a?(Hash)
240
265
  @log.log(data, &blk)
241
266
  end
@@ -254,6 +279,7 @@ module Scrolls
254
279
  # => nil
255
280
  #
256
281
  def info(data, &blk)
282
+ data = coalesce_strings_to_hash(data)
257
283
  data = data.merge(:level => "info") if data.is_a?(Hash)
258
284
  @log.log(data, &blk)
259
285
  end
@@ -272,6 +298,7 @@ module Scrolls
272
298
  # => nil
273
299
  #
274
300
  def warn(data, &blk)
301
+ data = coalesce_strings_to_hash(data)
275
302
  data = data.merge(:level => "notice") if data.is_a?(Hash)
276
303
  @log.log(data, &blk)
277
304
  end
@@ -290,6 +317,7 @@ module Scrolls
290
317
  # => nil
291
318
  #
292
319
  def unknown(data, &blk)
320
+ data = coalesce_strings_to_hash(data)
293
321
  data = data.merge(:level => "alert") if data.is_a?(Hash)
294
322
  @log.log(data, &blk)
295
323
  end
@@ -300,4 +328,8 @@ module Scrolls
300
328
  @log
301
329
  end
302
330
 
331
+ def coalesce_strings_to_hash(string_or_something_else)
332
+ return string_or_something_else unless string_or_something_else.is_a?(String)
333
+ { "log_message" => string_or_something_else }
334
+ end
303
335
  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"
data/test/test_parser.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  require File.expand_path("../test_helper", __FILE__)
2
2
 
3
- class TestScrollsParser < Test::Unit::TestCase
3
+ class TestScrollsParser < Minitest::Test
4
4
  include Scrolls::Parser
5
5
 
6
6
  def test_parse_bool
@@ -79,6 +79,21 @@ class TestScrollsParser < Test::Unit::TestCase
79
79
  assert_equal 't="2012-06-19T16:02:35+01:00"', unparse(data)
80
80
  end
81
81
 
82
+ def test_unparse_escape_keys
83
+ html = "<p>p</p>"
84
+ slash = "p/p"
85
+
86
+ data = { html => "d", slash => "d" }
87
+ assert_equal '&lt;p&gt;p&lt;&#x2F;p&gt;=d p&#x2F;p=d',
88
+ unparse(data, escape_keys=true)
89
+ end
90
+
91
+ def test_unparse_strict_logfmt
92
+ data = { s: 'echo "hello"' }
93
+ assert_equal 's="echo \"hello\""', unparse(data, escape_keys=false, strict_logfmt=true)
94
+ assert_equal data.inspect, parse(unparse(data, escape_keys=false, strict_logfmt=true)).inspect
95
+ end
96
+
82
97
  def test_parse_time
83
98
  time = Time.new(2012, 06, 19, 16, 02, 35, "+01:00")
84
99
  string = "t=2012-06-19T16:02:35+01:00"
data/test/test_scrolls.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  require File.expand_path("../test_helper", __FILE__)
2
2
 
3
- class TestScrolls < Test::Unit::TestCase
3
+ class TestScrolls < Minitest::Test
4
4
  def setup
5
5
  @out = StringIO.new
6
6
  Scrolls.init(
@@ -8,12 +8,9 @@ class TestScrolls < Test::Unit::TestCase
8
8
  )
9
9
  end
10
10
 
11
- def teardown
12
- end
13
-
14
11
  def test_default_construct
15
12
  Scrolls.init
16
- assert_equal Scrolls::IOLog, Scrolls.stream.class
13
+ assert_equal Scrolls::IOLogger, Scrolls.logger.class
17
14
  end
18
15
 
19
16
  def test_default_global_context
@@ -93,6 +90,21 @@ class TestScrolls < Test::Unit::TestCase
93
90
  end
94
91
  end
95
92
 
93
+ def test_deeply_nested_context_after_exception
94
+ Scrolls.log(:o => "o") do
95
+ begin
96
+ Scrolls.log(:io => 'io') do
97
+ raise "Error from inside of nested logging"
98
+ end
99
+ rescue
100
+ Scrolls.log(:o => 'o')
101
+ end
102
+ end
103
+ @out.truncate(124)
104
+ output = "o=o at=start\nio=io at=start\nio=io at=exception reraise=true class=RuntimeError message=\"Error from inside of nested logging\""
105
+ assert_equal output, @out.string
106
+ end
107
+
96
108
  def test_default_time_unit
97
109
  assert_equal "seconds", Scrolls.time_unit
98
110
  end
@@ -103,12 +115,18 @@ class TestScrolls < Test::Unit::TestCase
103
115
  end
104
116
 
105
117
  def test_setting_incorrect_time_unit
106
- assert_raise Scrolls::TimeUnitError do
118
+ assert_raises Scrolls::TimeUnitError do
107
119
  Scrolls.time_unit = "years"
108
120
  Scrolls.log(:tu => "yrs")
109
121
  end
110
122
  end
111
123
 
124
+ def test_unknown_log_level
125
+ assert_raises Scrolls::LogLevelError do
126
+ Scrolls.log(:level => "nope")
127
+ end
128
+ end
129
+
112
130
  def test_logging
113
131
  Scrolls.log(:test => "basic")
114
132
  assert_equal "test=basic\n", @out.string
@@ -124,7 +142,7 @@ class TestScrolls < Test::Unit::TestCase
124
142
  begin
125
143
  raise Exception
126
144
  rescue Exception => e
127
- Scrolls.log_exception({:test => "exception"}, e)
145
+ Scrolls.log_exception(e, {:test => "exception"})
128
146
  end
129
147
 
130
148
  oneline_bt = @out.string.gsub("\n", 'XX')
@@ -136,7 +154,7 @@ class TestScrolls < Test::Unit::TestCase
136
154
  begin
137
155
  raise Exception
138
156
  rescue Exception => e
139
- Scrolls.log_exception({:o => "o"}, e)
157
+ Scrolls.log_exception(e, {:o => "o"})
140
158
  end
141
159
 
142
160
  oneline_bt = @out.string.gsub("\n", 'XX')
@@ -205,22 +223,22 @@ class TestScrolls < Test::Unit::TestCase
205
223
 
206
224
  def test_sending_string_error
207
225
  Scrolls.error("error")
208
- assert_equal "log_message=error\n", @out.string
226
+ assert_equal "log_message=error level=warning\n", @out.string
209
227
  end
210
228
 
211
229
  def test_sending_string_fatal
212
230
  Scrolls.fatal("fatal")
213
- assert_equal "log_message=fatal\n", @out.string
231
+ assert_equal "log_message=fatal level=error\n", @out.string
214
232
  end
215
233
 
216
234
  def test_sending_string_warn
217
235
  Scrolls.warn("warn")
218
- assert_equal "log_message=warn\n", @out.string
236
+ assert_equal "log_message=warn level=notice\n", @out.string
219
237
  end
220
238
 
221
239
  def test_sending_string_unknown
222
240
  Scrolls.unknown("unknown")
223
- assert_equal "log_message=unknown\n", @out.string
241
+ assert_equal "log_message=unknown level=alert\n", @out.string
224
242
  end
225
243
 
226
244
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scrolls
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0.pre
4
+ version: 0.9.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Curt Micol
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-08-23 00:00:00.000000000 Z
11
+ date: 2021-10-22 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Logging, easier, more consistent.
14
14
  email:
@@ -25,11 +25,10 @@ files:
25
25
  - docs/global-context.md
26
26
  - docs/syslog.md
27
27
  - lib/scrolls.rb
28
- - lib/scrolls/iolog.rb
29
- - lib/scrolls/log.rb
28
+ - lib/scrolls/iologger.rb
30
29
  - lib/scrolls/logger.rb
31
30
  - lib/scrolls/parser.rb
32
- - lib/scrolls/syslog.rb
31
+ - lib/scrolls/sysloglogger.rb
33
32
  - lib/scrolls/utils.rb
34
33
  - lib/scrolls/version.rb
35
34
  - scrolls.gemspec
@@ -51,12 +50,11 @@ required_ruby_version: !ruby/object:Gem::Requirement
51
50
  version: '0'
52
51
  required_rubygems_version: !ruby/object:Gem::Requirement
53
52
  requirements:
54
- - - ">"
53
+ - - ">="
55
54
  - !ruby/object:Gem::Version
56
- version: 1.3.1
55
+ version: '0'
57
56
  requirements: []
58
- rubyforge_project:
59
- rubygems_version: 2.6.11
57
+ rubygems_version: 3.2.29
60
58
  signing_key:
61
59
  specification_version: 4
62
60
  summary: When do we log? All the time.
data/lib/scrolls/log.rb DELETED
@@ -1,262 +0,0 @@
1
- require "scrolls/parser"
2
- require "scrolls/utils"
3
- require "scrolls/iolog"
4
- require "scrolls/syslog"
5
-
6
- module Scrolls
7
-
8
- class TimeUnitError < RuntimeError; end
9
-
10
- module Log
11
- extend self
12
-
13
- extend Parser
14
- extend Utils
15
-
16
- LOG_LEVEL = (ENV['LOG_LEVEL'] || 6).to_i
17
- LOG_LEVEL_MAP = {
18
- "emergency" => 0,
19
- "alert" => 1,
20
- "critical" => 2,
21
- "error" => 3,
22
- "warning" => 4,
23
- "notice" => 5,
24
- "info" => 6,
25
- "debug" => 7
26
- }
27
-
28
- def context
29
- Thread.current[:scrolls_context] ||= {}
30
- end
31
-
32
- def context=(h)
33
- Thread.current[:scrolls_context] = h
34
- end
35
-
36
- def global_context
37
- get_global_context
38
- end
39
-
40
- def global_context=(data)
41
- set_global_context(data)
42
- end
43
-
44
- def add_global_context(new_data)
45
- default_global_context unless @global_context
46
- @global_context.update { |previous_data| previous_data.merge(new_data) }
47
- end
48
-
49
- def facility=(f)
50
- @facility = LOG_FACILITY_MAP[f] if f
51
- if Scrolls::SyslogLogger.opened?
52
- Scrolls::SyslogLogger.new(progname, facility)
53
- end
54
- end
55
-
56
- def facility
57
- @facility ||= default_log_facility
58
- end
59
-
60
- def stream=(out=nil)
61
- @defined = out.nil? ? false : true
62
- if out == 'syslog'
63
- @stream = Scrolls::SyslogLogger.new(progname, facility)
64
- else
65
- @stream = sync_stream(out)
66
- end
67
- end
68
-
69
- def stream
70
- @stream ||= sync_stream
71
- end
72
-
73
- def time_unit=(u)
74
- set_time_unit(u)
75
- end
76
-
77
- def time_unit
78
- @tunit ||= default_time_unit
79
- end
80
-
81
- def add_timestamp=(b)
82
- @add_timestamp = !!b
83
- end
84
-
85
- def add_timestamp
86
- @add_timestamp || false
87
- end
88
-
89
- def single_line_exceptions=(b)
90
- @single_line_exceptions = !!b
91
- end
92
-
93
- def single_line_exceptions?
94
- @single_line_exceptions || false
95
- end
96
-
97
- def log(data, &blk)
98
- # If we get a string lets bring it into our structure.
99
- if data.kind_of? String
100
- rawhash = { "log_message" => data }
101
- else
102
- rawhash = data
103
- end
104
-
105
- if gc = get_global_context
106
- ctx = gc.merge(context)
107
- logdata = ctx.merge(rawhash)
108
- end
109
-
110
- # By merging the logdata into the timestamp, rather than vice-versa, we
111
- # ensure that the timestamp comes first in the Hash, and is placed first
112
- # on the output, which helps with readability.
113
- logdata = { :now => Time.now.utc }.merge(logdata) if add_timestamp
114
-
115
- unless blk
116
- write(logdata)
117
- else
118
- start = Time.now
119
- res = nil
120
- log(logdata.merge(:at => "start"))
121
- begin
122
- res = yield
123
- rescue StandardError => e
124
- log(logdata.merge(
125
- :at => "exception",
126
- :reraise => true,
127
- :class => e.class,
128
- :message => e.message,
129
- :exception_id => e.object_id.abs,
130
- :elapsed => calc_time(start, Time.now)
131
- ))
132
- raise e
133
- end
134
- log(logdata.merge(:at => "finish", :elapsed => calc_time(start, Time.now)))
135
- res
136
- end
137
- end
138
-
139
- def log_exception(data, e)
140
- sync_stream(STDERR) unless @defined
141
-
142
- # If we get a string lets bring it into our structure.
143
- if data.kind_of? String
144
- rawhash = { "log_message" => data }
145
- else
146
- rawhash = data
147
- end
148
-
149
- if gc = get_global_context
150
- logdata = gc.merge(rawhash)
151
- end
152
-
153
- excepdata = {
154
- :at => "exception",
155
- :class => e.class,
156
- :message => e.message,
157
- :exception_id => e.object_id.abs
158
- }
159
-
160
- if e.backtrace
161
- if single_line_exceptions?
162
- btlines = []
163
- e.backtrace.each do |line|
164
- btlines << line.gsub(/[`'"]/, "")
165
- end
166
-
167
- if btlines.length > 0
168
- squish = { :site => btlines.join('\n') }
169
- log(logdata.merge(excepdata.merge(squish)))
170
- end
171
- else
172
- log(logdata.merge(excepdata))
173
-
174
- e.backtrace.each do |line|
175
- log(logdata.merge(excepdata).merge(
176
- :at => "exception",
177
- :class => e.class,
178
- :exception_id => e.object_id.abs,
179
- :site => line.gsub(/[`'"]/, "")
180
- ))
181
- end
182
- end
183
- end
184
- end
185
-
186
- def with_context(prefix)
187
- return unless block_given?
188
- old = context
189
- self.context = old.merge(prefix)
190
- res = yield if block_given?
191
- ensure
192
- self.context = old
193
- res
194
- end
195
-
196
- private
197
-
198
- def get_global_context
199
- default_global_context unless @global_context
200
- @global_context.value
201
- end
202
-
203
- def set_global_context(data=nil)
204
- default_global_context unless @global_context
205
- @global_context.update { |_| data }
206
- end
207
-
208
- def default_global_context
209
- @global_context = Atomic.new({})
210
- end
211
-
212
- def set_time_unit(u=nil)
213
- unless ["ms","milli","milliseconds","s","seconds"].include?(u)
214
- raise TimeUnitError, "Specify only 'seconds' or 'milliseconds'"
215
- end
216
-
217
- if ["ms", "milli", "milliseconds", 1000].include?(u)
218
- @tunit = "milliseconds"
219
- @t = 1000.0
220
- else
221
- default_time_unit
222
- end
223
- end
224
-
225
- def default_time_unit
226
- @t = 1.0
227
- @tunit = "seconds"
228
- end
229
-
230
- def calc_time(start, finish)
231
- default_time_unit unless @t
232
- ((finish - start).to_f * @t)
233
- end
234
-
235
- def sync_stream(out = STDOUT)
236
- IOLog.new(out)
237
- end
238
-
239
- def write(data)
240
- if log_level_ok?(data[:level])
241
- msg = unparse(data)
242
- stream.log(msg)
243
- end
244
- end
245
-
246
- def log_level_ok?(level)
247
- if level
248
- LOG_LEVEL_MAP[level.to_s] <= LOG_LEVEL
249
- else
250
- true
251
- end
252
- end
253
-
254
- def progname
255
- File.basename($0)
256
- end
257
-
258
- def default_log_facility
259
- LOG_FACILITY
260
- end
261
- end
262
- end
@@ -1,46 +0,0 @@
1
- require 'syslog'
2
-
3
- module Scrolls
4
-
5
- LOG_FACILITY = ENV['LOG_FACILITY'] || Syslog::LOG_USER
6
- LOG_FACILITY_MAP = {
7
- "auth" => Syslog::LOG_AUTH,
8
- "authpriv" => Syslog::LOG_AUTHPRIV,
9
- "cron" => Syslog::LOG_CRON,
10
- "daemon" => Syslog::LOG_DAEMON,
11
- "ftp" => Syslog::LOG_FTP,
12
- "kern" => Syslog::LOG_KERN,
13
- "mail" => Syslog::LOG_MAIL,
14
- "news" => Syslog::LOG_NEWS,
15
- "syslog" => Syslog::LOG_SYSLOG,
16
- "user" => Syslog::LOG_USER,
17
- "uucp" => Syslog::LOG_UUCP,
18
- "local0" => Syslog::LOG_LOCAL0,
19
- "local1" => Syslog::LOG_LOCAL1,
20
- "local2" => Syslog::LOG_LOCAL2,
21
- "local3" => Syslog::LOG_LOCAL3,
22
- "local4" => Syslog::LOG_LOCAL4,
23
- "local5" => Syslog::LOG_LOCAL5,
24
- "local6" => Syslog::LOG_LOCAL6,
25
- "local7" => Syslog::LOG_LOCAL7,
26
- }
27
-
28
- class SyslogLogger
29
- def initialize(ident = 'scrolls', facility = Syslog::LOG_USER)
30
- options = Syslog::LOG_PID|Syslog::LOG_CONS
31
- if Syslog.opened?
32
- @syslog = Syslog.reopen(ident, options, facility)
33
- else
34
- @syslog = Syslog.open(ident, options, facility)
35
- end
36
- end
37
-
38
- def log(data)
39
- @syslog.log(Syslog::LOG_INFO, "%s", data)
40
- end
41
-
42
- def self.opened?
43
- Syslog.opened?
44
- end
45
- end
46
- end