scrolls 0.1.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,8 +1,10 @@
1
1
  # Scrolls
2
2
 
3
- Logging gem, compiled from many different internal projects in order to make
4
- it easier to add logging to projects. Basically I grew tired of moving this
5
- file around.
3
+ Scrolls is a logging library that is focused on outputting logs in a
4
+ key=value structure. It's in use at Heroku where we use the event data
5
+ to drive metrics and monitoring services.
6
+
7
+ Scrolls is rather opinionated.
6
8
 
7
9
  ## Installation
8
10
 
@@ -20,31 +22,123 @@ Or install it yourself as:
20
22
 
21
23
  ## Usage
22
24
 
23
- It's pretty easy to use:
25
+ At Heroku we are big believers in "logs as data". We log everything so
26
+ that we can act upon that event stream of logs. Internally we use logs
27
+ to produce metrics and monitoring data that we can alert on.
28
+
29
+ Here's an example of a log you might specify in your application:
30
+
31
+ ```ruby
32
+ Scrolls.log(fn: "trap", signal: s, at: "exit", status: 0)
33
+ ```
34
+
35
+ The output of which might be:
36
+
37
+ fn=trap signal=TERM at=exit status=0
38
+
39
+ This provides a rich set of data that we can parse and act upon.
40
+
41
+ A feature of Scrolls is setting contexts. Scrolls has two types of
42
+ context. One is 'global_context' that prepends every log in your
43
+ application with that data and a local 'context' which can be used,
44
+ for example, to wrap requests with a request id.
45
+
46
+ In our example above, the log message is rather generic, so in order
47
+ to provide more context we might set a global context that links this
48
+ log data to our application and deployment:
49
+
50
+ ```ruby
51
+ Scrolls.global_context(app: "myapp", deploy: ENV["DEPLOY"])
52
+ ```
53
+
54
+ This would change our log output above to:
55
+
56
+ app=myapp deploy=production fn=trap signal=TERM at=exit status=0
57
+
58
+ If we were in a file and wanted to wrap a particular point of context
59
+ we might also do something similar to:
60
+
61
+ ```ruby
62
+ Scrolls.context(ns: "server") do
63
+ Scrolls.log(fn: "trap", signal: s, at: "exit", status: 0)
64
+ end
65
+ ```
66
+
67
+ This would be the output (taking into consideration our global context
68
+ above):
24
69
 
25
- require 'scrolls'
70
+ app=myapp deploy=production ns=server fn=trap signal=TERM at=exit status=0
26
71
 
27
- Put this somewhere early on in your application:
72
+ This allows us to track this log to `Server#trap` and we received a
73
+ 'TERM' signal and exited 0.
28
74
 
29
- Scrolls::Log.start
75
+ As you can see we have some standard nomenclature around logging.
76
+ Here's a cheat sheet for some of the methods we use:
30
77
 
31
- Use it:
78
+ * `app`: Application
79
+ * `lib`: Library
80
+ * `ns`: Namespace (Class, Module or files)
81
+ * `fn`: Function
82
+ * `at`: Execution point
83
+ * `deploy`: Our deployment (typically an environment variable i.e. `DEPLOY=staging`)
84
+ * `elapsed`: Measurements (Time)
85
+ * `count`: Measurements (Counters)
32
86
 
33
- Scrolls.log(:testing => true, :at => "readme")
87
+ Scrolls makes it easy to measure the run time of a portion of code.
88
+ For example:
89
+
90
+ ```ruby
91
+ Scrolls.log(fn: "test") do
92
+ Scrolls.log(status: "exec")
93
+ # Code here
94
+ end
95
+ ```
96
+
97
+ This will output the following log:
98
+
99
+ fn=test at=start
100
+ status=exec
101
+ fn=test at=finish elapsed=0.300
102
+
103
+ You can change the time unit that Scrolls uses to "milliseconds" (the
104
+ default is "seconds"):
105
+
106
+ ```ruby
107
+ Scrolls.time_unit = "ms"
108
+ ```
109
+
110
+ Scrolls has a rich #parse method to handle a number of cases. Here is
111
+ a look at some of the ways Scrolls handles certain values.
112
+
113
+ Time and nil:
114
+
115
+ ```ruby
116
+ Scrolls.log(t: Time.at(1340118167), this: nil)
117
+ t=t=2012-06-19T11:02:35-0400 this=nil
118
+ ```
119
+
120
+ True/False:
121
+
122
+ ```ruby
123
+ Scrolls.log(that: false, this: true)
124
+ that=false this=true
125
+ ```
126
+
127
+ ## History
128
+
129
+ This library originated from various logging methods used internally
130
+ at Heroku. Starting at version 0.2.0 Scrolls was ripped apart and
131
+ restructured to provide a better foundation for the future. Tests and
132
+ documentation were add at that point as well.
34
133
 
