scrolls 0.1.0 → 0.2.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.
- data/README.md +113 -19
- data/Rakefile +2 -0
- data/lib/scrolls/atomic.rb +58 -0
- data/lib/scrolls/log.rb +196 -0
- data/lib/scrolls/parser.rb +64 -0
- data/lib/scrolls/utils.rb +21 -0
- data/lib/scrolls/version.rb +1 -1
- data/lib/scrolls.rb +96 -151
- data/scrolls.gemspec +8 -8
- data/test/test_atomic.rb +33 -0
- data/test/test_helper.rb +6 -0
- data/test/test_parser.rb +52 -0
- data/test/test_scrolls.rb +99 -20
- metadata +15 -16
data/README.md
CHANGED
@@ -1,8 +1,10 @@
|
|
1
1
|
# Scrolls
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
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
|
-
|
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
|
-
|
70
|
+
app=myapp deploy=production ns=server fn=trap signal=TERM at=exit status=0
|
26
71
|
|
27
|
-
|
72
|
+
This allows us to track this log to `Server#trap` and we received a
|
73
|
+
'TERM' signal and exited 0.
|
28
74
|
|
29
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
38
|
-
|
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
|
-
|
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
@@ -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
|
data/lib/scrolls/log.rb
ADDED
@@ -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
|
data/lib/scrolls/version.rb
CHANGED
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
|
-
|
18
|
-
|
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
|
-
|
22
|
-
|
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
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
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 = ["
|
6
|
-
gem.email = ["
|
7
|
-
gem.description =
|
8
|
-
gem.summary =
|
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
|
-
|
11
|
-
gem.files = `git ls-files`.split(
|
12
|
-
gem.
|
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
|
data/test/test_atomic.rb
ADDED
@@ -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
|
data/test/test_helper.rb
ADDED
data/test/test_parser.rb
ADDED
@@ -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
|
-
|
2
|
-
require "minitest/autorun"
|
1
|
+
require_relative "test_helper"
|
3
2
|
|
4
|
-
|
5
|
-
|
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
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
14
|
-
|
15
|
-
|
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
|
-
|
18
|
-
|
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
|
-
|
21
|
-
|
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
|
-
|
24
|
-
|
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
|
28
|
-
|
29
|
-
|
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
|
4
|
+
version: 0.2.1
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
|
-
-
|
8
|
+
- Curt Micol
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-
|
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
|
-
-
|
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
|