chalk-log 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,63 @@
1
+ # Thin wrapper over Logging::Logger. This is the per-class object
2
+ # instantiated by the `log` method.
3
+ class Chalk::Log::Logger
4
+ attr_reader :backend
5
+
6
+ # Initialization of the logger backend. It does the actual creation
7
+ # of the various logger methods. Will be called automatically upon
8
+ # your first `log` method call.
9
+ def self.init
10
+ Chalk::Log::LEVELS.each do |level|
11
+ define_method(level) do |*data, &blk|
12
+ return if logging_disabled?
13
+ @backend.send(level, data, &blk)
14
+ end
15
+ end
16
+ end
17
+
18
+ # The level this logger is set to.
19
+ def level
20
+ @backend.level
21
+ end
22
+
23
+ # Set the maximum log level.
24
+ #
25
+ # @param level [Fixnum|String|Symbol] A valid Logging::Logger level, e.g. :debug, 0, 'DEBUG', etc.
26
+ def level=(level)
27
+ @backend.level = level
28
+ end
29
+
30
+ # Create a new logger, and auto-initialize everything.
31
+ def initialize(name)
32
+ # It's generally a bad pattern to auto-init, but we want
33
+ # Chalk::Log to be usable anytime during the boot process, which
34
+ # requires being a little bit less explicit than we usually like.
35
+ Chalk::Log.init
36
+ @backend = ::Logging::Logger.new(name)
37
+ if level = configatron.chalk.log.default_level
38
+ @backend.level = level
39
+ end
40
+ end
41
+
42
+ # Check whether logging has been globally turned off, either through
43
+ # configatron or LSpace.
44
+ def logging_disabled?
45
+ configatron.chalk.log.disabled || LSpace[:'chalk.log.disabled']
46
+ end
47
+
48
+ def with_contextual_info(contextual_info={}, &blk)
49
+ unless blk
50
+ raise ArgumentError.new("Must pass a block to #{__method__}")
51
+ end
52
+ unless contextual_info.is_a?(Hash)
53
+ raise TypeError.new(
54
+ "contextual_info must be a Hash, but got #{contextual_info.class}"
55
+ )
56
+ end
57
+ existing_context = LSpace[:'chalk.log.contextual_info'] || {}
58
+ LSpace.with(
59
+ :'chalk.log.contextual_info' => contextual_info.merge(existing_context),
60
+ &blk
61
+ )
62
+ end
63
+ end
@@ -0,0 +1,84 @@
1
+ module Chalk::Log::Utils
2
+ # Nicely formats a backtrace:
3
+ #
4
+ # ```ruby
5
+ # format_backtrace(['line1', 'line2'])
6
+ # #=> line1
7
+ # line2
8
+ # ```
9
+ #
10
+ # (Used internally when `Chalk::Log` is formatting exceptions.)
11
+ #
12
+ # TODO: add autotruncating of backtraces.
13
+ def self.format_backtrace(backtrace)
14
+ if configatron.chalk.log.compress_backtraces
15
+ backtrace = compress_backtrace(backtrace)
16
+ backtrace << '(Disable backtrace compression by setting configatron.chalk.log.compress_backtraces = false.)'
17
+ end
18
+
19
+ " " + backtrace.join("\n ")
20
+ end
21
+
22
+ # Explodes a nested hash to just have top-level keys. This is
23
+ # generally useful if you have something that knows how to parse
24
+ # kv-pairs.
25
+ #
26
+ # ```ruby
27
+ # explode_nested_hash(foo: {bar: 'baz', bat: 'zom'})
28
+ # #=> {'foo_bar' => 'baz', 'foo_bat' => 'zom'}
29
+ # ```
30
+ def self.explode_nested_hash(hash, prefix=nil)
31
+ exploded = {}
32
+
33
+ hash.each do |key, value|
34
+ new_prefix = prefix ? "#{prefix}_#{key}" : key.to_s
35
+
36
+ if value.is_a?(Hash)
37
+ exploded.merge!(self.explode_nested_hash(value, new_prefix))
38
+ else
39
+ exploded[new_prefix] = value
40
+ end
41
+ end
42
+
43
+ exploded
44
+ end
45
+
46
+ # Compresses a backtrace, omitting gem lines (unless they appear
47
+ # before any application lines).
48
+ def self.compress_backtrace(backtrace)
49
+ compressed = []
50
+ gemdir = Gem.dir
51
+
52
+ hit_application = false
53
+ # This isn't currently read by anything, but we could easily use
54
+ # it to limit the number of leading gem lines.
55
+ leading_lines = 0
56
+ gemlines = 0
57
+ backtrace.each do |line|
58
+ if line.start_with?(gemdir)
59
+ # If we're in a gem, always increment the counter. Record the
60
+ # first three lines if we haven't seen any application lines
61
+ # yet.
62
+ if !hit_application
63
+ compressed << line
64
+ leading_lines += 1
65
+ else
66
+ gemlines += 1
67
+ end
68
+ elsif gemlines > 0
69
+ # If we were in a gem and now are not, record the number of
70
+ # lines skipped.
71
+ compressed << "<#{gemlines} #{gemlines == 1 ? 'line' : 'lines'} omitted>"
72
+ compressed << line
73
+ hit_application = true
74
+ gemlines = 0
75
+ else
76
+ # If we're in the application, always record the line.
77
+ compressed << line
78
+ hit_application = true
79
+ end
80
+ end
81
+
82
+ compressed
83
+ end
84
+ end
@@ -0,0 +1,5 @@
1
+ module Chalk
2
+ module Log
3
+ VERSION = '0.1.2'
4
+ end
5
+ end
@@ -0,0 +1,8 @@
1
+ :tddium:
2
+ :ruby_version: ruby-1.9.3-p194
3
+ :bundler_version: 1.3.5
4
+ :test_pattern:
5
+ - "test/wholesome/[^_]*.rb"
6
+ - "test/unit/[^_]*.rb"
7
+ - "test/integration/[^_]*.rb"
8
+ - "test/functional/[^_]*.rb"
@@ -0,0 +1,16 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ require 'minitest/autorun'
5
+ require 'minitest/spec'
6
+ require 'mocha/setup'
7
+
8
+ module Critic
9
+ require_relative '_lib/fake'
10
+
11
+ class Test < ::MiniTest::Spec
12
+ def setup
13
+ # Put any stubs here that you want to apply globally
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,5 @@
1
+ module Critic::Fake
2
+ Dir[File.expand_path('../fake/*.rb', __FILE__)].each do |file|
3
+ require_relative(file)
4
+ end
5
+ end
@@ -0,0 +1,9 @@
1
+ class Critic::Fake::Event
2
+ attr_reader :data, :time, :level
3
+
4
+ def initialize(opts)
5
+ @data = opts.fetch(:data)
6
+ @time = opts.fetch(:time, Time.new(1979,4,9))
7
+ @level = opts.fetch(:level, 1)
8
+ end
9
+ end
@@ -0,0 +1,10 @@
1
+ require File.expand_path('../../_lib', __FILE__)
2
+
3
+ module Critic::Functional
4
+ module Stubs
5
+ end
6
+
7
+ class Test < Critic::Test
8
+ include Stubs
9
+ end
10
+ end
@@ -0,0 +1,188 @@
1
+ require File.expand_path('../_lib', __FILE__)
2
+
3
+ require 'chalk-log'
4
+
5
+ module Critic::Functional
6
+ class LogTest < Test
7
+ def enable_timestamp
8
+ configatron.chalk.log.stubs(:timestamp).returns(true)
9
+ end
10
+
11
+ def disable_timestamp
12
+ configatron.chalk.log.stubs(:timestamp).returns(false)
13
+ end
14
+
15
+ def disable_pid
16
+ configatron.chalk.log.stubs(:pid).returns(false)
17
+ end
18
+
19
+ def disable_tagging
20
+ configatron.chalk.log.stubs(:tagging).returns(false)
21
+ end
22
+
23
+ before do
24
+ Chalk::Log.init
25
+ Process.stubs(:pid).returns(9973)
26
+ disable_timestamp
27
+ end
28
+
29
+ class MyClass
30
+ include Chalk::Log
31
+ end
32
+
33
+ describe 'when called without arguments' do
34
+ it 'does not loop infinitely' do
35
+ MyClass.log.info
36
+ end
37
+ end
38
+
39
+ describe 'when called with a message' do
40
+ it 'does not mutate the input' do
41
+ canary = "'hello, world!'"
42
+ baseline = canary.dup
43
+ MyClass.log.info(canary)
44
+ assert_equal(baseline, canary)
45
+ end
46
+ end
47
+
48
+ describe 'layout' do
49
+ before do
50
+ @layout = MyClass::Layout.new
51
+ end
52
+
53
+ def layout(opts)
54
+ event = Critic::Fake::Event.new(opts)
55
+ formatted = @layout.format(event)
56
+
57
+ # Everything should end with a newline, but they're annoying
58
+ # to have to test elsewhere, so strip it away.
59
+ assert_equal("\n", formatted[-1], "Layout did not end with a newline: #{formatted.inspect}")
60
+ formatted.chomp
61
+ end
62
+
63
+ it 'log entry from info' do
64
+ rendered = layout(data: ["A Message", {:key1 => "ValueOne", :key2 => ["An", "Array"]}])
65
+ assert_equal('[9973] A Message: key1=ValueOne key2=["An","Array"]', rendered)
66
+ end
67
+
68
+ it 'logs the action_id correctly' do
69
+ LSpace.with(action_id: 'action') do
70
+ rendered = layout(data: ["A Message", {:key1 => "ValueOne", :key2 => ["An", "Array"]}])
71
+ assert_equal('[9973|action] A Message: key1=ValueOne key2=["An","Array"]', rendered)
72
+ end
73
+ end
74
+
75
+ it 'logs timestamp correctly' do
76
+ enable_timestamp
77
+ LSpace.with(action_id: 'action') do
78
+ rendered = layout(data: ["A Message", {:key1 => "ValueOne", :key2 => ["An", "Array"]}])
79
+ assert_equal('[1979-04-09 00:00:00.000000] [9973|action] A Message: key1=ValueOne key2=["An","Array"]', rendered)
80
+ end
81
+ end
82
+
83
+ it 'logs without pid correctly' do
84
+ disable_pid
85
+ LSpace.with(action_id: 'action') do
86
+ rendered = layout(data: ["A Message", {:key1 => "ValueOne", :key2 => ["An", "Array"]}])
87
+ assert_equal('[action] A Message: key1=ValueOne key2=["An","Array"]', rendered)
88
+ end
89
+ end
90
+
91
+ it 'log from info hash without a message' do
92
+ rendered = layout(data: [{:key1 => "ValueOne", :key2 => ["An", "Array"]}])
93
+ assert_equal('[9973] key1=ValueOne key2=["An","Array"]', rendered)
94
+ end
95
+
96
+ it 'renders [no backtrace] as appropriate' do
97
+ rendered = layout(data: ["Another Message", StandardError.new('msg')])
98
+ assert_equal("[9973] Another Message: error_class=StandardError error=msg\n[9973] [no backtrace]", rendered)
99
+ end
100
+
101
+ it 'renders when given error and info hash' do
102
+ rendered = layout(data: ["Another Message", StandardError.new('msg'), {:key1 => "ValueOne", :key2 => ["An", "Array"]}])
103
+ assert_equal(%Q{[9973] Another Message: key1=ValueOne key2=["An","Array"] error_class=StandardError error=msg\n[9973] [no backtrace]}, rendered)
104
+ end
105
+
106
+ it 'renders an error with a backtrace' do
107
+ error = StandardError.new('msg')
108
+ backtrace = ["a fake", "backtrace"]
109
+ error.set_backtrace(backtrace)
110
+
111
+ rendered = layout(data: ["Yet Another Message", error])
112
+ assert_equal("[9973] Yet Another Message: error_class=StandardError error=msg\n[9973] a fake\n[9973] backtrace", rendered)
113
+ end
114
+
115
+ it 'renders an error passed alone' do
116
+ error = StandardError.new('msg')
117
+ backtrace = ["a fake", "backtrace"]
118
+ error.set_backtrace(backtrace)
119
+
120
+ rendered = layout(data: [error])
121
+ assert_equal("[9973] error_class=StandardError error=msg\n[9973] a fake\n[9973] backtrace", rendered)
122
+ end
123
+
124
+ it 'handles bad unicode' do
125
+ rendered = layout(data: [{:key1 => "ValueOne", :key2 => "\xC3"}])
126
+ assert_equal("[9973] key1=ValueOne key2=\"\\xC3\" [JSON-FAILED]", rendered)
127
+ end
128
+
129
+ it 'allows disabling tagging' do
130
+ enable_timestamp
131
+ disable_tagging
132
+
133
+ LSpace.with(action_id: 'action') do
134
+ rendered = layout(data: [{:key1 => "ValueOne", :key2 => "Value Two"}])
135
+ assert_equal(%Q{key1=ValueOne key2="Value Two"}, rendered)
136
+ end
137
+ end
138
+
139
+ describe 'faults' do
140
+ it 'shows an appropriate error if the invalid arguments are provided' do
141
+ rendered = layout(data: ['foo', nil])
142
+
143
+ lines = rendered.split("\n")
144
+ assert_equal('[Chalk::Log fault: Could not format message] error_class="Chalk::Log::InvalidArguments" error="Invalid leftover arguments: [\"foo\", nil]"', lines[0])
145
+ assert(lines.length > 1)
146
+ end
147
+
148
+ it 'handles single faults' do
149
+ e = StandardError.new('msg')
150
+ @layout.expects(:do_format).raises(e)
151
+ rendered = layout(data: ['hi'])
152
+
153
+ lines = rendered.split("\n")
154
+ assert_equal('[Chalk::Log fault: Could not format message] error_class=StandardError error=msg', lines[0])
155
+ assert(lines.length > 1)
156
+ end
157
+
158
+ it 'handles double-faults' do
159
+ e = StandardError.new('msg')
160
+ def e.to_s; raise 'Time to double-fault'; end
161
+
162
+ @layout.expects(:do_format).raises(e)
163
+ rendered = layout(data: ['hi'])
164
+
165
+ lines = rendered.split("\n")
166
+ assert_match(/Chalk::Log fault: Double fault while formatting message/, lines[0])
167
+ assert_equal(1, lines.length, "Lines: #{lines.inspect}")
168
+ end
169
+
170
+ it 'handles triple-faults' do
171
+ e = StandardError.new('msg')
172
+ def e.to_s
173
+ f = StandardError.new('double')
174
+ def f.to_s; raise 'Time to triple fault'; end
175
+ raise f
176
+ end
177
+
178
+ @layout.expects(:do_format).raises(e)
179
+ rendered = layout(data: ['hi'])
180
+
181
+ lines = rendered.split("\n")
182
+ assert_match(/Chalk::Log fault: Triple fault while formatting message/, lines[0])
183
+ assert_equal(1, lines.length, "Lines: #{lines.inspect}")
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,296 @@
1
+ require File.expand_path('../_lib', __FILE__)
2
+
3
+ require 'chalk-log'
4
+
5
+ module Critic::Functional
6
+ class LogTest < Test
7
+ def self.add_appender
8
+ unless @appender
9
+ @appender = ::Logging.appenders.string_io(
10
+ 'test-stringio',
11
+ layout: Chalk::Log.layout
12
+ )
13
+ ::Logging.logger.root.add_appenders(@appender)
14
+ end
15
+ @appender
16
+ end
17
+
18
+ before do
19
+ Chalk::Log.init
20
+ end
21
+
22
+ describe 'when a class has included Log' do
23
+ it 'instances are loggable' do
24
+ class MyClass
25
+ include Chalk::Log
26
+ end
27
+
28
+ MyClass.new.log.info("Hi!")
29
+ end
30
+
31
+ it 'the class is loggable' do
32
+ class YourClass
33
+ include Chalk::Log
34
+ end
35
+
36
+ YourClass.log.info("Hi!")
37
+ end
38
+ end
39
+
40
+ describe 'including a loggable module into another' do
41
+ describe 'the inclusions are straightline' do
42
+ it 'make the includee loggable' do
43
+ module LogTestA
44
+ include Chalk::Log
45
+ end
46
+
47
+ module LogTestB
48
+ include LogTestA
49
+ end
50
+
51
+ assert(LogTestB < Chalk::Log)
52
+ assert(LogTestB.respond_to?(:log))
53
+ end
54
+
55
+ it 'preserves any custom include logic prior to Log inclusion' do
56
+ module CustomLogTestA
57
+ def self.dict=(dict)
58
+ @dict = dict
59
+ end
60
+
61
+ def self.dict
62
+ @dict
63
+ end
64
+
65
+ def self.included(other)
66
+ dict['included'] = true
67
+ end
68
+
69
+ include Chalk::Log
70
+ end
71
+
72
+ dict = {}
73
+ CustomLogTestA.dict = dict
74
+
75
+ module CustomLogTestB
76
+ include CustomLogTestA
77
+ end
78
+
79
+ assert(CustomLogTestB < Chalk::Log)
80
+ assert(CustomLogTestB.respond_to?(:log))
81
+ assert_equal(true, dict['included'])
82
+ end
83
+
84
+ # TODO: it'd be nice if this weren't true, but I'm not sure
85
+ # how to get a hook when a method is overriden.
86
+ it 'custom include logic after Log inclusion clobbers the default include logic' do
87
+ module CustomLogTestC
88
+ def self.dict=(dict)
89
+ @dict = dict
90
+ end
91
+
92
+ def self.dict
93
+ @dict
94
+ end
95
+
96
+ include Chalk::Log
97
+
98
+ def self.included(other)
99
+ dict['included'] = true
100
+ end
101
+ end
102
+
103
+ dict = {}
104
+ CustomLogTestC.dict = dict
105
+
106
+ module CustomLogTestD
107
+ include CustomLogTestC
108
+ end
109
+
110
+ assert(CustomLogTestD < Chalk::Log)
111
+ assert(!CustomLogTestD.respond_to?(:log))
112
+ assert_equal(true, dict['included'])
113
+ end
114
+ end
115
+ end
116
+
117
+ describe 'extending a Log module into another' do
118
+ describe 'the inclusions are straightline' do
119
+ it 'make the extendee loggable' do
120
+ module ExtendLogTestA
121
+ include Chalk::Log
122
+ end
123
+
124
+ module ExtendLogTestB
125
+ extend ExtendLogTestA
126
+ end
127
+
128
+ assert(ExtendLogTestB < Chalk::Log)
129
+ assert(ExtendLogTestB.respond_to?(:log))
130
+ end
131
+ end
132
+ end
133
+
134
+ describe 'when a class is loggable' do
135
+ class MyLog
136
+ include Chalk::Log
137
+ end
138
+
139
+ it 'log.warn works' do
140
+ msg = 'msg'
141
+ # For some reason this isn't working:
142
+ MyLog.log.backend.expects(:warn).once
143
+ MyLog.log.warn(msg)
144
+ end
145
+
146
+ it 'log.info works' do
147
+ msg = 'msg'
148
+ MyLog.log.backend.expects(:info).once
149
+ MyLog.log.info(msg)
150
+ end
151
+
152
+ it 'accepts blocks' do
153
+ class LogTestE
154
+ include Chalk::Log
155
+ end
156
+ LogTestE.log.level = "INFO"
157
+
158
+ LogTestE.log.debug { assert(false, "DEBUG block called when at INFO level") }
159
+ called = false
160
+ LogTestE.log.info { called = true; "" }
161
+ assert(called, "INFO block not called at INFO level")
162
+ end
163
+ end
164
+
165
+ class TestLogInstanceMethods < Test
166
+ include Chalk::Log
167
+
168
+ before do
169
+ TestLogInstanceMethods.log.level = 'INFO'
170
+ Chalk::Log.init
171
+ @appender = LogTest.add_appender
172
+ end
173
+
174
+ def assert_logged(expected_string, *args)
175
+ assert_includes(@appender.sio.string, expected_string, *args)
176
+ end
177
+
178
+ it 'accepts blocks on instance methods' do
179
+ called = false
180
+ log.debug { assert(false, "DEBUG block called at INFO") }
181
+ log.info { called = true; "" }
182
+ assert(called, "INFO block not called at INFO level")
183
+ end
184
+
185
+ describe 'when contextual info is set' do
186
+
187
+ it 'adds contextual information to `.info` log lines' do
188
+ log.with_contextual_info(key: 'value') {log.info("message")}
189
+ assert_logged("key=value")
190
+ end
191
+
192
+ it 'nests contexts' do
193
+ log.with_contextual_info(top_key: "top_value") do
194
+ log.info("top message")
195
+ log.with_contextual_info(inner_key: "inner_value") do
196
+ log.info("inner message")
197
+ end
198
+ end
199
+ %w{top_key=top_value inner_key=inner_value}.each do |pair|
200
+ assert_logged(pair)
201
+ end
202
+ end
203
+
204
+ it 'requires a block' do
205
+ exn = assert_raises(ArgumentError) do
206
+ log.with_contextual_info(i_am_not: "passing a block")
207
+ end
208
+ assert_includes(exn.message, "Must pass a block")
209
+ end
210
+
211
+ it 'requires its argument must be a hash' do
212
+ exn = assert_raises(TypeError) do
213
+ log.with_contextual_info('not a hash') {}
214
+ end
215
+ assert_includes(exn.message, 'must be a Hash')
216
+ end
217
+ end
218
+ end
219
+
220
+ describe 'when chaining includes and extends' do
221
+ it 'correctly make the end class loggable' do
222
+ module Base1
223
+ include Chalk::Log
224
+ end
225
+
226
+ class Child1
227
+ extend Base1
228
+ end
229
+
230
+ Child1.log.info("Hello!")
231
+ assert(true)
232
+ end
233
+
234
+ it 'correctly make the end class loggable when chaining an include and extend' do
235
+ module Base2
236
+ include Chalk::Log
237
+ end
238
+
239
+ module Middle2
240
+ extend Base2
241
+ end
242
+
243
+ class Child2
244
+ include Middle2
245
+ end
246
+
247
+ Child2.log.info("Hello!")
248
+ assert(true)
249
+ end
250
+
251
+ it 'correctly make the end class loggable when chaining an extend and an extend' do
252
+ module Base3
253
+ include Chalk::Log
254
+ end
255
+
256
+ module Middle3
257
+ extend Base3
258
+ end
259
+
260
+ class Child3
261
+ extend Middle3
262
+ end
263
+
264
+ Child3.log.info("Hello!")
265
+ assert(true)
266
+ end
267
+
268
+ it 'correctly make the end class loggable when it has already included loggable' do
269
+ module Base4
270
+ include Chalk::Log
271
+ end
272
+
273
+ module Middle4
274
+ extend Base4
275
+ end
276
+
277
+ class Child4
278
+ include Chalk::Log
279
+ extend Middle4
280
+ end
281
+
282
+ Child4.log.info("Hello!")
283
+ assert(true)
284
+ end
285
+ end
286
+
287
+ it 'correctly makes a module loggable' do
288
+ module Base5
289
+ include Chalk::Log
290
+ end
291
+
292
+ Base5.log.info("Hello!")
293
+ assert(true)
294
+ end
295
+ end
296
+ end