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 +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
|