35
134
  ## Thanks
36
135
 
37
- I only compiled this library, many others made it work. Here are some of the
38
- authors of projects that I've used to get an idea of a consistent log gem:
136
+ Most of the ideas used in Scrolls are those of other engineers at
137
+ Heroku, I simply ripped them off to create a single library. Huge
138
+ thanks to:
39
139
 
40
140
  * Mark McGranaghan
41
141
  * Noah Zoschke
42
142
  * Mark Fine
43
-
44
- ## Contributing
45
-
46
- 1. Fork it
47
- 2. Create your feature branch (`git checkout -b my-new-feature`)
48
- 3. Commit your changes (`git commit -am 'Added some feature'`)
49
- 4. Push to the branch (`git push origin my-new-feature`)
50
- 5. Create new Pull Request
143
+ * Fabio Kung
144
+ * Ryan Smith
data/Rakefile CHANGED
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env rake
2
2
  require "bundler/gem_tasks"
3
3
 
4
+ ENV['TESTOPTS'] = "-v"
5
+
4
6
  require "rake/testtask"
5
7
  Rake::TestTask.new do |t|
6
8
  t.pattern = "test/test_*.rb"
@@ -0,0 +1,58 @@
1
+ # The result of issues with an update I made to Scrolls. After talking with
2
+ # Fabio Kung about a fix I started work on an atomic object, but he added some
3
+ # fixes to #context without it and then used Headius' atomic gem.
4
+ #
5
+ # The code below is the start and cleanup of my atomic object. It's slim on
6
+ # details and eventually cleaned up around inspiration from Headius' code.
7
+ #
8
+ # LICENSE: Apache 2.0
9
+ #
10
+ # See Headius' atomic gem here:
11
+ # https://github.com/headius/ruby-atomic
12
+
13
+ require 'thread'
14
+
15
+ class AtomicObject
16
+ def initialize(o)
17
+ @mtx = Mutex.new
18
+ @o = o
19
+ end
20
+
21
+ def get
22
+ @mtx.synchronize { @o }
23
+ end
24
+
25
+ def set(n)
26
+ @mtx.synchronize { @o = n }
27
+ end
28
+
29
+ def verify_set(o, n)
30
+ return false unless @mtx.try_lock
31
+ begin
32
+ return false unless @o.equal? o
33
+ @o = n
34
+ ensure
35
+ @mtx.unlock
36
+ end
37
+ end
38
+ end
39
+
40
+ class Atomic < AtomicObject
41
+ def initialize(v=nil)
42
+ super(v)
43
+ end
44
+
45
+ def value
46
+ self.get
47
+ end
48
+
49
+ def value=(v)
50
+ self.set(v)
51
+ v
52
+ end
53
+
54
+ def update
55
+ true until self.verify_set(o = self.get, n = yield(o))
56
+ n
57
+ end
58
+ end
@@ -0,0 +1,196 @@
1
+ require "scrolls/parser"
2
+ require "scrolls/utils"
3
+
4
+ module Scrolls
5
+
6
+ class TimeUnitError < RuntimeError; end
7
+
8
+ module Log
9
+ extend self
10
+
11
+ extend Parser
12
+ extend Utils
13
+
14
+ LOG_LEVEL = (ENV['LOG_LEVEL'] || 3).to_i
15
+ LOG_LEVEL_MAP = {
16
+ "emergency" => 0,
17
+ "alert" => 1,
18
+ "critical" => 2,
19
+ "error" => 3,
20
+ "warning" => 4,
21
+ "notice" => 5,
22
+ "info" => 6,
23
+ "debug" => 7
24
+ }
25
+
26
+ def context
27
+ Thread.current[:scrolls_context] ||= {}
28
+ end
29
+
30
+ def context=(h)
31
+ Thread.current[:scrolls_context] = h
32
+ end
33
+
34
+ def global_context
35
+ get_global_context
36
+ end
37
+
38
+ def global_context=(data)
39
+ set_global_context(data)
40
+ end
41
+
42
+ def stream=(out=nil)
43
+ @defined = out.nil? ? false : true
44
+
45
+ @stream = sync_stream(out)
46
+ end
47
+
48
+ def stream
49
+ @stream ||= sync_stream
50
+ end
51
+
52
+ def time_unit=(u)
53
+ set_time_unit(u)
54
+ end
55
+
56
+ def time_unit
57
+ @tunit ||= default_time_unit
58
+ end
59
+
60
+ def log(data, &blk)
61
+ if gc = get_global_context
62
+ ctx = gc.merge(context)
63
+ logdata = ctx.merge(data)
64
+ end
65
+
66
+ unless blk
67
+ write(logdata)
68
+ else
69
+ start = Time.now
70
+ res = nil
71
+ log(logdata.merge(at: "start"))
72
+ begin
73
+ res = yield
74
+ rescue StandardError, Timeout::Error => e
75
+ log(
76
+ :at => "exception",
77
+ :reraise => true,
78
+ :class => e.class,
79
+ :message => e.message,
80
+ :exception_id => e.object_id.abs,
81
+ :elapsed => calc_time(start, Time.now)
82
+ )
83
+ raise e
84
+ end
85
+ log(logdata.merge(at: "finish", elapsed: calc_time(start, Time.now)))
86
+ res
87
+ end
88
+ end
89
+
90
+ def log_exception(data, e)
91
+ sync_stream(STDERR) unless @defined
92
+
93
+ if gc = get_global_context
94
+ logdata = gc.merge(data)
95
+ end
96
+
97
+ log(logdata.merge(
98
+ :at => "exception",
99
+ :class => e.class,
100
+ :message => e.message,
101
+ :exception_id => e.object_id.abs
102
+ ))
103
+ if e.backtrace
104
+ bt = e.backtrace.reverse
105
+ bt[0, bt.size-6].each do |line|
106
+ log(logdata.merge(
107
+ :at => "exception",
108
+ :class => e.message,
109
+ :exception_id => e.object_id.abs,
110
+ :site => line.gsub(/[`'"]/, "")
111
+ ))
112
+ end
113
+ end
114
+ end
115
+
116
+ def with_context(prefix)
117
+ return unless block_given?
118
+ old = context
119
+ self.context = old.merge(prefix)
120
+ yield if block_given?
121
+ self.context = old
122
+ end
123
+
124
+ private
125
+
126
+ def get_global_context
127
+ default_global_context unless @global_context
128
+ @global_context.value
129
+ end
130
+
131
+ def set_global_context(data=nil)
132
+ default_global_context unless @global_context
133
+ @global_context.update { |_| data }
134
+ end
135
+
136
+ def default_global_context
137
+ @global_context = Atomic.new({})
138
+ end
139
+
140
+ def set_time_unit(u=nil)
141
+ unless ["ms","milli","milliseconds","s","seconds"].include?(u)
142
+ raise TimeUnitError, "Specify only 'seconds' or 'milliseconds'"
143
+ end
144
+
145
+ if ["ms", "milli", "milliseconds", 1000].include?(u)
146
+ @tunit = "milliseconds"
147
+ @t = 1000.0
148
+ else
149
+ default_time_unit
150
+ end
151
+ end
152
+
153
+ def default_time_unit
154
+ @t = 1.0
155
+ @tunit = "seconds"
156
+ end
157
+
158
+ def calc_time(start, finish)
159
+ default_time_unit unless @t
160
+ ((finish - start).to_f * @t)
161
+ end
162
+
163
+ def mtx
164
+ @mtx ||= Mutex.new
165
+ end
166
+
167
+ def sync_stream(out=nil)
168
+ out = STDOUT if out.nil?
169
+ s = out
170
+ s.sync = true
171
+ s
172
+ end
173
+
174
+ def write(data)
175
+ if log_level_ok?(data[:level])
176
+ msg = unparse(data)
177
+ mtx.synchronize do
178
+ begin
179
+ stream.puts(msg)
180
+ rescue NoMethodError => e
181
+ raise
182
+ end
183
+ end
184
+ end
185
+ end
186
+
187
+ def log_level_ok?(level)
188
+ if level
189
+ LOG_LEVEL_MAP[level.to_s] <= LOG_LEVEL
190
+ else
191
+ true
192
+ end
193
+ end
194
+
195
+ end
196
+ end
@@ -0,0 +1,64 @@
1
+ module Scrolls
2
+ module Parser
3
+ extend self
4
+
5
+ def unparse(data)
6
+ data.map do |(k,v)|
7
+ if (v == true)
8
+ "#{k}=true"
9
+ elsif (v == false)
10
+ "#{k}=false"
11
+ elsif v.is_a?(Float)
12
+ "#{k}=#{format("%.3f", v)}"
13
+ elsif v.nil?
14
+ "#{k}=nil"
15
+ elsif v.is_a?(Time)
16
+ "#{k}=#{Time.at(v).strftime("%FT%H:%M:%S%z")}"
17
+ elsif v.is_a?(String) && v =~ /\\|\"| /
18
+ v = v.gsub(/\\|"/) { |c| "\\#{c}" }
19
+ "#{k}=\"#{v}\""
20
+ else
21
+ "#{k}=#{v}"
22
+ end
23
+ end.compact.join(" ")
24
+ end
25
+
26
+ def parse(data)
27
+ vals = {}
28
+ str = data.dup if data.is_a?(String)
29
+
30
+ patterns = [
31
+ /([^= ]+)="([^"\\]*(\\.[^"\\]*)*)"/, # key="\"literal\" escaped val"
32
+ /([^= ]+)=([^ =]+)/ # key=value
33
+ ]
34
+
35
+ patterns.each do |pattern|
36
+ str.scan(pattern) do |match|
37
+ v = match[1]
38
+ v.gsub!(/\\"/, '"') # unescape \"
39
+ v.gsub!(/\\\\/, "\\") # unescape \\
40
+
41
+ if v.to_i.to_s == v # cast value to int or float
42
+ v = v.to_i
43
+ elsif format("%.3f", v.to_f) == v
44
+ v = v.to_f
45
+ elsif v == "false"
46
+ v = false
47
+ elsif v == "true"
48
+ v = true
49
+ end
50
+
51
+ vals[match[0]] = v
52
+ end
53
+ # sub value, leaving keys in order
54
+ str.gsub!(pattern, "\\1")
55
+ end
56
+
57
+ # rebuild in-order key: value hash
58
+ str.split.inject({}) do |h,k|
59
+ h[k.to_sym] = vals[k]
60
+ h
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,21 @@
1
+ module Scrolls
2
+ module Utils
3
+
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
11
+
12
+ def hashified_list(l)
13
+ return {} if l.empty?
14
+ l.inject({}) do |h, i|
15
+ h[i.to_sym] = true
16
+ h
17
+ end
18
+ end
19
+
20
+ end
21
+ end
@@ -1,3 +1,3 @@
1
1
  module Scrolls
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.1"
3
3
  end
data/lib/scrolls.rb CHANGED
@@ -1,172 +1,117 @@
1
1
  require "thread"
2
- require "atomic"
3
-
2
+ require "scrolls/atomic"
3
+ require "scrolls/log"
4
4
  require "scrolls/version"
5
5
 
6
6
  module Scrolls
7
7
  extend self
8
8
 
9
+ # Public: Set a context in a block for logs
10
+ #
11
+ # data - A hash of key/values to prepend to each log in a block
12
+ # blk - The block that our context wraps
13
+ #
14
+ # Examples:
15
+ #
16
+ def context(data, &blk)
17
+ Log.with_context(data, &blk)
18
+ end
19
+
20
+ # Public: Get or set a global context that prefixs all logs
21
+ #
22
+ # data - A hash of key/values to prepend to each log
23
+ #
24
+ def global_context(data=nil)
25
+ if data
26
+ Log.global_context = data
27
+ else
28
+ Log.global_context
29
+ end
30
+ end
31
+
32
+ # Public: Log data and/or wrap a block with start/finish
33
+ #
34
+ # data - A hash of key/values to log
35
+ # blk - A block to be wrapped by log lines
36
+ #
37
+ # Examples:
38
+ #
39
+ # Scrolls.log(test: "test")
40
+ # test=test
41
+ # => nil
42
+ #
43
+ # Scrolls.log(test: "test") { puts "inner block" }
44
+ # at=start
45
+ # inner block
46
+ # at=finish elapsed=0.000
47
+ # => nil
48
+ #
9
49
  def log(data, &blk)
10
50
  Log.log(data, &blk)
11
51
  end
12
52
 
53
+ # Public: Log an exception
54
+ #
55
+ # data - A hash of key/values to log
56
+ # e - An exception to pass to the logger
57
+ #
58
+ # Examples:
59
+ #
60
+ # begin
61
+ # raise Exception
62
+ # rescue Exception => e
63
+ # Scrolls.log_exception({test: "test"}, e)
64
+ # end
65
+ # test=test at=exception class=Exception message=Exception exception_id=70321999017240
66
+ # ...
67
+ #
13
68
  def log_exception(data, e)
14
69
  Log.log_exception(data, e)
15
70
  end
16
71
 
17
- def global_context(data)
18
- Log.global_context = data
72
+ # Public: Setup a new output (default: STDOUT)
73
+ #
74
+ # out - New output
75
+ #
76
+ # Examples
77
+ #
78
+ # Scrolls.stream = StringIO.new
79
+ #
80
+ def stream=(out)
81
+ Log.stream=(out)
19
82
  end
20
83
 
21
- def context(data, &blk)
22
- Log.with_context(data, &blk)
84
+ # Public: Return the stream
85
+ #
86
+ # Examples
87
+ #
88
+ # Scrolls.stream
89
+ # => #<IO:<STDOUT>>
90
+ #
91
+ def stream
92
+ Log.stream
23
93
  end
24
94
 
25
- module Log
26
- extend self
27
-
28
- LOG_LEVEL = (ENV["LOG_LEVEL"] || 3).to_i
29
-
30
- #http://tools.ietf.org/html/rfc5424#page-11
31
- LOG_LEVEL_MAP = {
32
- "emergency" => 0,
33
- "alert" => 1,
34
- "critical" => 2,
35
- "error" => 3,
36
- "warning" => 4,
37
- "notice" => 5,
38
- "info" => 6,
39
- "debug" => 7
40
- }
41
-
42
- attr_accessor :stream
43
-
44
- def context
45
- Thread.current[:scrolls_context] ||= {}
46
- end
47
-
48
- def context=(hash)
49
- Thread.current[:scrolls_context] = hash
50
- end
51
-
52
- def start(out = nil)
53
- # This allows log_exceptions below to pick up the defined output,
54
- # otherwise stream out to STDERR
55
- @defined = out.nil? ? false : true
56
-
57
- sync_stream(out)
58
- @global_context = Atomic.new({})
59
- end
60
-
61
- def sync_stream(out = nil)
62
- out = STDOUT if out.nil?
63
- @stream = out
64
- @stream.sync = true
65
- end
66
-
67
- def mtx
68
- @mtx ||= Mutex.new
69
- end
70
-
71
- def write(data)
72
- if log_level_ok?(data[:level])
73
- msg = unparse(data)
74
- mtx.synchronize do
75
- begin
76
- @stream.puts(msg)
77
- rescue NoMethodError => e
78
- puts "You need to start your logger, `Scrolls::Log.start`"
79
- end
80
- end
81
- end
82
- end
83
-
84
- def unparse(data)
85
- data.map do |(k, v)|
86
- if (v == true)
87
- k.to_s
88
- elsif v.is_a?(Float)
89
- "#{k}=#{format("%.3f", v)}"
90
- elsif v.nil?
91
- nil
92
- else
93
- v_str = v.to_s
94
- if (v_str =~ /^[a-zA-z0-9\-\_\.]+$/)
95
- "#{k}=#{v_str}"
96
- else
97
- "#{k}=\"#{v_str.sub(/".*/, "...")}\""
98
- end
99
- end
100
- end.compact.join(" ")
101
- end
102
-
103
- def log(data, &blk)
104
- merged_context = @global_context.value.merge(context)
105
- logdata = merged_context.merge(data)
106
-
107
- unless blk
108
- write(logdata)
109
- else
110
- start = Time.now
111
- res = nil
112
- log(logdata.merge(:at => :start))
113
- begin
114
- res = yield
115
- rescue StandardError, Timeout::Error => e
116
- log(logdata.merge(
117
- :at => :exception,
118
- :reraise => true,
119
- :class => e.class,
120
- :message => e.message,
121
- :exception_id => e.object_id.abs,
122
- :elapsed => Time.now - start
123
- ))
124
- raise(e)
125
- end
126
- log(logdata.merge(:at => :finish, :elapsed => Time.now - start))
127
- res
128
- end
129
- end
130
-
131
- def log_exception(data, e)
132
- sync_stream(STDERR) unless @defined
133
- log(data.merge(
134
- :exception => true,
135
- :class => e.class,
136
- :message => e.message,
137
- :exception_id => e.object_id.abs
138
- ))
139
- if e.backtrace
140
- bt = e.backtrace.reverse
141
- bt[0, bt.size-6].each do |line|
142
- log(data.merge(
143
- :exception => true,
144
- :exception_id => e.object_id.abs,
145
- :site => line.gsub(/[`'"]/, "")
146
- ))
147
- end
148
- end
149
- end
150
-
151
- def log_level_ok?(level)
152
- if level
153
- LOG_LEVEL_MAP[level.to_s] <= LOG_LEVEL
154
- else
155
- true
156
- end
157
- end
158
-
159
- def with_context(prefix)
160
- return unless block_given?
161
- old_context = context
162
- self.context = old_context.merge(prefix)
163
- yield if block_given?
164
- self.context = old_context
165
- end
166
-
167
- def global_context=(data)
168
- @global_context.update { |_| data }
169
- end
95
+ # Public: Set the time unit we use for 'elapsed' (default: "seconds")
96
+ #
97
+ # unit - The time unit ("milliseconds" currently supported)
98
+ #
99
+ # Examples
100
+ #
101
+ # Scrolls.time_unit = "milliseconds"
102
+ #
103
+ def time_unit=(unit)
104
+ Log.time_unit=(unit)
105
+ end
170
106
 
107
+ # Public: Return the time unit currently configured
108
+ #
109
+ # Examples
110
+ #
111
+ # Scrolls.time_unit
112
+ # => "seconds"
113
+ #
114
+ def time_unit
115
+ Log.time_unit
171
116
  end
172
117
  end
data/scrolls.gemspec CHANGED
@@ -2,16 +2,16 @@
2
2
  require File.expand_path('../lib/scrolls/version', __FILE__)
3
3
 
4
4
  Gem::Specification.new do |gem|
5
- gem.authors = ["Heroku"]
6
- gem.email = ["curt@heroku.com"]
7
- gem.description = "Logging, easier, more consistent."
8
- gem.summary = "When do we log? All the time."
5
+ gem.authors = ["Curt Micol"]
6
+ gem.email = ["asenchi@asenchi.com"]
7
+ gem.description = %q{Logging, easier, more consistent.}
8
+ gem.summary = %q{When do we log? All the time.}
9
9
  gem.homepage = "https://github.com/asenchi/scrolls"
10
- gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
11
- gem.files = `git ls-files`.split("\n")
12
- gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
13
14
  gem.name = "scrolls"
14
15
  gem.require_paths = ["lib"]
15
16
  gem.version = Scrolls::VERSION
16
- gem.add_dependency("atomic", "~> 1.0.0")
17
17
  end
@@ -0,0 +1,33 @@
1
+ require_relative "test_helper"
2
+
3
+ class TestAtomic < Test::Unit::TestCase
4
+ def test_construct
5
+ atomic = Atomic.new
6
+ assert_equal nil, atomic.value
7
+
8
+ atomic = Atomic.new(0)
9
+ assert_equal 0, atomic.value
10
+ end
11
+
12
+ def test_value
13
+ atomic = Atomic.new(0)
14
+ atomic.value = 1
15
+
16
+ assert_equal 1, atomic.value
17
+ end
18
+
19
+ def test_update
20
+ atomic = Atomic.new(1000)
21
+ res = atomic.update {|v| v + 1}
22
+
23
+ assert_equal 1001, atomic.value
24
+ assert_equal 1001, res
25
+ end
26
+
27
+ def test_update_retries
28
+ tries = 0
29
+ atomic = Atomic.new(1000)
30
+ atomic.update{|v| tries += 1 ; atomic.value = 1001 ; v + 1}
31
+ assert_equal 2, tries
32
+ end
33
+ end
@@ -0,0 +1,6 @@
1
+ require "test/unit"
2
+ require "stringio"
3
+
4
+ $: << File.expand_path("../../lib", __FILE__)
5
+
6
+ require "scrolls"
@@ -0,0 +1,52 @@
1
+ require_relative "test_helper"
2
+
3
+ class TestScrollsParser < Test::Unit::TestCase
4
+ include Scrolls::Parser
5
+
6
+ def test_parse_bool
7
+ data = { test: true, exec: false }
8
+ assert_equal "test=true exec=false", unparse(data)
9
+ assert_equal data.inspect, parse(unparse(data)).inspect
10
+ end
11
+
12
+ def test_parse_numbers
13
+ data = { elapsed: 12.00000, __time: 0 }
14
+ assert_equal "elapsed=12.000 __time=0", unparse(data)
15
+ assert_equal data.inspect, parse(unparse(data)).inspect
16
+ end
17
+
18
+ def test_parse_strings
19
+ # Strings are all double quoted, with " or \ escaped
20
+ data = { s: "echo 'hello' \"world\"" }
21
+ assert_equal 's="echo \'hello\' \\"world\\""', unparse(data)
22
+ assert_equal data.inspect, parse(unparse(data)).inspect
23
+
24
+ data = { s: "hello world" }
25
+ assert_equal 's="hello world"', unparse(data)
26
+ assert_equal data.inspect, parse(unparse(data)).inspect
27
+
28
+ data = { s: "slasher\\" }
29
+ assert_equal 's="slasher\\\\"', unparse(data)
30
+ assert_equal data.inspect, parse(unparse(data)).inspect
31
+
32
+ # simple value is unquoted
33
+ data = { s: "hi" }
34
+ assert_equal 's=hi', unparse(data)
35
+ assert_equal data.inspect, parse(unparse(data)).inspect
36
+ end
37
+
38
+ def test_parse_constants
39
+ data = { s1: :symbol, s2: Scrolls }
40
+ assert_equal "s1=symbol s2=Scrolls", unparse(data)
41
+ end
42
+
43
+ def test_parse_time
44
+ data = { t: Time.at(1340118155) }
45
+ assert_equal "t=2012-06-19T11:02:35-0400", unparse(data)
46
+ end
47
+
48
+ def test_parse_nil
49
+ data = { n: nil }
50
+ assert_equal "n=nil", unparse(data)
51
+ end
52
+ end
data/test/test_scrolls.rb CHANGED
@@ -1,31 +1,110 @@
1
- require "stringio"
2
- require "minitest/autorun"
1
+ require_relative "test_helper"
3
2
 
4
- $: << "../lib"
5
- require "scrolls"
3
+ class TestScrolls < Test::Unit::TestCase
4
+ def setup
5
+ @out = StringIO.new
6
+ Scrolls.stream = @out
7
+ end
8
+
9
+ def teardown
10
+ Scrolls.global_context({})
11
+ end
12
+
13
+ def test_construct
14
+ assert_equal StringIO, Scrolls.stream.class
15
+ end
16
+
17
+ def test_default_global_context
18
+ assert_equal Hash.new, Scrolls.global_context
19
+ end
20
+
21
+ def test_setting_global_context
22
+ Scrolls.global_context(g: "g")
23
+ Scrolls.log(d: "d")
24
+ assert_equal "g=g d=d\n", @out.string
25
+ end
26
+
27
+ def test_default_context
28
+ Scrolls.log(data: "d")
29
+ assert_equal Hash.new, Scrolls::Log.context
30
+ end
6
31
 
7
- class TestScrollsParser < MiniTest::Unit::TestCase
8
- def test_unparse_tags
9
- data = {:test => true, :tag => true}
10
- assert "test tag" == Scrolls::Log.unparse(data)
32
+ def test_setting_context
33
+ Scrolls.context(c: "c") { Scrolls.log(i: "i") }
34
+ output = "c=c i=i\n"
35
+ assert_equal output, @out.string
11
36
  end
12
37
 
13
- def test_unparse_strings
14
- data = {:test => "strings"}
15
- assert "test=strings" == Scrolls::Log.unparse(data)
38
+ def test_all_the_contexts
39
+ Scrolls.global_context(g: "g")
40
+ Scrolls.log(o: "o") do
41
+ Scrolls.context(c: "c") do
42
+ Scrolls.log(ic: "i")
43
+ end
44
+ Scrolls.log(i: "i")
45
+ end
46
+ @out.truncate(37)
47
+ output = "g=g o=o at=start\ng=g c=c ic=i\ng=g i=i"
48
+ assert_equal output, @out.string
49
+ end
50
+
51
+ def test_deeply_nested_context
52
+ Scrolls.log(o: "o") do
53
+ Scrolls.context(c: "c") do
54
+ Scrolls.log(ic: "i")
55
+ end
56
+ Scrolls.log(i: "i")
57
+ end
58
+ @out.truncate(21)
59
+ output = "o=o at=start\nc=c ic=i"
60
+ assert_equal output, @out.string
61
+ end
16
62
 
17
- data = {:s => "echo 'hello' \"world\""}
18
- assert 's="echo \'hello\' ..."' == Scrolls::Log.unparse(data)
63
+ def test_deeply_nested_context_dropped
64
+ Scrolls.log(o: "o") do
65
+ Scrolls.context(c: "c") do
66
+ Scrolls.log(ic: "i")
67
+ end
68
+ Scrolls.log(i: "i")
69
+ end
70
+ @out.truncate(25)
71
+ output = "o=o at=start\nc=c ic=i\ni=i"
72
+ assert_equal output, @out.string
73
+ end
74
+
75
+ def test_default_time_unit
76
+ assert_equal "seconds", Scrolls.time_unit
77
+ end
19
78
 
20
- data = {:s => "hello world"}
21
- assert 's="hello world"' == Scrolls::Log.unparse(data)
79
+ def test_setting_time_unit
80
+ Scrolls.time_unit = "milliseconds"
81
+ assert_equal "milliseconds", Scrolls.time_unit
82
+ end
83
+
84
+ def test_setting_incorrect_time_unit
85
+ assert_raise Scrolls::TimeUnitError do
86
+ Scrolls.time_unit = "years"
87
+ end
88
+ end
89
+
90
+ def test_logging
91
+ Scrolls.log(test: "basic")
92
+ assert_equal "test=basic\n", @out.string
93
+ end
22
94
 
23
- data = {:s => "hello world\\"}
24
- assert 's="hello world\"' == Scrolls::Log.unparse(data)
95
+ def test_logging_block
96
+ Scrolls.log(outer: "o") { Scrolls.log(inner: "i") }
97
+ output = "outer=o at=start\ninner=i\nouter=o at=finish elapsed=0.000\n"
98
+ assert_equal output, @out.string
25
99
  end
26
100
 
27
- def test_unparse_floats
28
- data = {:test => 0.3}
29
- assert "test=0.300" == Scrolls::Log.unparse(data)
101
+ def test_log_exception
102
+ begin
103
+ raise Exception
104
+ rescue Exception => e
105
+ Scrolls.log_exception({test: "exception"}, e)
106
+ end
107
+ @out.truncate(27)
108
+ assert_equal "test=exception at=exception", @out.string
30
109
  end
31
110
  end
metadata CHANGED
@@ -1,30 +1,19 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scrolls
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.1
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
8
- - Heroku
8
+ - Curt Micol
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-04-26 00:00:00.000000000 Z
13
- dependencies:
14
- - !ruby/object:Gem::Dependency
15
- name: atomic
16
- requirement: &70343059161980 !ruby/object:Gem::Requirement
17
- none: false
18
- requirements:
19
- - - ~>
20
- - !ruby/object:Gem::Version
21
- version: 1.0.0
22
- type: :runtime
23
- prerelease: false
24
- version_requirements: *70343059161980
12
+ date: 2012-06-20 00:00:00.000000000 Z
13
+ dependencies: []
25
14
  description: Logging, easier, more consistent.
26
15
  email:
27
- - curt@heroku.com
16
+ - asenchi@asenchi.com
28
17
  executables: []
29
18
  extensions: []
30
19
  extra_rdoc_files: []
@@ -35,8 +24,15 @@ files:
35
24
  - README.md
36
25
  - Rakefile
37
26
  - lib/scrolls.rb
27
+ - lib/scrolls/atomic.rb
28
+ - lib/scrolls/log.rb
29
+ - lib/scrolls/parser.rb
30
+ - lib/scrolls/utils.rb
38
31
  - lib/scrolls/version.rb
39
32
  - scrolls.gemspec
33
+ - test/test_atomic.rb
34
+ - test/test_helper.rb
35
+ - test/test_parser.rb
40
36
  - test/test_scrolls.rb
41
37
  homepage: https://github.com/asenchi/scrolls
42
38
  licenses: []
@@ -63,4 +59,7 @@ signing_key:
63
59
  specification_version: 3
64
60
  summary: When do we log? All the time.
65
61
  test_files:
62
+ - test/test_atomic.rb
63
+ - test/test_helper.rb
64
+ - test/test_parser.rb
66
65
  - test/test_scrolls.rb